Skip to content

Releases

Every version of this specification: what changed in it, and what you can still fetch. Subscribe to releases.xml to be told when it changes — the feed carries the entry and links the package directly, so a feed reader or a Teams/Slack connector is enough to keep up.

Every release

Version Date Changes Package
1.25.1 2026-09-10 5 added, 6 breaking changes, 3 document status reasons, salient fields, and form-template field mapping nexus-1.25.1.tgz current
1.25.0 2026-08-31 breaking · 7 changed, 3 removed, 1 fixed nexus-1.25.0.tgz
1.24.0 2026-08-29 5 added, 3 changed, 1 removed nexus-1.24.0.tgz
1.23.0 2026-08-29 breaking · 6 added, 4 changed, 1 removed nexus-1.23.0.tgz
1.22.0 2026-08-26 breaking · 6 added, 15 changed, 1 removed nexus-1.22.0.tgz
1.21.1 2026-08-26 7 added, 4 changed nexus-1.21.1.tgz
1.21.0 2026-08-25 breaking · 13 added, 13 changed, 3 removed, 4 fixed nexus-1.21.0.tgz
1.20.0 2026-08-24 breaking · 14 added, 31 changed, 1 removed, 4 fixed nexus-1.20.0.tgz
1.19.0 2026-08-21 breaking · 4 added, 23 changed, 2 deprecated nexus-1.19.0.tgz
1.18.0 2026-08-19 12 added, 5 notes for implementers nexus-1.18.0.tgz
1.17.0 2026-08-16 breaking · 7 added, 3 changed, 16 fixed nexus-1.17.0.tgz
1.16.0 2026-08-14 breaking · 2 added, 4 changed, 3 fixed nexus-1.16.0.tgz
1.15.1 2026-08-12 16 added, 3 changed, 1 removed, 3 fixed nexus-1.15.1.tgz
1.15.0 2026-08-11 withdrawn nexus-1.15.0.tgz
1.14.1 2026-08-09 2 added, 4 fixed not archived
1.14.0 2026-08-09 3 added, 8 changed, 4 fixed not archived
1.13.0 skipped — no withdrawn release, no aborted branch
1.12.0 2026-08-05 6 added, 7 changed, 3 fixed not archived
1.11.0 2026-08-02 18 added, 4 changed, 4 removed, 5 fixed not archived
1.10.0 2026-07-23 1 added, 2 changed, 1 removed, 1 fixed not archived

Packages are archived from 1.15.0 onward. Earlier releases were built but not kept, so their package can be rebuilt from source (the FSH for any release is in the repository's history) but is not served here — nothing is missing from the record, only the prebuilt artifact. A tarball is self-describing once downloaded: its own package.json carries the name and version, and the archived copies are named nexus-<version>.tgz so the version is visible before you open it.

This IG is a fork of the Polaris FHIR IG taken at 1.10.0 (2026-07-23); releases before that belong to the upstream spec and its own history.

Versions are sequential, not SemVer. MAJOR is reserved for the release that declares this specification stable -- a decision to be taken deliberately and announced, not one to arrive at by arithmetic. Until then MINOR moves on any change and PATCH on fixes, which means the changelog carries the compatibility contract instead of the version number: a breaking change is named as breaking, in bold, with the consumer action spelled out. Read the entries rather than inferring safety from the number. A caret range on this package is not safe, and is not meant to be.

Each release heading records the TypedFhir version that generated it. This IG is consumed through TypedFhir's typed accessors, validators and dispatch tables rather than as raw StructureDefinitions, so a change to the generator is a change to what the IG means in practice and belongs here rather than in a separate library changelog. The two version numbers move independently -- TypedFhir tracks the library and vendors several IGs at once -- so "which library produced this release's artifacts" would otherwise be unanswerable after the fact.

Artifact names link to the spec. The first mention of a profile, value set, code system or handbook topic in an entry links to its page, so "what changed" and "what it is now" are one click apart rather than a search away. On the published site those links resolve; reading this file directly on GitHub, they will not, because they are written relative to the site and are validated against it by the build.

What changed, release by release

[1.25.1] - 2026-09-10

Generated by TypedFhir 0.15.0. Unchanged from 1.25.0; the library moves independently of this specification and did not need to move for this release.

Four bodies of work, six breaking changes. Read Breaking changes first.

The privacy and record-integrity model is the release. Item consent gains the three things a clinic actually asks of it -- items the clinic defines itself, an answer that can expire, and a record of which wording was agreed to -- and a statement of disagreement can now point at an element rather than a whole resource. Two smaller bodies of work were carried in [Unreleased] before all of it and ship here: a status reason on DocumentReference, and salient fields plus form-template field mapping.

A patient can restrict their record, and dispute what it says

See: Consent, Privacy and Record Integrity, Privacy Directives, Consent Override, Corrections, Statements of Disagreement, Security Labels, Privacy Directive, Statement of Disagreement

First pass: these controls are recorded here, and must be enforced by an implementation

This release defines how a restriction, an override, a correction, a disagreement and a disclosure label are recorded, and states what a conformant implementation must then do with each. No element of the model enforces itself.

A consumer of this API must apply the policy itself. No guarantee is made that a restricted resource has been filtered out of a response, or that a disclosure has been refused on your behalf. Each handbook page names the surfaces its obligation reaches.

The Privacy Directive profile models a standing patient restriction on the collection, use or disclosure of their own information under Ontario's Personal Health Information Protection Act -- the first Consent in this guide that expresses a restriction rather than a permission. The Statement of Disagreement profile carries an individual's own words about information a custodian refused to correct, with the associations a release path needs to include it wherever the disputed information goes.

Two extensions fill gaps R4 leaves. Corrected By points from a record known to be wrong to the one that supersedes it: R4 gives DocumentReference a relatesTo and gives Observation, Condition and Patient nothing, so a reader holding the stale record has no address to go to. Record Severed marks content retained and traceable but removed from ordinary clinical view -- a statement about visibility, which no status code expresses, because status is about whether content is true.

Correction requests are ordinary clinical Tasks under a new privacy-request bin in Task code. There is no correction Task profile: statutory work with a response clock has to land in the worklist people already read. HL7's Patient Request for Corrections vocabulary is adopted for task types and business status; its profiles and its package dependency are not.

See: Consent, Privacy and Record Integrity, Item Consent, App Consent, Consent Family

Consent.provision.type on the ROOT provision now carries the decision on every Consent profile in this guide. permit grants, deny restricts, and status says only whether the record is operative.

Read provision.type. status = active means the record is in force, and nothing more.

Before this release the three families disagreed. Item consent carried its answer in status alone -- active was yes, rejected was no -- while app consent expressed refusal as a nested provision and a grant as the ABSENCE of one. That was survivable while those were the only two Consents in the store. It stopped being survivable when a privacy directive arrived, because an active directive means the patient said no: one element then meant opposite things across families sharing a resource type, a scope and a search, and a consumer that read status first read a lockbox as permission.

Two smaller faults went with it. A grant expressed as the absence of a nested denial is not a statement, so a truncated or partially-written app consent read as a grant. And rejected is an R4 lifecycle code -- a party rejected the consent -- which is the wrong kind of thing to carry a patient's answer.

R4's element text says type is "not permitted in root rule", and this guide does not follow that reading. IHE Privacy Consent on FHIR and Ontario's Provincial Consent Override Interface both constrain the root type to 1.., because a consent whose base rule states no decision states none where a reader looks first. Following the prior art puts the decision where a PCF or PCOI reader already looks. A consumer meeting a Consent with no root type MUST NOT infer permit; treat it as malformed and fail closed.

This is also the shorter path to R5, which moves the grant/deny answer out of status into an explicit decision element and drops rejected from the status value set. R5 is doing to Consent what this release does to these profiles, for the same reason.

The Consent Family code system, required on category and searchable, remains -- but it now answers only what a decision governs, not how to read it.

Break-glass is deliberately not a fourth family. It is an exception inside the patient's own directive -- a nested permit with purpose = BTG -- following IHE Privacy Consent on FHIR, and the record that somebody actually invoked it is an AuditEvent following IHE BALP, with the reason on agent.purposeOfUse from consent override grounds. Those grounds are TREAT, BTG and PATRQT, shaped by what primary care actually does: the ordinary legitimate override is a covering clinician treating the patient in front of them, not an emergency, and a vocabulary offering only emergency grounds produces a reason field everybody fills in dishonestly. Where none fits, the purpose carries free text and no coding.

Notice of an override goes to the patient's most responsible provider as a clinical Task under a new privacy-notice bin in Task code -- distinct from privacy-request, because a notice is raised by the system and carries no statutory clock.

Security labels, including the notice that a record has a gap

See: Security Labels, Disclosure control labels, Disclosure completeness labels

Disclosure control labels gives operational meaning to four HL7 v3 ActCode RefrainPolicy codes on meta.security: NODSCLCD for a record restricted from transmission, and NORDSCLCD for a record ingested under a consent override, which must be identifiable as usable only for the purpose it was collected for.

Disclosure completeness labels carries REDACTED, and it exists because suppressing information is only half of a lockbox. Where a custodian discloses for the provision of health care and the individual has restricted part of the record, PHIPA obliges the custodian to notify the recipient that what they received is incomplete. A receiving clinician who cannot see the gap reads the record as whole and treats the patient on that basis, which turns a privacy choice into a clinical hazard. REDACTED rather than SUBSETTED deliberately: SUBSETTED carries the usage note that it is not suitable where information was removed for security reasons.

Every code in both value sets is already a member of R4's security-labels value set, so a resource carrying one is structurally valid against base R4 without this guide. A label declares an obligation; it does not implement one. A producer must apply the label wherever the stated condition holds, and a consumer that cannot honour a label it encounters must refuse the disclosure rather than proceed without honouring it.

These pages state requirements, not status. Every page in this section, and the Privacy Directive and Statement of Disagreement profile pages, say what a conformant implementation must do on each surface -- which paths a directive has to be evaluated on, when a label has to be applied, when a disclosure has to be refused, and where the specification itself stops (no criteria-based data selector, no element-level segmentation, no status code for a released access request). The obligations are unchanged; they are stated as the specification rather than as a snapshot of one deployment, and are therefore still accurate next release. No profile, extension, value set or code changes shape.

Access requests, the response clock, and privacy agreements

See: Access Requests, Consent, Privacy and Record Integrity, Corrections, Consent Override, clinical Task, Task code

This is documentation. No profile, extension or value set changes shape here, and no code is minted; the only source change is comment text on elements that already existed.

An individual's right of access to their own record has a page. The request is an ordinary clinical Task in the same privacy-request bin as a correction -- that code covers access by its own definition, because it is named for the right being exercised rather than the outcome -- with Task.focus naming the responsive records and Task.statusReason carrying the provision relied on where access is refused. Access is also the precondition for a correction request, and the two pages now say so and link.

The statutory response clock is Task.restriction.period. Its end is the due date, per the base element definition, so a privacy request's deadline sorts in any worklist that already sorts on due dates and no privacy-specific element is needed to hold it. Which duration applies is a question for the governing legislation and, for records held by another custodian, for that custodian; where the date goes is not. Do not write it on Task.executionPeriod, which means when the work is being or was done.

An access decision has no status vocabulary yet. The correction business status codes carry over for intake (queued, in-review, waiting-for-information, requester-cancelled) and for refusal (denied), but the terminal states describe a record being amended and an access request does not amend a record: nothing in the vocabulary says released, or released in part. No local code is minted for it. What would settle it: an access vocabulary published elsewhere and adopted the same way this one was, or a release path whose own states are worth naming.

Regulatory reporting is a query, not an artifact. Counts of requests, and of refusals with the provisions invoked, come out of the request Tasks grouped by Task.businessStatus -- refusals are the denied bucket, the provision is on statusReason, Task.code selects privacy work and Task.authoredOn bounds the period. Because those buckets are HL7's published vocabulary rather than a private status column, the report is portable: a denied count means the same thing in any store carrying the vocabulary, with no mapping table in between.

Watermarking belongs to the export layer. A watermark is a property of a rendered output rather than of a resource -- the same content released to two recipients carries two different watermarks -- so nothing is modelled for it. What travels with a released packet is the Provenance of the assembly and the REDACTED completeness marker on the packet's meta.security.

Accepting a privacy agreement is a Consent, and this guide does not model it yet. consentcategorycodes#npp -- "Notice of Privacy Practices", defined as acknowledgement of a custodian's notice of privacy practices -- is HL7's code for that family, so nothing needs minting. Presenting a notice is an event and belongs in AuditEvent; the patient's acceptance or refusal is a standing decision and belongs in a Consent stating it on provision.type, like every other family here. No profile is added and no code is added to Consent Family in this release: a family code in a required, searchable element with no profile behind it would carry no contract for reading the resource. What would settle it: a surface that captures the answer, and a consumer that reads it.

See: Item Consent, Statements of Disagreement, Item Consent, Statement of Disagreement, Consent Item

Clinic-defined consent items, without a vendor catalogue of them. No code is added to Consent Item for photography, program contact or a student sitting in, and none will be: a vendor-maintained list of every consent a clinic might invent is incomplete on the day it ships, and each addition costs the clinic a release it has to wait for. Instead item consent gains a second category slice, clinicDefined, carrying LOINC 59284-0 "Patient Consent" as its marker plus a tenant-scoped coding with the clinic's own code and a human-readable text.

The marker is chosen rather than minted, and it earns the job twice: it is a member of R4's own consent-category value set, so a clinic-defined item consent satisfies a base extensible binding that a nexus-consent-item coding cannot, and it is the single category IHE Privacy Consent on FHIR puts on every consent it profiles -- so an outside reader that knows nothing of this guide still recognises the resource as a patient consent. It identifies no item on its own, which is why the tenant coding and text are both required beside it: a tenant code system is not resolvable by an outside consumer, so without text there is nothing to render.

category[item] and category[clinicDefined] are both 0..1 and the new item-consent-item-is-identified-once invariant (error) requires exactly one. Carrying both is two answers to which item is this?; carrying neither is a decision the lookup search never returns, which is worse than no record because staff believe it was captured.

The search that lists a patient's item consents has to ask for both markers now. A clinic-defined coding is not in the catalogue system, so category=<catalogue>| returns none of them and every clinic-defined item reads as Not Recorded. One token search with two values covers both: category=https://fhir.apps.health/CodeSystem/nexus-consent-item|,http://loinc.org|59284-0.

A tenant MUST NOT reuse a catalogue code in its own system. The recommended deterministic identifier is {patient-id}:{item-code} with the code system dropped, so a clinic-defined item coded sms computes the governed sms item's key exactly. The next conditional update on either matches the other and versions it: one consent silently replaces another, both resources are individually valid, and nothing reports it. Nothing in this specification detects the collision and nothing can -- a profile cannot validate a tenant catalogue it has never seen -- so the control is a naming rule where the tenant catalogue is authored. Writers that read-then-update by logical id are not exposed to it, the one case where that path is safer than the conditional update.

A consent item can now be time-bounded, on provision.period, and an elapsed period.end is not permission. A consumer reads the period as well as provision.type. Nothing filters an expired consent out of a search result and status stays active, because the record is still an operative record of what was decided -- so a reader that finds permit and stops will email a patient whose consent ran out last year, with an audit trail showing it had permission. Outside the period there is no answer whichever way the decision points: a lapsed permit is not permission and a lapsed deny is not a refusal to rely on.

A lapsed consent is not a fourth display state. It maps onto the state the model already has for there is no answer here -- the one a missing Consent and an entered-in-error Consent map onto: unanswered in the UI, a no at the gate, a prompt to ask again. It is emphatically not Declined, since the patient never refused. No invariant is added for expiry, deliberately: a resource whose period has ended is an accurate record rather than a malformed one, so an error would be wrong, and a warning would fire on every correctly-recorded lapsed consent and on every historical version that was in force when written, which turns the guide's warnings into noise. R4's per-1 already refuses a period whose start is after its end.

policy.uri is documented as the home of the consent WORDING, and should be versioned. No element changes shape: policy.uri has been required on item consent since it replaced the retired policyRule pin earlier in this release. What it is for is now stated. A consent is an answer to a specific piece of text, and the text changes; a URI naming the policy but not its version cannot answer what did this patient actually agree to. Re-wording an item therefore produces a new policy.uri, not an edited one, and the old URI keeps resolving for the patients who answered it. This matters most for a clinic-defined item, whose text no published catalogue holds. Nothing enforces it -- the element is a URI, and this specification cannot tell a versioned one from an unversioned one.

A statement of disagreement can point at an element, not just a resource. about on Statement of Disagreement accepts HL7's targetPath extension (0..*, from the hl7.fhir.uv.extensions.r4 dependency this guide already pins), naming the disputed element as a FHIRPath expression -- Observation.value.ofType(Quantity) rather than the whole Observation. Put it on every reference to the record, versioned and unversioned alike, for the same reason both references exist: a focus carried on only one is lost to whichever consumer reads the other.

Two upstream limits travel with it. The extension is draft, so expect its definition to move. And its FHIRPath is the simple subset with .resolve() additionally disallowed, so it addresses within one resource and cannot reach across a reference. Neither narrows the obligation: it still attaches to the record, and a release path that cannot honour element-level targeting carries the statement against the whole resource -- coarser, and correct. Dropping the statement because the path could not be resolved is the one outcome the right exists to prevent.

FHIR-466.

Breaking changes

See: App Consent, Item Consent, Communication

  • Consent.policyRule is no longer fixed to cric on app consent and item consent; both now require policy.uri instead. consentpolicycodes#cric is 45 CFR 46 -- the United States Common Rule for informed consent in human-subjects research. It is not a communication-preference policy, not a data-sharing policy, and not Canadian, and the code system it belongs to contains no Canadian concept at all: its 27 codes are US federal and state instruments plus three European ones. Rather than substitute another wrong code, both profiles now name the actual governing policy by URI, which satisfies R4's ppc-1 invariant and is jurisdictionally accurate.

    Consumer action: a writer that emitted the fixed policyRule must emit policy.uri carrying the applicable policy instead; a reader that keyed on cric to recognise a Nexus consent must key on category. Every shipped example is migrated in this release.

  • Communication.sender and .recipient now admit RelatedPerson. Rights over a record are exercisable by an individual and their substitute decision-makers, and a proxy must not be flattened onto the Patient: where a right is exercised by proxy the record has to show that authority was delegated, because that is what gets examined when the exercise is challenged.

    Consumer action: a reader that switched exhaustively on the reference target type of sender or recipient will meet a type it has not seen. Widening a reference is breaking for consumers even though it is additive for writers.

  • Item consent moves its decision from status to provision.type, and stops writing rejected. Provided is now (status = active, provision.type = permit); Declined or withdrawn is (status = active, provision.type = deny). provision moves from 0..1 reserved-and-unused to 1..1 with a required type.

    Consumer action: a reader that tested status = 'active' for Provided and status = 'rejected' for Declined must read provision.type instead. A reader that treated any non-active status as "not permitted" now sees active on a declined item and will read it as permission unless it changes. Every shipped example is migrated in this release.

  • App consent moves its decision to the root provision.type. Refusal was a nested provision with type = deny; a grant was the absence of one. Both are now stated on provision.type at the root, which is 1..1.

    Consumer action: a reader that walked the provision tree looking for a nested deny must read the root provision.type. A writer that expressed a grant by omitting the nested denial must now state permit.

  • QuestionnaireResponse is now listed in the server capability statement. The profile has declared read-only SMART exposure since the release that gave every scope a reachable permission, but the statement did not list the type, so a scope naming it resolved to nothing and an application asking for it was refused. Read-only: an application that could rewrite a response could rewrite an answer after the fact.

    Consumer action: none for existing clients. An application that previously had questionnaireresponse:read refused may now be granted it.

FHIR-465.

  • Consent.category[item] moves from 1..1 to 0..1. It is no longer the only way an item consent identifies its item: a clinic-defined item carries no catalogue coding at all, and identifies itself on the new clinicDefined slice instead. The new item-consent-item-is-identified-once invariant (error) keeps exactly one of the two slices present, so no resource is left identifying nothing.

    Consumer action: a reader that reached straight for category[item] and assumed it was present must handle its absence, and must read the item from category[clinicDefined] -- the tenant coding for the code, and text for the label -- when it is the slice in play. A reader that treated the catalogue coding's absence as malformed will now reject valid item consents. Separately, a consumer that LISTS a patient's items must add http://loinc.org|59284-0 to its category search or every clinic-defined item will read as Not Recorded. Writers are unaffected: a governed item consent is written exactly as before.

FHIR-466.

Document status reasons, salient fields, and form-template field mapping

See: DocumentReference, MedicationStatement, MedicationDispense, FamilyMemberHistory

Carried in [Unreleased] before the privacy work and shipping with it. Additive throughout.

  • DocumentReference gains a status reason: the new nexus-document-status-reason extension (0..1, free text) is sliced onto the profile as statusReason, carrying why a document was marked superseded or entered-in-error. R4 DocumentReference has no statusReason element, and the code alone cannot say whether a retired consult was replaced by a corrected copy or filed on the wrong chart. The reason rides the version that recorded it -- the store's version history is the audit of who changed the status, when, from what to what, and why -- and writers remove it on reactivation, so an active document never carries a stale reason. Always optional; a status change with no reason is a complete record. Additive only.

PSD-999.

  • MedicationStatement, MedicationDispense and FamilyMemberHistory now declare salient fields, so all three gain synthesized accessors and a Salient fields tab, and a consumer rendering them into a chart or an LLM context is reading the profile's own statement of what matters rather than the whole envelope. Until now these three declared none: they were reachable and validated, but had no reader surface, so they arrived as bare elements. The sets are medication / status / dosage / effective / asserted / reason_code on MedicationStatement, medication / status / quantity / days_supply / handed_over / dosage on MedicationDispense, and relationship / status / date / condition / deceased on FamilyMemberHistory. The two medication sets deliberately mirror MedicationRequest where they overlap, so one drug vocabulary covers prescribed, reported and dispensed. Additive only -- no element, cardinality or binding changes, and nothing a current consumer reads moves.

    Two of them collapse a repeating element and say so in their comment: condition is the FIRST recorded condition's code, and deceased reads only the boolean arm of deceased[x], so a relative whose death is recorded as an age or a date leaves it absent. Absent means "not stated in that form", never "no".

FHIR-355.

  • Appointment.participant.type is documented and Must Support, with the participation type each actor carries — PART (Participation) on the patient, PPRF (primary performer) on the provider, both inside the bound encounter-participant-type value set. Consumers outside this ecosystem read the type rather than the referenced resource type, and Ocean drops a patient participant that has none, so an untyped booking never reaches its reminders list. No constraint is tightened: the element stays 0..* on the base extensible binding, and the four appointment examples now carry it.

No ticket yet.

Every reportable field now has an identity, and this specification is where it lives. Three new code systems: Nexus Field carries 458 concepts, one per reportable field, coded <category>.<field>; Nexus Field Category carries its 21 domains and Nexus Field Subcategory the 73 groups inside them. A field concept declares 26 properties and carries 24: where the field sits in the picker, which element and profile realise it, its data type, cardinality and repeat semantics, the code and the code system that identify it within its resource -- by name in clinicalCodeSystem and by canonical URI in codeSystemUri -- its expected unit, the permission needed to read it, how strongly it identifies a person, whether it is computed rather than stored, and the comparison family a report builder should offer, and -- where the path alone does not identify the field -- which entry of a repeating element it means (selector) and under what condition on the resource (gate). English displays sit beside fr-CA designations.

Renamed before release: these three were nexus-reporting-criteria, nexus-reporting-category and nexus-reporting-subcategory when they first landed on main. They never appeared in a cut release, but they were on main for a day and were announced internally under those names, so anything written against them in that window needs re-pointing: the ids and canonical URIs above, and the criteriaCode key in reporting-catalog.json, which is now fieldCode. Codes, concepts, properties and the <category>.<field> shape are unchanged -- only the three system names and that one key moved. The reason is that the concepts are reportable FIELDS: a criterion is a field plus an operator plus a value, and these carry only the field. nexus-reporting-picker-id and its ConceptMap keep their names, because they genuinely are the frozen record of what the report builder's picker shipped.

One field's CONTENT changed, and it is not part of the rename. demographics.sociodemographic-equity-set moved from pending to mapped: it gained fhirPath: Observation.value[x] and an elm mapping from a source change already on main, and the regeneration in this release is where that surfaces. It is named here because it was not attributable from the diff, which otherwise reads as a rename touching a generated table.

As first generated it projected UNGATED. Observation.value[x] with no code discriminator writes every Observation's value into the equity-set columns -- unlike occupation beside it, which gates on LOINC 11341-5 -- and that is not a narrower value but a value belonging to a different question. The projection now REFUSES it: the columns exist in the marts and stay NULL, with the reason named in the generated header, under the rule that an entry reading Observation.value[x] with no usable code is refused rather than emitted. Making the field project needs a code discriminator stated upstream on the concept; it is not something the projector can decide.

codeSystemUri is the machine-readable half of clinicalCodeSystem, and it is deliberately incomplete. A projection that filters on a coded criterion has to emit system and code together; a display name like LOINC or CDS-S CT-012 Phone Number Type cannot be matched against stored data, so a consumer had to keep its own name-to-URI table and every consumer kept a different one. 104 of the 138 concepts that name a code system now carry its canonical. The other 34 do not, because their clinicalCodeSystem names more than one system (CVX / Health Canada DIN, LOINC (observation code); SNOMED CT (value)) or names a table with no established canonical (the CDS-S concept tables, ICD-9-CA, CA Baseline birth sex). A guessed canonical is worse than an absent one -- it produces a query that matches nothing and reports zero rows rather than failing -- so where the system is not certain the property is absent, and absence means "not established", never "none".

Before this there was no identifier for a reportable field at all. Consumers keyed on a domain number and a prose name, or on ordinals minted from a design mock, so nothing could be joined and a stored report survived a rename by luck. A code here is permanent. Adding one is routine; removing or renaming one invalidates every stored report and cohort that carries it, so retirement is status = deprecated plus replacedBy, and the release checks refuse a concept-level removal.

identifierClass is a first pass and is not yet a masking control. It says whether a field identifies a person directly, narrows a population, or neither, which is what a de-identified export needs to key on. It was assigned by rule and classified up wherever it was arguable. It has not been through privacy review, and nothing should rely on it to mask until it has. The same caution applies to the fr-CA designations: coverage is complete at 458 of 458, but none of it has been through linguistic review, and complete is not the same as correct. That is why these systems ship as draft.

Element prose was carrying the wrong things, and 21 profiles were corrected. short is a one-line summary of what an element IS -- the same contract SalientField states for its own prose -- and in 37 places it was carrying something else: a conformance statement ("SHOULD be populated for originating writes"), a slice rationale, or an enumeration of the bound value set. Rationale moved to comment, 15 of which are new. Enumerations were dropped outright: a consumer that wants the values should expand the binding, and a description that recites them goes stale the moment the value set moves. Six elements that inherited an enumerating summary from base R4 now state their own.

Four elements on AllergyIntolerance gained error-severity binding enforcement, and ingest that passes today can start failing. No cardinality, binding, invariant or canonical moved in the FSH. What changed is which elements the differential mentions: giving type, category, criticality and reaction.severity a short put each of them into the differential for the first time, and the facade generator emits a validation rule for every element the differential names, reading the rule off the snapshot. Each of those four therefore acquired the R4 required binding it always had on paper as an error in the generated TypeScript, Python and C# facades. The bindings being enforced are R4's own -- allergy | intolerance, biologic | environment | food | medication, high | low | unable-to-assess, mild | moderate | severe -- and enforcing them is intended. Consumer action: an AllergyIntolerance that today passes validate() carrying a value outside one of those four sets will now fail it. Check ingest that maps a source system's own allergy vocabulary before upgrading. No other profile in this release gained a rule; these four are the whole change.

Five elements still summarise by enumeration, all on ServiceRequest and Consent. Both are profiled more than once, so which profile a reportable field means is genuinely open, and stating a summary would be picking one. That ambiguity is tracked where it belongs, in the catalogue's elmCandidates, and is not resolved here.

Added

  • A form template can say which of its fillable fields a chart value pre-fills -- the new nexus-form-template-field-mapping extension on DocumentReference (PSD-1594): a list of rules, each naming a fillable field of the template's file by its AcroForm field name, verbatim, and the chart value that fills it, coded from the new nexus-form-chart-value code system (value set: patient.fullName, patient.firstName, patient.lastName, patient.birthDate with its .day / .month / .year parts, patient.gender, patient.phone, patient.address with its .street / .city / .province / .postalCode parts, patient.socialInsuranceNumber, patient.healthNumber with its .number / .versionCode parts, and patient.mrp, the most responsible provider). A form started from a mapped template opens with those fields filled from the chart and every value still editable; an unmapped field, or one whose value cannot be resolved, stays blank. Rules are keyed by field name rather than widget because a name can be drawn on several pages and every widget carrying it is filled. The binding is extensible -- a deployment may carry a key the set does not list yet, and a reader that does not know a key leaves the field blank. Only a template carries the extension: the new nexus-documentreference-field-mapping-only-on-template invariant refuses it on any other document. The system-provisioned WSIB Form 8 template ships mapped; its example carries the full rule list.
  • The Reviewed extension's context widens to DocumentReference: a document now carries who signed it off and when, on the resource the question is asked about, so a reader renders the checkmark from the document alone rather than joining to the review task. It repeats there — a document is assigned to reviewers individually, and each stamp is one reviewer signing off for themselves. Absent still means unreviewed.
  • New Resource Note extension: an attributed note on a resource with no note element of its own, anchored at DomainResource so every such resource gets the same thread the same way. Each repeat is one Task Note turn — author and time required, machine authors as identifier-only references — and the thread is append-only, read oldest first by time. First use: the comment thread on a document, riding the DocumentReference itself, so reading the thread is reading the resource. Where a resource defines a native note element, that element remains the home.
  • Task code gains document-review: the marker a document review assignment carries, and the selector the document form lists a document's reviewers by — a task that merely focuses on a document is someone else's work item, and the form acts on what it lists (priority fan-out, reviewer removal). Distinct from review-report, the inbound triage stream.
  • New Document Category code system (with its value set) under the canonical base: the document form's category picker, recorded on DocumentReference.category — a workflow taxonomy for sorting a practice's documents, several of whose concepts (RX renewal request, administrative item) no standard terminology publishes; where one maps onto a CDS-S Report Class the definition says so. Optional on purpose: uncategorized is an ordinary state, and a document keeps any classification another producer stamped alongside this system's.

[1.25.0] - 2026-08-31

Generated by TypedFhir 0.15.0. Unchanged from 1.24.0; the library moves independently of this specification and did not need to move for this release.

Nothing here changes the specification -- no new profiles, no changed constraints, no moved canonicals. If you use the TypedFhir library directly, read the breaking note at the end of this preamble; otherwise there is nothing to do. Four pieces of work went into it.

The release process is rewritten. bin/ was shell and Python; it is 32 TypeScript files now, and a release is five commands rather than a chain that had to be run in the right order. Cutting a version moved inside the same command that checks the spec, so a failed cut leaves an ordinary working tree instead of a stamped, unpublished one, and publishing lost both of the flags that could skip verification. That put the HL7 validator off the default path -- 27,162 ms against 284 ms for the in-process one, which was essentially all of publishing's cost -- and once it was gone the site build was the release, so it was profiled rather than guessed. The navigation turned out to be 61% of every page: the largest reference page carried 519 KB of it and 1,701 links before a byte of its own content. Two other plugins were doing nothing at all. The build went from about 31 seconds to 10, the published site from 650 MB to 158 MB, and no URL moved -- sitemap, search index and the body of all 1,207 pages are byte-identical.

The multi-IG selector is gone. The library could be asked to validate against a named IG version: nexus-v1.24.0, or a bare nexus aliasing whichever version was vendored. Nothing asked. No service, no generator and no frontend passed one; the .NET port never implemented it; the only caller in the tree was a fixture that existed to demonstrate the feature. What it cost was bookkeeping: the version was written by hand in nine files, and four of them -- both registries and both registry tests -- were there only to spell that key. They are gone, along with the check that compared the hand-written key against the generated tables, which existed only because hand-written version numbers drift. sushi-config.yaml is now the one file in the repository where this version is written; everything else derives it.

The IG build is out of CI. A job there compiled the IG and ran the conformance suites against it. Those suites compare committed generated code against freshly compiled source, and between releases the two are supposed to differ -- generated code tracks the last release, the FSH source runs ahead toward the next, and they re-converge at the cut. So the job went red on contributions that were entirely correct, with nothing the author could do about it. The suites now run before a pull request, where the person running them knows whether the difference is intended.

The pages that came out of internal notes are rewritten. Several under Contributing and Features began as working notes and had never been rewritten for the people who now read them; they say what is settled, what is not, and what would settle it. Two that were missing from the navigation entirely -- and so published with no sidebar -- are in it. The site build is also stock MkDocs now: Material for MkDocs reaches end of life on 5 November 2026 and its successor supports neither of the two plugins this site was built on, so both were removed. Nothing here commits to that migration; it removes what would have blocked the choice.

One breaking change, and it is in the library rather than in this specification. If you pass ig: to a TypedFhir ingest entry point, or read igKey / ig_key off a report, remove it -- see Removed for the full consumer action. Callers that never named an IG, which is every known caller, need no change.

Changed

  • The site build is 43% faster and the published site four times smaller, from four Material navigation features -- navigation.prune, navigation.instant, navigation.instant.prefetch and navigation.instant.progress. Measured on this branch with the pre-build hook disabled, so the numbers are mkdocs alone: 24.3s -> 13.9s, and 650 MB -> 158 MB on disk. Once the validator is off the path (publish --fast), the site build is the release cost.

    The navigation was 61% of every page. Material renders the whole site's nav into all 1,203 pages, so the largest reference page shipped 519 KB of nav and 1,701 links before a byte of its own content. prune emits only the active branch plus its ancestors and siblings; a cousin profile collapses to one link. It touches nav markup and nothing else -- sitemap.xml, the search index and the <article> of all 1,207 pages are byte-identical to the unpruned build, so no published URL moves and the URL and fragment gates are unaffected.

    What it costs and what pays it back. A cousin profile becomes a link rather than an in-place expander, so reaching a different profile costs a navigation instead of a free CSS toggle. navigation.instant turns that navigation into an XHR content swap rather than a document reload, .prefetch starts the fetch on hover, and .progress shows a bar past 400ms. Handbook pages render identically either way: what prune strips there is the reference tree that was in the DOM but hidden behind a tab.

  • A second 29% off the site build by removing an extension and a plugin that did nothing: include-markdown and codehilite. 14.8s -> 10.5s, output byte-identical. Profiling the build rather than guessing is what found them: include-markdown ran on_page_markdown across all 1,203 pages scanning for a directive that appears in no page and never has (the includes in use are pymdownx.snippets --8<-- markers), and codehilite is an alternative to pymdownx.highlight rather than a layer under it, so registering both left the second one inert. Together with the navigation work above, a strict site build is now ~31s -> ~16s end to end.
  • The site build is stock MkDocs now: no plugins beyond search, no hooks:, no watch:. Navigation moves from the mkdocs-awesome-nav plugin's per-directory .nav.yml files to one ordinary nav:, assembled by bin/gen-nav.ts from an authored src/nav.yml plus the generated reference tree and read through mkdocs' own INHERIT. Rendering docs/fhir/ moves from the pre-build hook into bin/site.ts. Output is byte-identical.

    We were using none of awesome-nav's features -- no globs, no sort, no flatten, no hide -- only its ability to split the nav across directories, while the reference tree that is 96% of the entries was generated regardless. Removing it produced an identical 1,207-page site.

    hooks: and awesome-nav were the two things tying this site to MkDocs. Material for MkDocs reaches end of life on 5 November 2026 and its successor, Zensical, supports neither yet. Nothing here commits us to that migration; it removes what would have blocked it, and it costs one dependency and one Python file rather than adding anything. doc/fhir/bin/ is now Python-free.

    What it costs, named. mkdocs serve no longer re-renders docs/fhir/ when the IG source changes, so a bare mkdocs build is no longer self-sufficient -- npm run site is the entry point, and npm run site-kick restarts the server after an FSH or src/pages edit. Edits under docs/ live-reload as before. INHERIT is resolved at config load, before any hook could run, so the nav could never have been written from a hook anyway; the build gate grew a nav is current check so a stale nav fails loudly instead of publishing pages with no sidebar, which mkdocs reports only as INFO.

  • check-blast-radius resolves the base branch instead of assuming origin/main. IG releases stack -- 1.23.0 -> 1.24.0 -> 1.25.0, each PR based on the last -- and against main a stacked branch inherits every unmerged parent's diff. This branch measured 1,687 changed files that way, of which 1,295 were a single 1.23.0 commit untracking the rendered pages and only 67 were its own. A reviewer reads a number like that as noise, which is the exact failure the gate exists to prevent. The base now resolves in order: --base, then GITHUB_BASE_REF (set by GitHub Actions on a pull_request event, and authoritative there), then gh pr view for the current branch, then origin/main. Every path is printed, including the fallback, because a wrong base fails silently in both directions -- too far back and the diff drowns, too far forward and real changes stop being counted.
  • check-blast-radius gains a declared-IG-consumer list, and NativeFHIR/seed-generated.sh is its first entry. That script reads igVersion and sourceContentHash out of the generated meta to decide whether a cached seed dataset is stale -- it is the only one of NativeFHIR's eleven files that reads any of it, and IG releases have legitimately edited it before (1.20.0, and again this release after 1.23.0 deleted the sourceCommit field it had been reading). The test is "does this file read the IG's generated metadata", not who owns it: ownership is what the gate prints so you know who to ask, and it moves. Declared consumers are counted and printed but do not fail the gate, because an exemption nobody can see is one nobody re-examines.
  • set-version stamps one site it used to miss; sixteen other version stamps were deleted instead. Cutting this candidate found seventeen places still naming 1.24.0: five using Nexus version '<v>' declarations and all twelve measurement-catalog ObservationDefinition-*.json resources. Only one of the seventeen turned out to be worth keeping.

    Neither set was silent by accident. ts/test/cql-model-version.test.ts exists because the using bump "was made four releases running, and the fourth is the one that shipped"; ts/test/measurements.test.ts recorded the catalog stamp being walked by hand through 1.21.0 -> 1.20.0 -> 1.22.0 -> 1.21.1 with no check behind any of those moves. Both tests caught the drift again here -- they work. The first fix was to make all seventeen version sites so the stamps could not drift, which was the wrong fix in both cases: it grew machinery to maintain text that should not have carried a version.

    Four of the five using literals are deleted. Each said some form of "so using Nexus version '<v>' works with no further ceremony", where the claim does not depend on the number -- any version would do. They now say using Nexus. One was not merely stale but false: it claimed DM-01-nexus needs using Nexus version '<v>' when the fixture declares bare using Nexus, and the next comment in that same file says so, adding "A literal here is one more site an IG bump retires." The survivor is codegen/cql/gen-modelinfo.ts, where the number is the point: the sentence contrasts the stripped version against -candidate.

  • The measurement catalog drops its version field entirely, and says plainly that it is test data. Twelve ObservationDefinition resources that exist to exercise the CQL engine's unit-vouching -- that a unit-bearing threshold is refused unless something vouches for the convention. Nothing in production consumes them: no service outside TypedFhir imports the module, and every caller of compileToTrigger that passes a catalog is a test or a test-golden generator. Defining measurements for production is expected to be a richer mechanism that has not been designed.

    The field tracked nothing and was read by nothing -- the catalog keys entries on code|system and never looks at it -- so it was noise with a maintenance cost. measurements.test.ts now asserts the field stays absent, with the reasoning in the test, so it cannot creep back as a stamp. gen-measurements.ts no longer reads igVersion() at all, and the module and generator headers now say what the content is for rather than describing an unbuilt deployment/tenant resolution model as though it existed. See FHIR-102 for the related finding that these resources' canonicals are declared nowhere -- which this reframes: they are test fixtures, so the question is whether they should be in the fhir.apps.health namespace at all.

Fixed

  • Two Handbook pages were orphaned from the navigation and are now in it: guide/features/scheduling/converter-contracts and guide/features/scheduling/oscar-template-schema-notes, both FHIR-18/FHIR-19 working notes for the Oscar-side converter owners. A page absent from the nav renders with no sidebar in mkdocs-material, and mkdocs reports that as INFO rather than a warning, so strict: true stayed green while they published unnavigable. Both are still marked Draft.

Removed

  • BREAKING -- TypedFhir's multi-IG selector is gone. If you pass ig: to any ingest entry point, or read igKey / ig_key off a report, remove it. fhirToResources, fhirToResourcesLenient and their Python twins no longer accept an ig argument, DecodeReport no longer carries the key, and DEFAULT_IG / resolveKey / resolve_key / registeredIgKeys are no longer exported. getIg() takes no argument. Callers that never passed a selector -- which is every caller we can find -- need no change.

    Why. Nothing selected a second IG. The .NET port never implemented the selector at all; no service, no frontend and no generator passed one; and the only non-default consumer in the tree was the synthetic-v1.0.0 fixture, which existed to prove multi-IG selection worked. A capability whose sole user demonstrates that capability is not one.

    What it was costing. The <igId>-v<version> key put the IG version into two hand-maintained registries and two pinned tests, and needed a key-vs-generated-meta agreement check to catch the drift those literals invited. Removing it removes all five.

  • The IG version is no longer written anywhere by hand. There were nine version sites -- eleven occurrences across prose, worked CQL examples, the two registries and three pinned tests -- each walked forward every release, each having drifted at least once. All nine are gone: ts/codegen/lib/ig-specs.ts derives from the compiled IG via igVersion(); ts/test/ig.test.ts compares IG_META.igVersion to the same source rather than to a literal; the guide's worked criterion declares bare using Nexus, as the DM-01-nexus fixture always did; and README.md no longer pins a version. sushi-config.yaml is the only place a version is written, and the generated stamps are asserted against it.
  • docs/theme/js/tab-anchors.js, dead since 1.16.0. It activated ancestor content tabs so an inbound #inv-<key> anchor landed on content the reader could actually see; one page per tab made every view separately addressable and dropped the script's extra_javascript entry in that release, leaving the file orphaned and loaded by nothing. Confirmed unnecessary rather than merely unwired: across all 1,204 content pages 26,481 fragment links resolve to visible content, none missing and none inside an unselected tab, and exactly one page in the site -- the hand-written CQL artifacts guide -- still uses a content tab at all.

[1.24.0] - 2026-08-29

Added

  • A machine-written Task declares WHO produced it, on requester -- two new identifier authorities, NamingSystem/nexus-cds-rule (a decision-support rule) beside the existing NamingSystem/nexus-harness-graph (an AI-harness graph). Both are used the same way, as a logical Device reference: Reference.type = "Device" plus Reference.identifier under the producing registry's namespace, resolving to no Device resource on the server. A consumer telling producers apart now reads ONE declared field. (FHIR-90)

    Two authorities rather than one, because two separately owned registries mint these values and either could mint the same string. That is not only an attribution problem: producer identifiers feed the business keys below, and a key is SEARCHED and the resource it finds is UPDATED, so a shared namespace lets one producer's re-run overwrite the other producer's work item. The system names the registry that ISSUED the identifier, never the runtime that executed the rule -- a rule invoked as a tool from elsewhere keeps its identifier.

  • A decision-support rule keys its work under its own namespace: NamingSystem/nexus-cds-key, the exact counterpart of NamingSystem/nexus-harness-key and separate from it for the reason above. Same rule, and the same one way to get it wrong: the value is derived from WHAT THE TASK IS ABOUT (the patient and the gap), never from the evaluation that produced it. CDS Hooks supplies a hookInstance and a card uuid, and both are per-firing values that mint a fresh key every evaluation. (FHIR-90)
  • An attachment packet is a List: the ordered set of chart items assembled to travel with something going out. New profile StructureDefinition/nexus-attachment-packet with NamingSystem/nexus-attachment-packet for its identity. Membership and order are List.entry, mode is working, subject is the patient. The host points at the packet with a 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, so a packet is legal in each today and what this release adds is the sanction. (SD-175, SD-222)

    Why a List and not a document. R4 has a resource for exactly this shape -- DocumentManifest, "a collection of documents compiled for a purpose together with metadata that applies to the collection", carrying content, recipient, related and subject -- 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. IHE MHD reaches the same place: IHE.MHD.Comprehensive.SubmissionSet constrains List, and MHD's ihe-intendedRecipient extension sits ON THAT LIST rather than on a document -- recipient belongs to the packet prepared for someone, not to the document, which is why DocumentReference gains no recipient element here.

    Modelling the packet as its host's own sections was the alternative and it does not survive contact with the second host: a ServiceRequest has no sections, so an eReferral would have needed a second design, and so would every other host in the requirement.

    entry.item is deliberately not type-constrained. Enumerating the kinds shipped today would reintroduce the per-type modelling this profile replaces; a consumer renders the kinds it knows and reports the ones it does not.

  • A packet member can say which of its pages were left out -- StructureDefinition/nexus-attachment-packet-excluded-page on List.entry, repeating, one occurrence per excluded page, 1-based. IG-defined rather than a house extension because a third-party consumer is a named future reader and can only implement what is published.

    One occurrence per page rather than a range expression ("2-4,7"): a range is a second syntax this specification would have to define and every reader would have to parse identically to agree on what was sent, where a repeating integer needs no parser and a validator can check each value alone. Page numbers are meaningful only relative to the render they were taken from -- a different renderer may paginate the same member differently -- so they record what the assembler chose, not a durable re-rendering instruction. What the recipient received is the document stored on send.

  • LOINC 28562-7 "Chart section Set" joins ValueSet/nexus-composition-section, which is what an enclosures section codes to, and a new enclosures code joins CodeSystem/note-block-type so an attachments block renders through the same dispatch as every other block. 28562-7 is SCALE_TYP = Set / CLASS = ATTACH.CLINRPT: a SECTION code. LOINC 11503-0 is the DOCUMENT code for the rendered packet PDF and belongs on DocumentReference.type, never in a section slot -- the distinction this value set's extensible binding exists to catch. (SD-175)

Changed

  • Task.requester is the producer, and the two things it is NOT are now stated on the elements themselves. intent is not a machine marker -- humans raise proposals, and the element says what the owner is being asked to do. groupIdentifier is a DISPLAY GROUPING and nothing more: grouping is available to every producer, since a harness capability proposing several chart entries from one note is one evaluation raising a set exactly as a decision-support card is. A consumer that infers the producer from either, or from the ABSENCE of either, is reading a coincidence of current behaviour. (FHIR-90)
  • Task.identifier, Task.owner and the Task Note profile name both producer families. owner admits a rule as a logical Device alongside a graph, while the approve/resume undertaking stays the harness's own (assigning a task to a rule promises no resume), and nexus-note-agent-author was widened so a rule-authored turn is held to the same identifier-only shape as a graph-authored one. (FHIR-90)
  • DocumentReference.context.related is sanctioned as the BACK pointer from a rendered outbound artifact to the packet it renders and the host that packet travelled with. Ontario eReferral profiles this element the same way, so the pattern stays; what changes is that it stops being the ONLY link, now that the host carries a forward reference. Do not discover the relationship by searching it: related is not among the search parameters this server declares for DocumentReference, and an undeclared parameter is dropped silently rather than refused, so a search that appears to work may be returning the whole compartment. Resolve the reference. (SD-175)

Removed

  • NamingSystem/nexus-harness-key drops its documented legacy CDS-connector key shape ({hookInstance}:{cardUuid}:{suggestionIndex}). It was per-firing, so it minted a fresh key on every evaluation and defeated the duplicate suppression the key exists for -- the one shape in that file that broke the file's own derive-from-the-gap rule. CDS keys now live in their own namespace (nexus-cds-key, above). Consumer action: a producer fed by CDS Hooks derives its key from the subject and the gap, never from the firing. (FHIR-90)

[1.23.0] - 2026-08-29

Added

  • The Patient profile gains the Indigenous-profile fields. The Indian Act registration or status number is a Patient.identifier under the Canadian URI Registry's official system (…/ca-indigenous-northern-affairs-number; 9–10-digit value or a data-absent-reason while other card facets are recorded first; the validity period carries the card's issue/expiry, and the new nexus-emr-indian-status-card-type extension on the identifier carries the presented document form — SCIS, CIS, or TCRD). Three optional flat extensions carry the rest: Indian Act registration status (nexus-emr-indian-act-registration-status) and the First Nation registry group and community (nexus-emr-first-nation-registry-group / nexus-emr-first-nation-community), both coded from the curated seed-managed band list (ValueSet/nexus-first-nation-band, code = federal band number). Every field is independently recordable, records what the patient states (never verified against the Indian Register), and a recorded expiry does not imply a registration-status change.
  • Form documents are marked, and found, by one identifier system — the new NamingSystem/nexus-form (PSD-1230): a DocumentReference carrying an identifier under https://fhir.apps.health/NamingSystem/nexus-form is a form, its value a kebab-case slug naming the form type (wsib-form-8), and a patient's forms are enumerable with the standard identifier search's system-only token — the marker-identifier convention nexus-template established, applied to DocumentReference.
  • Form templates are marked by a sibling identifier system — the new NamingSystem/nexus-form-template (PSD-1230): a subject-less DocumentReference carrying an identifier under https://fhir.apps.health/NamingSystem/nexus-form-template is the tenant-wide blank of a form type, its value the same slug its filed nexus-form instances carry, and a tenant's templates are enumerable with the same system-only identifier token search.
  • Form templates are searchable by title (PSD-1230): the server declares the standard DocumentReference description string search, and DocumentReference?identifier=https://fhir.apps.health/NamingSystem/nexus-form-template|&description:contains={text} finds the templates whose title carries the text anywhere, case-insensitively -- so a template picker reads pages of the matches rather than the whole catalogue.
  • Form templates are uploaded and deleted by a tenant (PSD-1230): an uploaded template is a subject-less DocumentReference created with its PDF inline, description the title as typed, and the nexus-form-template marker's value the kebab-case slug of that title (WSIB Form 8 -> wsib-form-8) -- the form type every form later filled from it carries. A tenant holds one template per form type: an upload whose slug is already marked by a live (not entered-in-error) template is refused, so a client must probe DocumentReference?identifier=https://fhir.apps.health/NamingSystem/nexus-form-template|{slug} before creating. A template is removed with the standard delete interaction unless it carries the readonly tag (https://fhir.apps.health/tags/document-reference/readonly|true, the system-provisioned templates) -- those are never offered for deletion. Forms already filled from a deleted template stay on their charts: they carry the form type by slug and reference no template.
  • A C# consumer can ask, at runtime, which IG the package was built against. TypedFhir.Facade.Generated.IgMeta carries IgId, IgName, IgVersion, FhirVersion, Canonical, SourceContentHash, ProfileCount, ValueSetCount and InvariantCount. The TypeScript and Python ports have carried the same facts as IG_META for several releases; the .NET package had them only in a generated header comment, where no program could reach them. All three ports are now populated from one derivation, so they cannot disagree about their own provenance. (FHIR-93)

Changed

  • A form template no longer has to name a patient, and every other document still does. DocumentReference.subject relaxes from 1..1 to 0..1, guarded by the new invariant nexus-documentreference-subject-unless-template: a DocumentReference SHALL carry a subject unless it carries an identifier under NamingSystem/nexus-form-template. A template is the tenant-wide blank of a form type and is about no patient, which is the shape the form-template guidance describes, and the profile previously required a subject on it. Read the cardinality with the invariant, not on its own -- for every document that is not a template the requirement is exactly what it was, and it is now stated in evaluable FHIRPath rather than left to a cardinality. A consumer that treated subject as always present must handle the template case. Stated as one invariant rather than a second profile, because a template profile answers the question once per resource type. Both form systems now ship a worked example: a filed WSIB Form 8 and its template. (FHIR-94)
  • nexus-first-nation-registry-group and nexus-first-nation-community bind required, not extensible. The extensible binding permitted a code from any system while both definitions said a band outside the curated list could not be recorded; required is what the definitions describe. It costs nothing to record a band the list is missing, because ValueSet/nexus-first-nation-band is seed-managed reference data rather than an IG artifact -- membership updates ship as data and need no release, so a closed binding never becomes a release gate on recording a patient. (FHIR-94)
  • nexus-first-nation-community now states that the band list APPROXIMATES its domain. A band is a registry group; a community is a place and a people. One band may hold several communities and reserves, and a person can affiliate with a community whose band does not hold their registration. Both extensions bind the band list because it is the only curated Indigenous reference data this specification carries. Read a community value as the community the patient named, resolved to a band, and never as a claim about registration. (FHIR-94)
  • The IG version is no longer stamped into the header comment of every generated file. It has a structured home in each port -- IG_META in TypeScript and Python, IgMeta in C# -- and that is where a consumer reads it. Generated files carrying a version string drop from roughly two hundred to eight: the IG-table modules, Metadata.cs and PatientCompartmentMap.cs, each of which has a better claim to naming its IG than a per-resource facade does. No API moves; the affected lines are banner comments. (FHIR-93)

Removed

  • BREAKING for library consumers: IG_META.sourceCommit is removed from @awaremd/typed-fhir and the Python package. Consumer action: read IG_META.sourceContentHash instead -- a sha256:<hex> digest of the IG artifacts the tables were generated from, which is what a provenance stamp is for and which this release does not otherwise change.

    sourceCommit was derived from the source repository's commit log and was unsatisfiable by construction: it named a commit that did not exist until after the commit carrying it was made, so it could never be correct in a working tree. Nothing in the published surface read it. The field-dictionary manifest, the delta register and the seed-generator manifest move their ig block from sourceCommit to sourceContentHash for the same reason.

    Named as breaking here rather than signalled by the version number, per this changelog's own rule: versions are sequential, not SemVer, and the entry carries the compatibility contract. The consumer action is one field name. (FHIR-93)

[1.22.0] - 2026-08-26

Generated by TypedFhir 0.15.0. Unchanged from 1.21.1, but what it generates is not: the FHIRPath subset gained real membership, type-test and substring evaluation, so twenty-four invariants this guide states are now checked by the generated validators rather than reported as unverifiable. The same library version therefore produces a materially stricter client against this release than against the last one.

Added

  • Imaging Modality -- modality on the report, because the withdrawal of ImagingStudy left the requirement with nowhere to go. Modality is an ImagingStudy property in R4, and this guide no longer profiles that resource; the imaging capability that requires it (ON RFP 1.5.4.2) did not go away with it. 0..* MS on DiagnosticReport, repeating so a PET-CT report says two rather than choosing.

    It describes the DOCUMENT, not an acquisition. Saying a report came from a CT asserts nothing about series, instances or SOP UIDs, and needs no PACS to be honest about -- which is what lets this estate record modality while still declining to model ImagingStudy.

    DiagnosticReport.code is not a substitute, and that is the objection worth answering. A human reader infers modality from "Chest X-ray"; a results list filtering "show me the CT reports" cannot, without maintaining a procedure-to-modality mapping outside the record and getting it wrong silently. An implied fact every consumer must re-derive is not a recorded fact.

    Bound extensible to DICOM CID 29 -- the same value set and the same strength R4 binds ImagingStudy.modality to, so a consumer that already knows R4's imaging model needs no second rule, and heritage feeds carrying local modality codes stay conformant.

  • Nexus Composition Type, for the document kinds LOINC cannot name. Three generated-summary kinds exist in production and a consumer has to tell them apart: nexus-daysheet-summary (written ahead of a clinic for a booked patient), nexus-interval-summary (what happened between two points in time) and nexus-encounter-recap (one encounter, after it). All three are LOINC 34133-9 as a CLASS and none is distinguishable from the others by any LOINC code -- and they are not interchangeable, since a recap asserts that the encounter it covers took place. Plus nexus-letter-template, so the retired http://services.well.com/fhir/categories/letter-template has a home on this guide's base if one is still needed (templates are List as of 1.18.0, so check first).
  • Meta Updated By, an extension this EMR has been writing undeclared on Resource.meta since before this guide described it. It names the user account that last wrote a version, so a version list can say who touched each row without fetching a Provenance per row. It is a display convenience, not an audit record -- anything reasoning about authorship should read Provenance. The value is an identity-provider subject (auth0|...), NOT a Reference: it names an account, not every writer has a Practitioner, and a dangling reference would be worse than a string a consumer can match on.

    Like the two note extensions above it publishes at this guide's base while production still writes http://services.well.com/fhir/StructureDefinition/UpdatedBy. This one differs in a way that matters: it rides meta on every version of every resource ever saved, so the old address does not stop appearing when writers move -- it stops appearing on NEW versions, and history keeps it. Readers should plan to see both indefinitely and match on the URL suffix.

  • First Nation Bands and the First Nation Band CodeSystem behind it -- a canonical this guide bound two extensions to and declared nowhere. The First Nation registry group and community extensions added in 1.21.1 bind https://fhir.apps.health/ValueSet/nexus-first-nation-band, and nothing published it, so a consumer reading either extension could not discover the value domain and the link did not resolve. The canonical is unchanged: it is the one the EMR already writes, declared rather than minted, so no stored Coding moves.

    The content stays out (content = #not-present). The membership is the Indigenous Services Canada band list, held as seed-managed reference data on the same footing as the curated problem and procedure lists, and it changes on that list's schedule rather than this guide's release schedule. This publishes the system's identity and leaves its content where it lives, so an expansion is available only from a terminology server loaded with that seed. Both bindings are extensible, which is the honest strength: a band absent from the curated list cannot be coded.

    The code is the federal band number and the display is the band name as registered. Band names beginning with a literal ? are the names as registered, not encoding damage, and sort before "A" under ordinal comparison. The Canadian URI Registry names the same registry for IDENTIFIER use as .../NamingSystem/ca-federal-firstnation-band-id; this system is the CODING view of the same registry, which is why the two URLs differ and both are correct.

  • Composition Block Type and Composition Block Data, the two extensions a block-authored encounter-note section carries, together with the Note Block Type code system and Note Block Types value set the first of them binds. The note body is authored in a block editor -- an ordered stack of independent pieces, each with its own identity, heading and kind -- and the EMR has been writing that shape undeclared. A block is one Composition.section: section.id is its identity, section.title its heading, section.text its narrative, and R4's own ordering of section carries the sequence. A block raised and left blank is kept, with an empty narrative and an emptyReason of notstarted, because the gap is part of what the author wrote.

    This is the block surface returning on the terms 1.14.0 set for Composition itself: there is a committed consumer. It is not the shape 1.11.0 withdrew. That one nested sections recursively and carried its kinds in a complex extension whose slicing was among the unevaluable-slicing defects the removal cleared; these are two flat extensions on an ordered, non-recursive section. It does enumerate block kinds as nexus-emr-embedded-section-types did, and the difference is what is enumerated: one kind that has shipped, rather than four that had not.

    The narrative is authoritative, and that is the invariant to build on. Every block-authored section carries its content in section.text whatever its kind, so a consumer that ignores both extensions reads a correct, complete note. A block kind that puts clinical content in the structured value and nothing in the narrative is malformed however well it round trips.

    The block-kind vocabulary is extensible, and a reader that meets a kind it does not know must write that kind back unchanged. Every block-authored section declares its kind, text included; a section carrying no kind at all predates the extension and reads as text, which is tolerance for data filed before block authoring existed rather than a default, and is retirable once no such section remains. Note Block Types is how a kind becomes NAMED -- a writer whose kind is listed uses that code rather than a synonym -- but a kind the guide has not named yet is conformant. Rendering an unrecognised kind as text is correct; storing text in its place is data loss. Both extensions are draft, and the vocabulary grows as kinds ship rather than ahead of them.

    Both extensions publish under this guide's canonical base, https://fhir.apps.health/StructureDefinition/.... They were first minted under http://services.well.com/fhir/..., the hostname three older Composition extensions in the same library already use, and were moved before publication -- the only moment that move was free, since no note carrying either had been filed. Published canonicals are forever, and from this release these are published.

  • Composition Billing and Composition EHR Share, two extensions this EMR has been writing undeclared. Billing raised at note closeout (the service code, the diagnostic codes supporting it, and units where the fee code is charged per unit), and one record per completed share of a signed note to an EHR outside the practice. Both were live on filed notes while this guide described neither, so a reader saw a Composition profile with no billing concept and no sharing concept at all.

    These are declared at this guide's canonical base, which is NOT the URL the software writes today. Production emits them under http://services.well.com/fhir/StructureDefinition/...; they publish here under https://fhir.apps.health/.... The block extensions added above could simply be moved, because nothing had been filed against them yet. These have live clinical data behind them and cannot be moved by editing a constant, so the specification states where they belong and the implementation is knowingly behind it on the URL. Writers: write the canonicals declared here. Readers: expect both for as long as filed notes carry the old address. Matching on the URL suffix (composition-billing, composition-ehr-share) reads both; resolving only the canonical silently misses billing and sharing on every note written before the writers move.

Changed

  • Twenty-four conformance rules that the generated validators reported as unverifiable are now CHECKED at runtime, in all three ports. The unverifiable set goes from 36 to 12. Consumer action: a resource that carries one of these defects now raises an error where it previously produced an invariant-unverifiable note. Every one of them was already enforced by the HL7 validator, so a resource that passed a full FHIR validation is unaffected; what changes is that the in-process validator no longer needs a second tool to see them. Callers that cannot absorb a new error use validate() and triage the issues rather than assertValid().

    Newly checked: Appointment app-2 / app-3, Basic app-state-system-inv, the nine MedicationRequest tim-* dosage-timing rules, Organization org-1, Questionnaire que-1 / que-10 / que-13, QuestionnaireResponse qrs-1, RiskAssessment ras-1, the three "resolves to a contained Bundle" rules this guide states itself -- List nexus-template-entry-is-contained-bundle, Task nexus-sugg-2 and Questionnaire questionnaire-extract-bundle-is-contained -- and the three "that contained Bundle is a transaction" rules beside them: List nexus-template-bundle-is-transaction, Task nexus-sugg-3 and Questionnaire questionnaire-extract-bundle-is-transaction. Also newly checked, and an element rule rather than an invariant: Composition section.text.status, which R4 requires at 1..1 and which nothing here checked -- a section written as {"text": {"div": "..."}} passed this library and failed a full FHIR validation. The profile adds no constraint; it restates the base cardinality so the generated validators can see it.

    No profile changed. These are the same published rules, checked where they were previously only reported. The compiled FHIRPath subset gained membership (in), union (|), %resource, %ucum, substring(), numeric addition, boolean literals, trace(), (expr).not(), boolean equality, and the ability to evaluate a rule declared on an element below the resource root.

    What is still not machine-checked, and why, so the remaining gap is a stated one rather than an absence: CareTeam ctm-1 needs reference resolution (resolve()); Questionnaire que-2 and questionnaire-extract-is-bundle-only need a recursive descendants() walk; Observation obs-7, Condition con-3 and the two Patient *-instances-clean rules need set algebra over projected values; Questionnaire que-7 and RiskAssessment ras-2 need element-level type tests (is / as against a datatype rather than a resource); Consent.category and Encounter.class bind value sets that cannot be expanded offline; and the DocumentReference attachment-content rule is verified at document assembly rather than per resource. All twelve are still enforced by the HL7 validator and by any conformant external validator, and all twelve are still reported as invariant-unverifiable at runtime rather than silently skipped.

  • BREAKING: the block-kind binding is extensible, and a reader must write an unknown kind back unchanged. Note Block Types bound required through 1.21.x, and 1.21.1 stated the consequence in as many words: the vocabulary was closed, and a client reading a section whose kind was ahead of its own read and rewrote that section as text, losing the kind. That rule is withdrawn. The binding is now extensible, and Note Block Type carries two more codes beside text -- merge-field-body (the letter composer) and cited-claims (the summary graphs).

    Why it changed. required made this guide a release gate on the editor: ship a block kind and every note carrying one is nonconformant until the IG cuts a release, which is not a dependency a specification should impose on the software it describes. It also did not protect the record. A reader that cannot represent a kind writes the section back as text, so closing the set converted "a kind this guide has not named" into "a kind the record no longer holds".

    Consumer action: a reader built to 1.21.1's rule must stop rewriting an unrecognised block kind as text. Render it as text -- there is no widget for a kind you do not know, and section.text is authoritative, so nothing clinical is lost -- then write the code back unchanged. Display and storage are different decisions. A reader that keeps collapsing unknown kinds destroys merge-field-body and cited-claims on every round trip. Rendering an unrecognised kind as text is correct; storing text in its place is data loss. A writer whose kind is named in the value set uses that code rather than a synonym; a writer shipping a kind the guide has not named yet is conformant carrying its own.

  • The block-type extension is the RENDER axis, not the authoring tool -- and this corrects how 1.22.0 described it earlier in its own development. A section answers three independent questions and each has its own element: what it is clinically (section.code), who produced it (section.author, which already admits Device), and how to render it (composition-block-type). Read the last as "which editor widget wrote this" and a letter and a generated summary have no kind at all, and a consumer is forced to infer rendering from author, which answers a different question. Keeping the three apart is what makes an encounter note, a letter and a summary the same object: a problem-list section is 11450-4 whoever produced it, and a claims section renders identically however it was assembled. No constraint changed -- the definitions did.
  • composition-block-data carries an Attachment, not a JSON string. contentType makes the envelope readable even where the contents are not described here; a JSON string in a valueString is self-describing to nobody, and no published international IG carries structured state that way. Reach for SDC first: where a render kind is really a form (vitals, diabetes review, flowsheet), a Questionnaire plus a QuestionnaireResponse is the modelled answer and gets section.entry for free. Consumer action: a writer emitting valueString must move to valueAttachment. Nothing is filed against this extension yet. Also states plainly that unreadable is not absent -- a writer that cannot parse a stored value must round-trip it untouched rather than drop it.
  • BREAKING: Composition.type and Composition.category swap roles. type is the CLASS and the element to switch on. type (1..1) binds Composition Classification extensible and carries exactly one of three LOINC codes: 34109-9 "Note", 34133-9 "Summary of episode note", 51852-2 "Letter". category (0..) binds Composition Kind preferred* and carries the refinement underneath -- which kind of note (11506-3, 11488-4, 18842-5, 34117-2, 34748-4), which kind of summary (the codes in nexus-composition-type). Composition.section.code binds the new Composition Section extensible, unchanged.

    Earlier drafts of this release had these two the other way round, and told readers to classify on category. That was wrong, and the reason is structural rather than a matter of taste: type is 1..1 in base R4 and cannot be relaxed, so it is the only element guaranteed present on every Composition. The one question every consumer must be able to answer -- am I rendering clinician-authored content or machine-generated content -- has to live there. category is 0..* and may be absent, which makes it the right home for refinements a producer may or may not know, and the wrong home for the distinction nobody can afford to miss.

    Consumer action: switch on type, and search on it. A search for every summary a patient has, of every kind, is type=http://loinc.org|34133-9. Reading category alone will miss documents.

    Producer action: write the class on type and the refinement on category. A progress note is type = 34109-9 with category = [11506-3]. A day-sheet summary is type = 34133-9 with category = [nexus-daysheet-summary]. Converters need remapping: OSCAR sends LOINC 11488-4 "Consult note" on every casemgmt_note whatever the note is, and that is a refinement code -- it belongs on category, with the real class written to type.

    Repeating the type code on category is allowed and is the ordinary thing to do where a document has no kind finer than its class. The refinement set is a strict superset of the class set so the repeat always validates, and a build gate enforces that nesting rather than leaving it to prose.

    section.code is bound for the first time, and extensible on purpose. LOINC supplies both document codes and section codes out of one system in one format: 34109-9 ("Note", a document) and 8716-3 ("Vital signs", a section) are indistinguishable as data, so a document code in the section slot is a mistake the format itself cannot prevent and no reader can detect after the fact. Preferred would document the vocabulary and warn on nothing; extensible warns on a code outside the set, which is exactly what a document code there is. A legitimate section kind the set does not yet name will also warn -- accepted, because growing the set is one line and the alternative is catching this by eye forever.

    The strengths are asymmetric on purpose, and the stronger one sits on the smaller vocabulary. type is extensible because its set enumerates the class axis completely -- note, summary, letter is the whole space -- so a code outside it is nearly always a refinement written into the class slot, which is worth a warning. category is preferred because LOINC's document axis is large and no set here enumerates it: a producer holding a more precise code should use it without being penalised for the precision.

  • The narrative/structured contract is written down on Composition, and it is the most load-bearing rule in the profile. A section carries its content twice on purpose: the structured data is the display path for this EMR's UI, the narrative is for everyone who is not that UI. A renderer shows the structured data and ignores the narrative -- nothing appears twice. The consequence is that both sides must be complete: anything the UI needs must be structured, and anything clinically meaningful must also be in the narrative, including negatives. "No active problems" is a finding, not filler. Booleans and scalars count -- a value living only inside a structured payload leaves every consumer who is not this UI reading an incomplete document.
  • Imaging date and modality follow the withdrawn ImagingStudy profile. imaging date remaps to DiagnosticReport.effective[x], which is the study date for an estate that models the report and is populated where ImagingStudy.started never was. imaging modality moves to the new Imaging Modality extension above, minted in this release for exactly this reason: R4's DiagnosticReport carries no modality element, and the imaging capability asks for one explicitly.
  • BREAKING (TypedFhir): Composition.section reads as a Sections value type, and Composition.prose is gone. The note body is not a scalar. section was handed over as raw JSON in all three ports, and the Python facade additionally carried a prose accessor reading section.first().text.div -- the FIRST BLOCK of a block-authored note, silently dropping the rest. That is a plausible, non-empty value that is a fraction of the record, and it got worse the more a provider wrote; a four-section SOAP note filed before block authoring existed had the same defect and returned Subjective alone.

    Both are replaced by a typed collection in the shape of Notes, in which a section IS an authoring block: it carries the block's identity (section.id), heading (section.title), narrative and narrative STATUS (section.text), clinical code (section.code), authorship, entries, render kind (composition-block-type) and editor state (composition-block-data) as typed members, and appending returns a NEW collection.

    Consumer action, reading the whole body: composition.section.body -- every section's narrative, in stored order, joined, derived where a reader can see that it is derived. Consumer action, writing: build Section values and assign, or composition.section = composition.section.append(block). Replacing ONE block's narrative and leaving the others alone is section.replacing(id, ...), and it is the operation the removed setter could not express: it wrote section[0].text.div and nothing else, so on a multi-section note it overwrote the first block and orphaned the rest, and it omitted text.status, which R4 requires. Sections cannot express that mistake -- a narrative and its status are set together or not at all, and section.text.status is now checked by the generated validators (below).

    Nothing had adopted prose. It existed in the Python port alone -- the TypeScript and C# facades emit no salient accessors -- and it was read-only for the length of this release only. In those two ports section changing from raw JSON to Sections is a new breaking change in ports that had nothing to break, which is the right change and is stated rather than absorbed here. The chart attribute named prose is unchanged: it is the salient chart name, DiagnosticReport publishes the same one, and only the accessor behind it moved (to Composition.body, which is read-only for the reason above). python/examples/write_a_summary.py shows the write path.

  • The whole typed accessor surface now says which of its fields can be WRITTEN. WRITABLE_FIELDS (a static set, also reachable from an instance) publishes the accessors that accept an assignment. A caller holding a polymorphically wrapped resource previously had to introspect the generated class for a property with a setter to find out -- reading the implementation to answer a question the API should answer. Fifteen accessors across the guide are withheld, all of them derived through a read that cannot be reversed; in Python, assigning one now raises a ReadOnlyFieldError naming the field and the reason, in place of the built-in AttributeError that named neither. ReadOnlyFieldError subclasses AttributeError, so existing except AttributeError handling is unaffected.
  • BREAKING (TypedFhir): Composition.category does not accept a bare code; Composition.type does. The rebinding above moved category onto Composition Kind, which unions http://loinc.org with this guide's own composition-type system. A bare code has no unambiguous expansion across two systems, so the generated facades withhold the bare-code shorthand on that element rather than guess which system was meant.

    Consumer action: name the system on category. composition.category = ["11506-3"] becomes [new Concept([new Coding("http://loinc.org", "11506-3")])] in TypeScript, [Concept(codings=[Coding(system="http://loinc.org", code="11506-3")])] in Python, and the same explicit Concept in C#. All three ports refuse the bare code at the call site. Note that a REPEATING coded field keeps a permissive static type in TypeScript, so the refusal there is a runtime one rather than a compile error -- unlike a scalar field, which narrows.

    Composition.type takes a bare code, because its value set draws on LOINC alone: composition.type = "34133-9" writes the system for you. The discriminator is the system count of the bound value set, not the element and not the release.

  • Composition.section.code is what a section clinically IS, and the block-type extension is a different axis. Where a section has a clinical meaning it carries it here: a vital signs section is LOINC 8716-3. It is not a place for a document-type code (LOINC 34109-9 "Note" types a document, not a section), and it is not interchangeable with composition-block-type, which says how to RENDER the section rather than what the section is. C-CDA on FHIR and IPS both make section.code the element a consumer navigates by; a note whose only statement of what a section is lives in a proprietary extension cannot be read by a consumer that does not implement this guide. The binding that now enforces this, and the reason it is extensible rather than preferred, are in the vocabulary entry above.
  • The block extensions' pages said the canonical was unsettled. It was not, and they now say so. Composition Block Type and Composition Block Data publish under https://fhir.apps.health and have since before the first note carrying either was filed. The FSH commentary rendered on their pages still described the URLs as services.well.com and framed the base as an open question, contradicting the declaration on the same page. Their descriptions also carried an AWAITING REVIEW note saying a rename was likely -- which stopped being true once the canonical published, since published canonical URLs are forever. Both now say the canonical is settled. No definition changed; if you read those pages before this release, re-read them.
  • Where the services.well.com namespace goes is IN SCOPE for 1.22.0 and is not settled by the two extensions above. Composition is not the only resource in this estate carrying identifiers and extensions under that host, and the two declared this release are the subset that could be ruled on without moving data. The general question -- which of those artifacts this guide adopts, which it redeclares at its own base, and what a migration costs where records already carry the old address -- is the larger piece of this release and is not answered by this entry. No consumer action follows from this note. It is here so that a reader who finds a services.well.com URL on the wire knows the guide is aware of it rather than silent on it.
  • Whether a block's structured value belongs in an extension at all is NOT settled. The shape of the value is: composition-block-data carries an Attachment, and the consumer action is in the entry above. What remains open is the element -- an extension, or Composition.section.entry. What would settle it is the first block kind that promotes its own facts to entry on save, a vitals block being the obvious candidate: the editor state is whatever the widget needs, and the promoted Observation is what a consumer querying the chart actually wants. The question is stated on the extension's own page. No further consumer action follows from this note -- a writer that follows the entry above is correct either way, because a later move to section.entry would add a place to read the facts rather than change where the editor state lives.
  • BREAKING: the package publishes as health.apps.nexus. It was nexus, and nexus-emr before that. The old id was never publishable in the first place: the FHIR NPM Package Specification requires a name of "two or more namespaces separated by a dot", and a bare nexus is one -- so every release to date has carried an id no registry would accept. That stayed invisible only because this package is handed out as a tarball rather than resolved by name, and it stops being invisible the moment it is published anywhere. The namespace is the reverse of this guide's own canonical host; fhir is deliberately not repeated as a segment, since the canonical already carries it and every package in a FHIR registry is FHIR. The value is on IG metadata.

    Consumer action: a dependency line reading nexus: 1.21.x becomes health.apps.nexus: 1.22.0. FHIR package caches key on name#version, so this does NOT present as an upgrade -- a cached nexus#1.21.1 and a fetched health.apps.nexus#1.22.0 are two packages that coexist happily, and a consumer who bumps only the version keeps resolving the old one with no error. Change the name, not the number.

    Nothing is retroactive. Every already-published version keeps the id it published with, because rewriting one would break resolution for anyone depending on it. The site's download filenames are unchanged and still read nexus-<version>.tgz for every release: that stem is a site convention, deliberately not the package id, so a filename that does not match the id is expected here rather than a mismatch worth reporting.

    Package ids are permanent -- a published id keeps resolving for whoever depended on it -- so this moves once and is not expected to move again. It moves now because it has to move before a first registry publication rather than after one.

Removed

  • BREAKING: NexusEmrCoreImagingStudy is withdrawn, and DiagnosticReport.imagingStudy is no longer must-support. The profile, its intro page and its example are gone, and DiagnosticReport leaves the element at its base R4 definition.

    Why: nothing produces it. ImagingStudy carries series and instance identifiers, modality and SOP references that only a PACS can mint. This estate models the imaging REPORT -- what a results viewer actually renders -- and no system in it holds DICOM acquisition metadata. So the profile described a shape with no producer, and the must-support flag required consumers to support a reference that is never populated. Keeping a first-pass profile published invites implementers to build against a shape this guide is not committing to, which is the same reasoning that withdrew the experimental clinical-document surface in 1.11.0.

    It also cost something concrete. Because the specification asserted ImagingStudy and the corpus could not honestly synthesize one, the gap reappeared on every coverage audit and had to be suppressed by a standing exclusion in the seed generator. The exclusion suppressed the symptom in one report; it did not retract the claim, and the claim is what kept resurfacing.

    Consumer action. A consumer resolving DiagnosticReport.imagingStudy should stop expecting it to be populated by anything in this estate, and no longer needs to support the reference for conformance. A report that genuinely originates from a system holding studies MAY still carry the element -- it survives at base R4 -- but this guide makes no claim about it. Imaging content is on the report: conclusion for the impression, conclusionCode for the coded conclusion, and presentedForm for the body.

    15 published URLs are removed and will 404: the profile page and its element, example, FSH, invariants, SMART and TypedFhir-API tabs. This is the second canonical removal in this guide's history and it is deliberate. Anything linking to them should link to the DiagnosticReport profile instead.

[1.21.1] - 2026-08-26

Generated by TypedFhir 0.15.0. Unchanged from 1.21.0: the specification below is unchanged in shape, and the client library's own additions in this release are new opt-in packages rather than a change to what it generates.

Added

  • The CQL toolchain is now embeddable from .NET and Python, so the chain this specification documents -- CQL in; validated ELM, a module-definition Library and a manifest out -- runs in-process from all three client languages with no service to deploy. One translator is embedded rather than reimplemented per language: every additional engine that can decide what a criterion means is a new authority on clinical semantics, and such engines diverge exactly where it shows least (null propagation in three-valued logic, interval boundary inclusivity, timezone handling at date boundaries, unit conversion). A divergence of that kind inside a signed clinical rule is a patient-safety problem rather than a bug.

    The surface is opt-in in every language -- @awaremd/typed-fhir/cql, Well.Services.TypedFhir.Cql, typed-fhir[cql] -- so a consumer who wants a facade, a patient cache or a FHIR query starts no engine and acquires no language runtime. Which of the three are published today is stated in Evaluating one patient; consumers of compiled criteria need none of them and a JSON parser is enough.

  • Three extensions that let a criteria artifact carry what its ELM cannot say. A module-definition Library says which resources a criterion needs; it does not say which of their elements the logic reads. Criteria Elements Read records one element path per repeat, relative to the resource retrieved. That is finer-grained than dataRequirement, which bounds which resources are fetched rather than which of their fields have to be materialised for the logic to evaluate -- so a consumer projecting a narrow view can size it from the artifact instead of from the CQL. It is a lower bound: derived from retrieves and property accesses, without inference across included libraries, so an absent path is not proof the logic never reads it.
  • Criteria Relative Date says whether a criterion's membership can go stale on its own. true means the logic is anchored to a moving reference point ("no visit in the last six months"), so membership changes with the clock and a periodic re-evaluation is required; false means it changes only when data changes, so a change feed is sufficient. A consumer deciding whether to schedule re-evaluation reads this rather than inferring it from the logic.
  • Measurement Rationale, required on every measurement definition, records the specific ambiguity or unit trap that definition prevents. The failures it guards against -- affine unit conversions, off-by-one ordinal scales -- produce numbers that render and validate while being wrong, so a definition that cannot name the failure it prevents is a guess. R4's ObservationDefinition has no narrative element, which is why this is an extension; it retires into description on a move to R5.
  • The CQL model this specification is authored against, published as an artifact. nexus-cql-modelinfo carries the generated Nexus ModelInfo, so a criteria author writing using Nexus version '<x.y.z>' can fetch the model from the specification rather than reconstructing it from the profiles.
  • CQL is now a documented part of this specification, with a companion page on criteria artifacts covering the boundary that surprises implementers most: you do not need a CQL compiler to consume clinical logic from this specification. Translation happens once, when a criterion is authored. Everything downstream reads JSON.
  • The Patient profile gains the Indigenous fields. This entry was written when the work merged and was missed when 1.21.1 was cut, so it is recorded here late.

    The Indian Act registration or status number is a Patient.identifier under the Canadian URI Registry's official system (.../NamingSystem/ca-indigenous-northern-affairs-number): a 9 or 10 digit value, or a data-absent-reason while other card facets are recorded before a number is. The identifier's validity period carries the card's issue and expiry, and the new Indian Status Card Type extension on the identifier carries the presented document form: SCIS, CIS or TCRD.

    Three optional flat extensions carry the rest: Indian Act Registration Status, and the First Nation Registry Group and First Nation Community affiliations, the latter two coded from the curated seed-managed band list (the code is the federal band number).

    Every field is independently recordable, and each records what the patient states rather than anything verified against the Indian Register. A recorded expiry does not imply a change of registration status.

Changed

  • Five profiles now declare the element a bare CQL retrieve filters on, via HL7's own cqf-modelInfo-primaryCodePath extension: DocumentReference, ImagingStudy, Invoice, Schedule and Slot. Base R4's own CQL ModelInfo declares no primary code path for these five, and the consequence is not a compile error: [Invoice: "some-code"] compiles, the translator only warns, and the criterion silently filters on nothing while its data-requirements manifest emits a codeFilter with no path -- which violates FHIR's own drq-1 invariant. Authoring against this specification's model makes the natural form correct rather than merely accepted.
  • Declared the two extensions a block-authored encounter-note section carries — Composition Block Type and Composition Block Data — which the EMR has been writing undeclared, together with the Note Block Type code system and Note Block Types value set the first of them binds. This is the block surface returning on the terms 1.14.0 set for Composition itself: there is a committed consumer. It is not the shape 1.11.0 withdrew — one section per block on R4's own section ordering rather than recursive containment, and two flat extensions rather than the sliced complex one whose slicing was among the unevaluable-slicing defects that removal cleared. It does enumerate block kinds as nexus-emr-embedded-section-types did, and the difference is what is enumerated: one kind that has shipped, rather than four that had not.
  • The block-kind vocabulary is closed, and a kind outside it is not preserved. The binding is required and every block-authored section declares its kind, text included. A client reading a section whose kind is ahead of its own reads and rewrites it as text: narrative, heading, identity and structured value survive, the kind does not. That is a deliberate trade — a specification that can describe everything the record holds, against forward compatibility with writers that are ahead — and deployments spanning versions should upgrade readers before writers. A section carrying no kind at all predates the extension and reads as text; that is tolerance for legacy data rather than a default, and is retirable once no such section remains.
  • Both extensions are published under this IG's canonical base, https://fhir.apps.health. They were first drafted at the address the EMR front end already wrote (http://services.well.com/fhir/StructureDefinition/...), on the reasoning that declaring a URL nothing emits would document a fiction. That was settled the other way before publication and the front end moved: published canonical URLs are forever, and reconciling the namespaces was cheapest while no note carrying either extension had been filed. One open question remains, and it is a modelling question rather than a naming one: a block's editor state is carried as a JSON-encoded extension rather than in section.entry.

[1.21.0] - 2026-08-25

Generated by TypedFhir 0.15.0. Unchanged from 1.20.0; the library moves independently of this specification and did not need to move for this release.

Two bodies of work: what a task means, and what a machine wrote.

Tasks had two ideas sharing one word, and the model showed it. A proposal is a task offered for a decision (Task.intent), which a user sees in a Suggestions list rather than a Tasks list. A suggested-resource task is one carrying a suggested chart entry inside it. Those are independent -- an authorized order can carry a suggested entry, and a proposal can carry none -- and treating either as a synonym for the other produced a model where Task.code had exactly one legal value and Task.intent was being read as a containment check. Both are now real vocabularies, suggestion is no longer a CODE anywhere in this specification, and three verbs that were being conflated -- approve, accept, instantiate -- are named apart.

The suggested payload stops being a bespoke shape and becomes a template. Prototype resources now ride in a contained transaction Bundle, which is what a reusable letter template carries too and what SDC has carried all along. One mechanism, three wrappers: a List for a named template, a Questionnaire for a form, a Task for a suggested chart entry. Instantiating any of them becomes an ordinary FHIR transaction rather than an operation this specification has to define, and the shape buys three things the old one could not express: multi-resource suggestions, create-versus-update stated per resource, and atomicity stated in the model instead of in prose.

One label answers "was this machine-produced": AIAST, spec-wide. Any resource whose clinical content was produced, in whole or in part, by a non-human agent carries http://terminology.hl7.org/CodeSystem/v3-ObservationValue#AIAST in meta.security, whatever its resource type. It is searchable, it costs one comparison, and it is the label HL7's AI Transparency on FHIR guide uses. The label answers whether; the producer reference and Provenance answer which agent, from what inputs, under whose supervision.

Most of the task work is breaking. Read these six before consuming this release:

  • CodeSystem/nexus-emr-suggestion is deleted. It shipped in 1.20.0; its canonical no longer resolves, and stored Coding.system values pointing at it are now unresolvable. Both its codes moved -- see Removed.
  • Task.code no longer accepts suggestion. A search for Task?code=...|suggestion returns nothing after this release.
  • Task.status binds a required subset. requested, received, rejected, failed and entered-in-error are no longer conformant. Every stored task carrying requested needs rewriting to ready.
  • Every stored template needs reshaping, and so does every stored suggested-resource task: the prototypes move into a contained transaction Bundle.
  • Every stored form needs the same reshaping, and its per-item templateExtract extensions removed. NexusEmrCoreQuestionnaire now prohibits that spelling -- prototypes are entries of the Bundle named by templateExtractBundle. See Added.
  • AIAST binds producers and is not retroactive. Nothing backfills it, so an absent label means unknown, not human until every producer applies it. Read it as evidence a machine was involved, never as evidence one was not.

Added

Task vocabularies

  • Nine categories for Task.code -- reminder, call, appointment, review-report, prevention, referral, billing, medication, other -- in the new Task Codes value set, bound extensibly. review-report is named for the work rather than the source on purpose: diagnostic imaging and other non-laboratory reports arrive on the same inbound stream, so a category called lab would either exclude them or quietly redefine lab. (FHIR-71)
  • A required subset of TaskStatus (Clinical Task Statuses): draft, ready, accepted, in-progress, on-hold, completed, cancelled. These are meanings, not a path -- any state may follow any other, and ready straight to completed is an ordinary day's work. on-hold is present for snooze and pause. (FHIR-71)
  • intent = #option joins the Clinical Task Intent value set, for a proposal that is one of a set of alternatives. Approving one moots its siblings, which are then cancelled with a statusReason recording that they stopped being on offer rather than that anyone declined them. Additive: no existing writer changes. (FHIR-71)
  • CodeSystem/nexus-task-input-type, giving Task.input.type a published label for the one slot a consumer branches on: suggested-resource, which points at the contained Bundle. The element stays otherwise free-text. (FHIR-71)
  • A priority below routine, as the priority-detail extension carrying trivial. Task.priority binds required to request-priority, whose floor is routine, and a profile may only strengthen a binding; the element is also a bare code with no second coding slot. So the refinement rides beside it under a rule that makes it safe to ignore: when the extension is present Task.priority shall be routine, so a consumer that does not read it under-states the task rather than mis-sorting it. (FHIR-71)
  • NamingSystem/nexus-cds-card, the identifier namespace for a decision-support card. One evaluation raises several proposals; each is its own Task, and every member carries the card on groupIdentifier so the set comes back from one Task?group-identifier= search with no parent resource. Values follow the harness key's rule -- derived from the gap the advice addresses, never from the evaluation that produced it. (FHIR-71)

AI attribution

  • An AI Attribution pattern page stating the AIAST rule once for the whole specification, and applying it to Composition (generated summaries and AI-scribed notes, including after a clinician edits and signs one -- attestation is a separate axis and both labels are carried), to RiskAssessment, whose "do computed scores carry it" question this settles, and to Task, where a proposal a graph or a decision-support rule put forward carries the label on the Task itself. Never on the contained payload: per dom-5 a contained resource cannot carry meta.security at all.

    Conversion from a source EMR is not authorship, so converted notes carry no label. Consumers must preserve the label when writing a labelled resource onward -- dropping it turns machine-produced content into apparently human-authored content. (FHIR-74)

Form extraction

  • BREAKING: a form carries its prototypes in a contained transaction Bundle, and only that. NexusEmrCoreQuestionnaire declares SDC's sdc-questionnaire-templateExtractBundle (0..1, Must Support) and prohibits the per-item sdc-questionnaire-templateExtract it carried in 1.20.0. Same container as nexus-template-list and a suggested-resource Task, and the same two invariants: the reference resolves to a contained Bundle, and that Bundle is a transaction. One container, three wrappers. (FHIR-71)

    Consumer action: every form stored against 1.20.0 needs its prototypes moved into a contained transaction Bundle, each as an entry with a fullUrl and a request, and its per-item templateExtract extensions removed. A form producing one resource has a one-entry Bundle.

  • Output that depends on the answers is expressed inside the Bundle. Each Bundle.entry carries a templateExtractContext expression naming the part of the response that fills it. An entry whose context selects a repeating item is instantiated once per repeat; an entry whose context matches nothing is removed, which the extension's own definition states. A repeating entry names its fullUrl by a templateExtractValue expression over an extractAllocateId variable, because one authored urn:uuid cannot distinguish iterations -- legal because templateExtractValue is declared with context Element, and spelled _fullUrl because fullUrl is a uri. (FHIR-71)

    This is a reading of SDC rather than a pattern SDC demonstrates. extract-complex-template2 leaves a single authored urn:uuid on an entry whose context is a repeating group, which is under-specified rather than wrong. If SDC lands a different answer, a stored form's repeating entries are the part that would need revisiting; nothing else here depends on it.

  • Why only one spelling, since SDC blesses two. Mixing them leaves a form with two prototype pools that cannot reference each other: a #id resolves against Questionnaire.contained, so an item cannot name a prototype carried as a Bundle entry, and inside an entry a #id resolves against that entry's own contained list, so an entry cannot name a loose one. A form needing an answer-driven output to cite a fixed one would have no legal way to author the link. This is a deliberate narrowing and a reversible one -- admitting the item spelling later invalidates nothing written against this profile. SDC ships the same demonstration form authored both ways (extract-complex-template and -template2, identical item trees over the same five outputs), which is what settles them as one capability rather than two features. (FHIR-71)
  • A worked example: Diabetes Annual Review. Three entries in one Bundle -- an Observation and the recall Task that cites it by fullUrl, produced whatever the answers say, and one Observation per reported hypoglycaemic episode, whose entry names its fullUrl by expression because it repeats. The Tobacco Use form is reshaped to the one-entry case. (FHIR-71)

Documentation

  • A Proposals page and a Suggested-Resource Tasks page, one per axis, both under Tasks. Between them they cover proposal-versus-order, where the prototypes actually sit, add / update / retire, the three verbs, the evaluation context, decision-support cards, citations and the idempotency key. (FHIR-71)
  • Three worked examples covering shapes that had none: a suggestion that retires a chart entry via a conditional PUT, and a pair of card alternatives at intent = #option. The lab-review example is now the worked review-report case: an incoming report already in the chart, named on focus, at intent = #order. (FHIR-71)

Changed

The suggested payload becomes a template

  • BREAKING: a suggested chart entry is carried in a contained transaction Bundle. A task carrying one contains a Bundle of type = transaction whose entries are the suggested resources, and the suggestion input references that Bundle rather than a resource directly.

    Why a Bundle, and not a List or loose contained resources. dom-2 says a contained resource SHALL NOT contain nested resources (contained.contained.empty(), error severity, unchanged in R5). Anything contained in a Task is forbidden from containing anything itself, so a contained List cannot hold the prototypes. Bundle is the one container exempt: it derives from Resource rather than DomainResource and has no contained element for the rule to find. This is SDC's own answer -- sdc-questionnaire-templateExtractBundle points at a contained transaction Bundle.

    What the transaction buys beyond legality. entry.request.method is per entry, so one task can create a Condition and update a MedicationRequest -- inexpressible before, because add-versus-update sat on Task.focus, which is 0..1. type = transaction states atomicity. ifNoneExist / ifMatch carry conditional writes. entry.fullUrl is how prototypes reference each other, sidestepping #id, which resolves against the root resource and would be ambiguous for a graph inside a Task. And instantiating becomes an operation every FHIR server implements: fill the prototypes in and POST the Bundle.

    Consumer action: read the suggested resources from the contained Bundle's entry.resource, and read entry.request for what each would do to the chart. (FHIR-71)

  • BREAKING: nexus-template-list carries its prototypes the same way. List.contained is now exactly one Bundle whose entries are the prototypes, and List.entry is exactly one item pointing at it; previously the prototypes were loose contained resources named individually. The List keeps the half a Bundle cannot do -- identity, name, lifecycle, tenancy stamp -- and the Bundle holds the graph.

    List.subject moves from 0..0 to 1..1 and is a placeholder: present, carrying a population expression as an extension, with no reference and no identifier (SDC's template-extraction pattern; Reference has no required children, so this is valid base R4). It SHOULD carry a display saying the value arrives on use. A template therefore still enters no patient compartment, which is the guarantee 0..0 used to buy, and it is now an ERROR-severity rule rather than a convention: nexus-template-subject-is-placeholder requires subject to carry neither reference nor identifier. Four invariants hold the profile, two of them new (nexus-template-subject-is-placeholder, nexus-template-bundle-is-transaction) and one renamed: nexus-template-entries-resolve becomes nexus-template-entry-is-contained-bundle, restated for the Bundle. A validator keyed on the old invariant id will not match.

    Consumer action: every stored template needs its prototypes moved into a contained transaction Bundle, each with a fullUrl and a request, and a placeholder List.subject added. The placeholder must be EMPTY -- a subject naming the patient the template was authored for, or carrying an identifier, is now an error-severity failure and would put every stored template into a patient compartment. The template picker's query (List?identifier=https://fhir.apps.health/NamingSystem/nexus-template|) is unaffected. (FHIR-71)

  • An evaluation context for template instantiation, named after SDC's launchContext and through it after SMART's launch parameters: %patient, %user, %encounter, %location and %clinical (the resource being reviewed -- Task.focus on a suggested-resource task). The notation is SDC's; the context is defined here, because SDC evaluates against a QuestionnaireResponse and this does not. (FHIR-71)

Task vocabularies

  • BREAKING: Task.code no longer carries suggestion, and the nine categories replace them outright. suggestion forced an orthogonal fact onto an element that holds one value: whether a task carries a suggested chart entry is answered by the containment, and a call, an appointment and a review-report can each carry one. Consumer action: a search for Task?code=...|suggestion returns nothing. To find tasks carrying suggestions, read the containment; to build a Suggestions list, search Task?intent=proposal. (FHIR-71)
  • BREAKING: Task.status binds a required subset, so requested, received, rejected, failed and entered-in-error are no longer conformant. requested was a second name for ready -- the profile documented them as one state, requested/ready -- and every stored task carrying it needs rewriting. rejected is the one that changes meaning: a declined proposal is now cancelled, the same as one that became moot, and Task.statusReason is what tells them apart. It is close to required in practice on a cancelled proposal, because without it the record cannot say whether a clinician considered this and said no. (FHIR-71)
  • BREAKING: identifier is no longer Must Support on ten profiles -- AppState, Task, Communication, Composition, DetectedIssue, DocumentReference, QuestionnaireResponse, Specimen and both Devices. The instance-scoped identifier scheme (.../NamingSystem/<instance>-<resource>-identifier) was retired in this release, and with it went the only thing that minted business identifiers onto these types. Must Support asserts that an implementer can be expected to populate an element; claiming it for one the specification neither mandates nor produces states a guarantee that is not being made.

    The element is unchanged in every other respect: same cardinality, same documentation, no binding, no mandated system. A writer that has a source-system identifier should still carry it, and a reader must still tolerate one. Patient, Practitioner and Organization keep theirs, because those are minted and their slices are real.

    Consumer action: none required. A consumer that drove behaviour off the must-support flag rather than off the element should stop expecting these identifiers to be present -- which they largely were not already. (FHIR-71)

  • Task.focus SHALL NOT be a local reference, enforced by nexus-task-focus-not-contained. focus names a resource that already exists in the chart, which was the intent throughout but was only ever implied. Contained content is referenced from input, never from focus. On a suggested-resource task it does not say whether a resource is created or updated -- that is entry.request.method, stated per resource, so one task can do both. (FHIR-71)
  • Task.focus accepts NexusEmrCoreAllergyIntolerance, a ninth target type. The list shipped without it and the omission was invisible: the retire worked example -- the one the suggested-resource handbook leads with -- carries an identifier-only focus, and a reference with no reference and no Reference.type gives a validator no target type to check. It conformed by being unresolvable. A producer doing the ordinary thing and writing AllergyIntolerance/123 would have been rejected by the example that taught them the pattern. Purely widening, and noted here because focus's type list was itself a named breaking change in 1.19.0. (FHIR-71)
  • NexusEmrCoreAllergyIntolerance.clinicalStatus relaxes from 1..1 to 0..1, because 1..1 made a retracted allergy unrepresentable. R4 carries a matched pair on this resource: ait-1 requires clinicalStatus unless verificationStatus is entered-in-error, and ait-2 forbids it when it is. The profile kept the first and collided with the second, so the one shape that must drop clinicalStatus was the one shape the profile required it on -- and retiring a chart entry, the case this release exists to model, is exactly that shape.

    Consumer action: none for a writer. ait-1 still requires clinicalStatus on every allergy that is not retracted, which is what the 1..1 was reaching for. A reader that treated clinicalStatus as always-present should now handle its absence, which means the entry was entered in error and must not be surfaced at all -- a different statement from inactive or resolved. (FHIR-71)

  • Both CapabilityStatements move their resource id off the FSH Instance name. NexusEmrServerCapabilityStatement becomes nexus-emr-server and NexusEmrSubscriptionsCapabilityStatement becomes nexus-emr-subscriptions. The publisher reports RESOURCE_ID_MISMATCH unless the id is the last segment of the canonical, and the id was set explicitly rather than by renaming the Instance so that the canonical URLs stay byte-identical -- those are what a conformant server is checked against.

    Consumer action: GET [base]/CapabilityStatement/NexusEmrServerCapabilityStatement (and the subscriptions equivalent) no longer resolves, and any ImplementationGuide.definition.resource reference naming the old id breaks. Use the new ids. No published page URL moves: neither statement renders a page. (FHIR-71)

  • BREAKING: the tobacco-use Questionnaire canonical moves from https://fhir.apps.health/Questionnaire/tobacco-use to https://fhir.apps.health/Questionnaire/nexus-emr-questionnaire-tobacco-use, bringing it into line with every other artifact id in this guide. The old canonical shipped in the 1.20.0 package.

    Consumer action: stored QuestionnaireResponse.questionnaire values pointing at the old canonical no longer resolve and must be rewritten. This is a one-line update per stored response; there is no automatic redirect. (FHIR-71)

AI attribution: two open questions narrow

  • NexusEmrCoreProvenance: the model-accounting question splits in two, and one half has an expected direction. Model identity and version and operational telemetry were recorded as a single item and are separable. The position on telemetry is settled and unchanged -- token spend, latency and cost are not clinical facts and stay out of the record. Model identity is genuinely open, and now names what would decide it: a consumer that must answer "which model version produced this statement" from the record alone, to withdraw or re-review every output of a defective version without the producing system's logs. External AI-transparency standards are moving toward requiring that, so expect this to be revisited rather than to stay open. No element changed. (FHIR-74)
  • NexusEmrCoreComposition: the ORIGIN question is narrower. The coarse question -- was any of this machine-produced -- is answered by AIAST and settled spec-wide. What stays open is only which mechanism carries the finer question of what a machine contributed and from where: author carrying a Device alongside the human, or a Provenance record. Neither is foreclosed. (FHIR-74)

Removed

  • Two Questionnaire invariants, with the extraction spelling they governed. questionnaire-extract-templates-are-contained and questionnaire-extract-templates-resolve both constrained sdc-questionnaire-templateExtract, which this release prohibits; they are replaced by questionnaire-extract-is-bundle-only plus the two Bundle rules. A form conforming to 1.20.0 does not conform to this release -- see Added. (FHIR-71)
  • The *-has-raw-identifier invariants are gone, from all 34 profiles that carried them. They were warning severity and advisory: each fired unless a resource carried at least one business identifier from a system other than the Nexus canonical one. Nothing replaces them. Carrying a source identifier is still the right thing to do where the resource was converted from a source EMR, and every profile still says so in prose, but it is no longer checked.

    Consumer action: a validation run that used these warnings as a lossy-conversion detector loses that signal in this release, and gets no warning whatever it does. There is no substitute in the specification; a converter that wants the check now owns it. identifier cardinality and slicing are unchanged on every profile -- nothing about what you may write has moved, only what is reported back. (FHIR-71)

  • BREAKING: CodeSystem/nexus-emr-suggestion, which shipped in 1.20.0. Its canonical no longer resolves and stored Coding.system values pointing at it are unresolvable. It bundled a task category and a structural input label under one name because both mentioned suggestions; they are separate axes and now live apart. The codes are unchanged, the systems are not:
    was is now
    nexus-emr-suggestion#suggestion on Task.code deleted -- see the Task.code entry above
    nexus-emr-suggestion#suggested-resource on Task.input.type nexus-task-input-type#suggested-resource

    Consumer action: rewrite the system on stored input labels to https://fhir.apps.health/CodeSystem/nexus-task-input-type. Stored Task.code values need a category from https://fhir.apps.health/CodeSystem/nexus-task-code, or removal -- code is optional. (FHIR-71)

Fixed

  • Two value sets had an id that disagreed with their own canonical, and the page URL followed the id rather than the canonical. vs-nexus-emr-indigenous-identity and vs-nexus-emr-racialized-group both published a ^url of .../ValueSet/nexus-emr-indigenous-identity and .../ValueSet/nexus-emr-racialized-group respectively, so the artifact a consumer resolves was never the one the page was named for. The ids now match the canonicals.

    The canonical URLs do not move -- they were always these values, and stored data referencing them is unaffected. Two page URLs do: ValueSet-vs-nexus-emr-indigenous-identity.html and ValueSet-vs-nexus-emr-racialized-group.html are gone, replaced by the same names without the vs- prefix. A bookmark or partner reference to either page needs updating. (FHIR-71)

  • A suggested-resource example carried a business identifier under a namespace that does not exist. NamingSystem/nexus-harness-suggestion-identifier is defined nowhere in this specification; the example now carries the harness key it was supposed to demonstrate, valued from the gap rather than from the run. (FHIR-71)
  • The specification said a form could not use a contained Bundle, and gave a reason that does not survive reading SDC. The claim was that "a repeating item produces one resource per answer and none if unanswered, which a static transaction Bundle cannot express". A Bundle entry is not static in that sense: sdc-questionnaire-templateExtractContext is legal on Bundle.entry, its own definition says an element whose expression yields no results is removed, and SDC's extract-complex-template2 example runs one authored RelatedPerson entry over a repeating group. Corrected across the Templates page, both profile intros, and the Questionnaire, nexus-template-list and Task profile text. The claim was load-bearing: it was the whole reason a form was modelled as the exception, and with it gone the form uses the same container as everything else. See Added. (FHIR-71)
  • Handbook links on profile pages sent readers of a local build to the live site. The source must keep the absolute URL -- the IG Publisher renders src/pages/ at the site root, so a relative ../guide/... climbs out of the output directory and aborts the run. The docs generator now rewrites that published prefix to a relative path when it embeds the page for this site, so the same source is correct in both places. Eleven links across eight pages. (FHIR-71)

[1.20.0] - 2026-08-24

Generated by TypedFhir 0.15.0. Bumped from 0.14.0, which generated 1.19.0. All four published packages move together.

Two things this specification could not describe, it now can. A referral is not an order, and the order profile's fixed intent, required category binding and mandatory code made a referral unrepresentable rather than merely awkward; NexusCoreReferral is a sibling profile for it, and NexusEmrCoreServiceRequest is unchanged in every constraint. A Questionnaire profile closes the half of forms the IG had left ungoverned -- it profiled the answer set and not the form that produced it -- and forms and letter templates turn out to be one mechanism rather than two that resemble each other.

Drug identity moves off the DIN alone. All three medication resources and AllergyIntolerance now slice ingredient-level codes, because every cross-record question -- is this duplicate therapy, are these the same drug, does this medication match a recorded allergy -- is an ingredient-level question, and a DIN identifies one manufacturer's marketed product and so cannot answer any of them.

Terminology documentation is the largest single body of work in the release. Every code system this specification binds now has a page saying what it is, who publishes it, where to obtain the codes and whose licence decides whether they may be reproduced: nineteen new artifacts covering thirty-four pinned system URIs, an evenly-spread code sample on the nine systems whose terms permit one, an explicit allow-list so a system added later publishes nothing until its terms have been read, and full code lists on the fourteen value sets over this specification's own systems that previously said "All codes from this system" and listed none.

On the library side, 279 of the 736 typed accessors change their return type, and the Python module layout moves beneath the top-level import. The read side is where the exposure is, and most of it keeps working, which is the problem.

Read these four before consuming this release:

  • The package id is nexus, not nexus-emr -- one line in your dependency declaration. No canonical in the namespace changed and nothing in stored data moves.
  • The DIN code system URI moves, and that one IS in stored data, on MedicationRequest.
  • 279 accessors change return type. Audit for .coding / .system / .code / .id reached directly off an accessor named in the table under TypedFhir libraries.
  • validate() is stricter twice over -- unknown elements now error, and twenty-two base-R4 required bindings are enforced that previously could not be read. Run your corpus through it before upgrading.

Added

Profiles, value sets and terminology bindings

  • A Referral profile -- the IG could not describe a referral, and now can. NexusCoreReferral (https://fhir.apps.health/StructureDefinition/nexus-core-referral) is a new ServiceRequest profile for a request that another clinician or service take the patient on. It is additive: NexusEmrCoreServiceRequest is unchanged in every constraint, so laboratory and imaging orders keep the guarantees they already had.

    A referral could not claim the order profile, and this was not a near miss. That profile fixes intent = order, binds category (required) to the five-concept base FHIR value set, and requires code 1..1. A referral is intent = proposal -- the referrer asks a receiving service to accept the patient and has no authority over whether it does -- its category is the receiving service or specialty, which that five-concept set cannot express, and the Ontario eReferral contract sends no code element at all. Three constraints, three mismatches.

    The new profile is a sibling derived from base R4, not a child of the order profile: a derived profile may tighten its parent but never loosen it, so a fixed order and a required binding are not recoverable downstream. intent is fixed to proposal, category is 1..* bound extensibly to the new NexusReferralServiceType value set, code is 0..1 with an invariant requiring code.text when it is present, and requester is required. A second invariant, referral-category-coded, asks for at least one SNOMED coding on category at warning severity rather than error, for the same reason the binding is extensible: destinations do arrive with no usable code, and the right answer for one of those is to carry the text rather than map it to an approximate neighbour, because an approximate destination routes the patient approximately.

    This aligns with the pan-Canadian and Ontario standards rather than inventing beside them. CA-eReC fixes intent = proposal and requires requester and authoredOn; the Ontario eReferral profile derives from CA-eReC; this IG already carries ca.on.erec.r4 as a pinned dependency. Two divergences are deliberate and documented on the profile itself: performer stays 0..* where CA-eReC requires it (that profile governs electronic messaging, where a destination is definitionally known, while this one also covers referrals recorded in the chart that were never sent electronically), and category does not adopt CA-eReC's binding -- see below.

  • A NexusReferralServiceType value set for referral destinations, mirroring Canada Health Infoway's active ReferralServiceCategoryCode over SNOMED CT Canadian Edition reference set 379431000087101.

    It is deliberately not called a specialty value set, and that is not cosmetic. Several destinations a primary-care referral routinely targets exist in SNOMED CT only as service concepts, with no specialty concept at all -- Sports medicine, Sleep medicine, Spinal surgery and Clinical neurophysiology among them. A specialty-only value set cannot describe the destinations this EMR already offers, which is why Infoway's set is a union of healthcare services and provider specialties. Implementers building a destination picker should expect both kinds of concept and must not filter to the specialty hierarchy.

    CA-eReC's own category binding is not adopted. It binds InterventionCodeSubsetCare, which represents "the care procedures performed by a Provider" -- an intervention taxonomy, which is what code means, not the destination category carries -- and which Infoway retired in April 2026 along with the Primary Health Care EMR Minimum Data Set, marking it "not recommended for continued use". code is therefore left with the base R4 binding until Infoway publishes a replacement.

    The binding is extensible, not required, and that is load-bearing rather than a hedge: Ocean sends 17561000 |Cardiologist| -- an occupation concept -- in category, and it is not a member of this value set. A required binding would make the referrals actually received non-conformant. Preserve the source coding exactly as received and add the Nexus coding alongside it in the same CodeableConcept; do not rewrite or drop the original. The new handbook page covers the Ocean relationship and the coding guidance in full, and the shipped example demonstrates the dual-coding pattern.

  • A Questionnaire profile, and forms are now the same mechanism as letter templates. NexusEmrCoreQuestionnaire closes the half of forms the IG never modelled: it profiled the answer set (QuestionnaireResponse) and not the form that produced it, so a response pointed at a definition the spec did not govern. The profile requires url and version -- a response is only interpretable against the version it answered, because the question text, item types and answer options all live in the form -- and binds answers to the chart through item.definition and item.code rather than a private extension. The structural news for implementers is that a form and a template are one mechanism. Both carry contained prototypes of what they produce and are instantiated by cloning them and filling in what the definition could not know; they differ only in what drives the fill (a QuestionnaireResponse, or an instantiation context). The handbook's Templates page now documents that once for both, and each profile page points at it. Named as not settled rather than approximated: approximate dates and durations (a real clinical requirement with no FHIR datatype), body diagrams, note generation, and FHIRPath expressions.

    SDC extraction is governed by two invariants, and nothing checks them at runtime. (Both were removed in 1.21.0 along with the spelling they governed -- see that release. Named here without links, because the anchors they had are gone.) questionnaire-extract-templates-are-contained requires every sdc-questionnaire-templateExtract to target a prototype contained in the form, and questionnaire-extract-templates-resolve requires that #id to resolve. Both traverse descendants().ofType(Extension), which reaches the root's own extensions, so an extraction at the form root -- the ordinary case -- is examined rather than passing vacuously; and both read the prototype from the complex extension's template sub-extension (extension('template').value.ofType(Reference)). SDC prohibits Extension.value[x] on templateExtract itself, so a form carrying valueReference there is not valid SDC and does not pass. Both are declared #error and both sit on the runtime allowlist, as do base R4's que-1, que-2, que-7, que-10 and que-13: the HL7 validator enforces all seven against this IG's examples and validate() enforces none of them. If you author forms programmatically, run the published validator or check containment yourself. The profile ships with a worked example, and the extraction rules were mutation-tested in both directions -- a stored-resource target and a dangling #id each fail the rule that should catch them, and a conformant root-level and item-level extraction each pass.

  • Medications now carry drug identity at TWO levels, and all three medication resources carry the same model. MedicationRequest, MedicationStatement and MedicationDispense each slice the medication concept's coding into din, ccdd, atc and aign. A DIN identifies one manufacturer's marketed product; CCDD (http://terminology.hl7.org/CodeSystem/hc-CCDD) and ATC (http://www.whocc.no/atc) identify the ingredient. Every cross-record question is an ingredient-level question -- are these two prescriptions the same drug, is this duplicate therapy, does this medication match a recorded allergy -- and a DIN cannot answer any of them, because the same drug from a second manufacturer carries a different DIN and a change of salt changes it again. Until now the spec offered a DIN slot and nothing else, and only on MedicationRequest; the other two named code systems in prose that constrained nothing. Nothing here is required and nothing existing is invalidated: every slice is 0..*, the slicing is open, text remains the only requirement on the concept, and instances carrying no coding, or an unsliced one, stay conformant. Consumer action, if you write these resources: carry the ingredient-level code the source actually has, alongside the DIN rather than instead of it. Do not synthesize one that was not given to you.

    The aign slice carries Health Canada's DPD active-ingredient group number (http://terminology.hl7.org/CodeSystem/v3-hc-aign, published through HL7 Terminology and resolvable from the hl7.terminology.r4 package this IG already depends on), and it is the one to reach for when there is no CCDD -- in practice most legacy EMR medication lists, where a DIN and a line of free text is all that exists. It gathers products sharing an active-ingredient composition across manufacturer and salt form and covers the whole DIN space, so it is reachable from a DIN when neither CCDD nor ATC is.

    The number is ten digits and it decomposes, which decides which of two systems a record should carry: [2 digits: how many active ingredients][5 digits: which active-ingredient group][3 digits: which strength group]. The full ten-digit number is therefore strength-specific, which is what a prescription, a dispense and a medication statement mean -- so the medication profiles bind it. AllergyIntolerance binds the five-digit middle portion instead (http://terminology.hl7.org/CodeSystem/v3-hc-aigc), because an allergy is to an ingredient and not to a strength. To match a medication against an allergen, compare positions 3 through 7 of the medication's number to the allergen's code -- they are different systems and different lengths, and comparing them as equal strings never matches.

  • AllergyIntolerance.code now has a drug-allergen path, via ccdd, atc and aigc slices on code.coding -- the same posture as Condition.code.coding[icd10ca] and Procedure.code.coding[cci], where the IG declares the slot and the system and does not enumerate codes. The element previously required code.text and mandated no system at all, which left no join key between an allergen and a medication: matching meant string comparison over clinician-typed free text. An allergen coded with CCDD, ATC or the DPD active-ingredient group is now matchable against a medication coded the same way -- the three medication profiles slice the same systems, because a join key has to be sliced on both sides or it does nothing. The one exception is the active-ingredient group, where the allergen slice binds the five-digit code (v3-hc-aigc) and the medication slices bind the ten-digit number (v3-hc-aign): an allergy is to an ingredient, not to a strength, and matching the two means comparing positions 3 through 7 of the number to the code rather than comparing the two values directly. There is deliberately no din slice. A patient is not allergic to a marketed product, they are allergic to what is in it; a DIN-to-DIN comparison misses the same drug from another manufacturer and misses a different salt of the same ingredient, so a DIN slot on an allergen would look like a join key while silently failing the cases the join exists for. All three slices are 0..* and the slicing is open, so existing allergy records are unaffected.

Terminology documentation

  • Every code system this specification binds now has a page that says what it is. The profiles pin thirty-four system URIs. Some were minted in this specification's own namespace and defined nowhere, so following one reached nothing; the rest were third-party systems with no statement of what they are, who publishes them, or where an implementer gets the codes. Both gaps are closed by nineteen new artifacts, and a pinned .system value on a profile page is now a link to the page describing it rather than a bare URI.

    Five artifacts define URIs this specification mints: a CodeSystem for the OHIP Schedule of Benefits (.../CodeSystem/on-ohip-schedule-of-benefits, content = not-present -- the system is declared, the Ministry of Health's fee codes are not reproduced), and NamingSystem resources for the OHIP billing number, the Ontario RMA / group billing number, the Ontario Service Location Indicator and the patient MRN. Each says what the identifier is, who assigns it, and what it does not mean -- the MRN page in particular, because an EMR-local chart number is unique only within the deployment that issued it and must not be used to match patients across systems.

    Thirteen third-party systems get a ValueSet in this specification's namespace (.../ValueSet/all-<system>), each a single include of the whole system with no codes enumerated: LOINC, SNOMED CT, UCUM, ICD-10-CA, CCI, DIN, CCDD, ATC, the two Health Canada active-ingredient group systems, the HL7 identifier-type codes, NullFlavor and data-absent-reason. Each page states what the system is, who publishes it, its canonical URI, where to obtain the codes, and where a licence gates use (ICD-10-CA and CCI are CIHI-licensed; SNOMED CT requires an Affiliate licence). No CodeSystem is published at a canonical this specification does not own -- a second definition of a canonical its publisher already serves is how one identifier acquires several spellings, so these claim no foreign canonical and redistribute nothing.

    The internal EMR user-account system gets a NamingSystem rather than a value set, because it names an identifier AUTHORITY and not a terminology: there is no concept list to point at and the values are opaque account ids, so "every code in the system" would be a category error. Its page says what the profiles already say -- it is private infrastructure and external consumers must not depend on it.

    Where the publisher's terms allow it, the page also shows a sample of the codes -- up to 500, spread evenly across the system rather than taken off the front, with the size of the population they came from stated above the table and again below it, so a list that stops at 500 cannot be read as a complete one. Nine systems qualify: LOINC, DIN, the two Health Canada active-ingredient group systems, ATC, UCUM, and the HL7 identifier-type, NullFlavor and data-absent-reason codes. Each carries the attribution its licence requires.

    The LOINC sample excludes every term carrying a third-party copyright notice -- 7,355 of them, mostly scored assessments and survey instruments whose questions belong to someone other than Regenstrief. The LOINC licence would allow reproducing those provided the notice travelled with them; dropping them instead means no third-party instrument text is republished here at all.

    Where the terms do not allow it, the page says so and says whose terms decide it. No ICD-10-CA, CCI or SNOMED CT code is reproduced anywhere in this specification, and none will be: rendering codes onto a published page is redistribution, and those are licensed by CIHI and SNOMED International respectively. CCDD is withheld as well, because redistributing its content is a permission separate from the free use it grants. The list of systems whose codes may be reproduced is an explicit allow-list, so a system added to this specification in future publishes nothing until its terms have been read and recorded. This applies to CodeSystem artifacts that declare a system without carrying its content, not only to value sets -- from a reader's point of view they are the same page, so they answer the question the same way.

    OHIP fee codes are among the ten, on the King's Printer's legal-materials permission: the Schedule of Benefits is incorporated by reference into R.R.O. 1990, Reg. 552 under the Health Insurance Act, and the King's Printer permits anyone to reproduce the text contained in Ontario's regulations without permission or charge, provided the reproduction is accurate and is not represented as an official version. Both conditions are met on the page, which states in terms that it is not an official version and that the Ministry's edition in force governs. The sample omits the 406 codes that carry no published description in any schedule, rather than listing them against their own code.

  • A value set over one of this specification's OWN code systems now lists its codes in full. Fourteen pages -- appointment modality, clinical roles, province/territory, pay program, task business status and the rest -- said "All codes from this system" and then listed none, which is the least useful thing a page about a five-code vocabulary can do. These are not a licence question at all: the concepts are in this specification's own artifacts, so the page shows every one of them rather than a sample.
  • The OHIP Schedule of Benefits page now carries the ICD-9 warning about Ontario's DIAGNOSTIC codes. They are a separate Ontario code set with its own system URI, they are ICD-9-LIKE in shape, and they are not a subset of ICD-9 -- so an OHIP diagnostic code must never be derived by truncating an ICD-9 code. The 895-971 range is a parallel Ontario space laid over ICD-9 numbers that mean something unrelated: 970 is Prenatal Care in Ontario and poisoning by CNS stimulants in ICD-9. Measured against the Provincial Health Care Vocabulary Standard, a SNOMED CT to ICD-9 to truncate route reaches 69.4% of cases and 124 of them come out valid and wrong -- nothing downstream rejects them, so they reach a claim and the diagnosis on the record is not the one the clinician made. Eighteen codes carry two unrelated meanings within the Ontario set itself. The warning was in the specification's source and on no page a reader would find.
  • The IP statements page names each terminology, flags the ones you must hold a licence for, and links where this specification binds them. It was sixteen unlabelled licence paragraphs in one flat list: nothing said which terminology a paragraph was about except the prose itself, nothing distinguished a notice you reproduce from a licence you must hold, and nothing connected a statement to the profiles and value sets that made it apply. The page now opens with a table -- terminology, system URI, and whether a licence is required -- and each statement is its own section, carrying the licence text unchanged plus the artifacts of ours that bind that system, linked. SNOMED CT, ICD-10-CA and CCI additionally carry a warning callout, so the three obligations the page's opening paragraph names are visible beside the terms rather than only in the preamble. No statement's text changed and the build check behind the page is unchanged: a bound code system with no statement still fails the release.

Guide, examples and site

  • Structured Data Capture is a new package dependency (hl7.fhir.uv.sdc), and it is the reason the above is one mechanism rather than two that resemble each other. SDC's template extraction is contained-prototypes-cloned-and-filled -- the same shape this IG reached independently in 1.18.0 -- so the Questionnaire profile adopts its vocabulary natively instead of minting a parallel one. Consumers must resolve one more package. Note that the extraction extensions are draft inside a 4.0.0 ballot and will move, so the dependency is pinned to an exact build, and a template List still does not require a Questionnaire to exist -- only the substitution vocabulary is shared.
  • Conversation Threading is now a guide topic of its own. One page states the mechanism behind every conversation in the chart -- a colleague's question, an accepted suggestion, and a back-and-forth with an AI agent are the same shape: the opening body in Task.description, the turns in Task.note. It documents the accept/resume round trip end to end (promote the payload, record what was actually created, say what was decided, hand the task back to the requester so the harness resumes the named thread) and the rendering rule that a thread is description followed by the notes in order. No profile changed.
  • Two worked examples, each for a relaxation that shipped with nothing exercising it. NexusEmrComposition-converted-summary-example is a summary classified on category alone -- the shape the profile's prose documents and the shape the attester exemption used to reject, so the example now holds that behaviour in place rather than describing it. NexusEmrCareTeam-nursing-pool-example is a CareTeam with no subject, the population that relaxation was made for: a standing nursing pool belongs to the clinic rather than to a patient. Both relaxations were announced in 1.19.0 with no instance demonstrating them, which is how the attester contradiction survived a release -- the only Composition example in the IG happened to carry the summary code in both category and type, so it passed for a reason the prose does not ask anyone to reproduce.
  • The 404 page renders. It has been served unstyled since this site was Polaris's -- bare checkboxes, a broken logo and the theme's icons at their intrinsic size -- because site_url was never set. Every other page gets relative asset paths; 404.html cannot, since it answers for a URL at any depth, so mkdocs writes root-absolute paths and derives the root from site_url. Absent, it assumed the domain root, and every stylesheet and script on that one page 404'd under /nexus-ig/. It lasted this long because a link checker never asks for a page that does not exist, so nothing ever fetched it.
  • The release feed is reachable from every page, not only from the page source. An RSS icon and the word RSS now sit beside the GitHub link in the site header, and in the drawer on narrow screens, pointing at the same releases.xml the Releases page links. Neither the feed nor its <link rel="alternate"> autodiscovery is new; what was missing was anything a reader could see and click, so the one supported way to be told when this specification changes was discoverable only by viewing source or by already being on the Releases page. The feed carries each release's entry and links its package directly, which is enough for a feed reader or a Teams/Slack connector.

Changed

Specification

  • BREAKING -- the package id is now nexus, not nexus-emr. The tarball, ImplementationGuide.id and .packageId, the ImplementationGuide canonical (https://fhir.apps.health/ImplementationGuide/nexus) and package-list.json's package-id all move together. If you depend on this IG, change the package name in your sushi-config.yaml dependencies (or wherever you resolve it) from nexus-emr to nexus.

    It is breaking for one line in your build and for nothing else.

    No identifier in the namespace changed, and nothing in stored data moves. Profile, extension, CodeSystem and NamingSystem canonicals are byte-for-byte what they were -- .../StructureDefinition/nexus-emr-core-patient is still that, and .../CodeSystem/canadian-phn-systems is still that. This is deliberately NOT what 1.15.0 did: that release renamed the identifiers themselves, which changed Coding.system and Identifier.system values that exist in stored data, and it was withdrawn the same day. The package id is not an identifier in the namespace; it is the name the package resolves under, and it appears in no resource.

    The -emr- infix is being retired where retiring it is free. This and the registry selector (fhirToResources({ ig: "nexus" }), previously "nexus-emr") are the two places that cost nothing. The identifiers stay.

  • The archived package downloads are renamed, and five previously published links move. packages/nexus-emr-<version>.tgz becomes packages/nexus-<version>.tgz for 1.15.1, 1.16.0, 1.17.0, 1.18.0 and 1.19.0 as well as for this release. The version-history page links the new names; the old URLs 404. Consumer action: if you bookmarked or scripted an archive URL, update it. Nothing about the PACKAGES moves -- an already-published package keeps the id it was published with, and FHIR tooling resolves by id, not by the filename on this site. The filename is deliberately not derived from the package id for exactly this reason: the id has changed twice across releases and a single stem needs no per-version exception map.
  • BREAKING -- the DIN code system URI moves to http://hl7.org/fhir/NamingSystem/ca-hc-din. MedicationRequest's din slice fixed http://hl7.org/fhir/sid/ca-hc-din, and it is the only slice ever published on that system: MedicationStatement and MedicationDispense gain theirs in this release (see Added) and carry the new URI from the start, so neither has ever carried the old one. No publisher declares the old URI. HL7 Terminology declares the Drug Identification Number system under OID 2.16.840.1.113883.5.1105 with three URIs: the NamingSystem form above, marked "official canonical URL going forward"; http://terminology.hl7.org/CodeSystem/ca-hc-din, retained for backward compatibility; and a canada.ca URL. The sid form is not among them, and the system is not served at it.

    This changes a Coding.system value that exists in stored data. Unlike the package rename above -- which moves the name a package resolves under and appears in no resource -- this one is in your medication records. Consumer action:

    - Writers must emit http://hl7.org/fhir/NamingSystem/ca-hc-din on the din slice. An instance carrying the old value no longer matches the slice; because the slicing is open it does not fail validation, it falls into the unsliced remainder, so anything reading coding[din] silently gets nothing. - Readers should accept both for as long as their own data carries either. The two URIs name the same code system, and the OID is the identity that holds across every spelling. - The exposure is MedicationRequest only. That is the one profile whose din slice shipped with the old URI, so it is the only place stored data can be carrying it. - This release does not migrate stored data. No Coding.system value is rewritten by installing it, and nothing detects or converts the old value on ingest. Both remain to be done and are not part of this change.

    Nothing else about the slice moves: the code values, the cardinality, the slicing mode and the representative-DIN strategy are all unchanged.

  • The server CapabilityStatement gains Questionnaire, and advertises two profiles on ServiceRequest. nexus-emr-server now declares Questionnaire with read, search, create and update and no delete, matching the profile's SmartExposedNoDelete grant: retiring a form is a status change, not a delete, because responses to a retired form must stay interpretable and removing the definition would strand every QuestionnaireResponse pointing at its canonical. ServiceRequest now lists both NexusCoreReferral and NexusEmrCoreServiceRequest in supportedProfile, so a client is told both canonicals it may meet at that endpoint. Which one applies is decided by intent, not guessed from category. Consumer action: an app that enumerates reachable types from the CapabilityStatement will see one more, and a client that assumed one profile per resource type must branch on ServiceRequest.
  • NexusEmrCoreServiceRequest no longer advertises referrals -- documentation only, no constraint changed. The profile's Description, its root and category definitions, its intent definition and its narrative page all said it covered referrals. Its binding forbade one: category is required-bound to a value set with exactly five members (108252007 Laboratory procedure, 363679005 Imaging, 409063005 Counselling, 409073007 Education, 387713003 Surgical procedure) that contains no referral concept and is not a subtree one could be added under. An implementer reading the definition and then binding-checking found the two disagreed, and the nearest usable concept was Counselling, which would have mislabelled every referral.

    Nothing was relaxed and no published canonical changed meaning. intent is still fixed to order, category is still 1..1 required-bound, code is still 1..1 with code.text. The text now says what the constraints already said, and points at NexusCoreReferral for the case it never covered. No action for consumers of laboratory or imaging orders.

    Worth stating plainly for anyone who assumed otherwise: base R4 puts ServiceRequest.category at 0..* with an example binding, and describes the resource as covering "a referral or a transfer of care request". This profile's required, exactly-one binding is a Nexus tightening, and the referral profile exists because that tightening is correct for orders and wrong for referrals -- not because the base resource is ambiguous.

  • The medication coding slices are discriminated by value, not pattern -- non-breaking. MedicationRequest.medicationCodeableConcept.coding has been sliced on system since it was first profiled, with discriminator.type = #pattern; it is now #value, matching the Condition.code.coding[icd10ca] and Procedure.code.coding[cci] slices and the two medication profiles that gain slicing in this release. No instance that validated before fails now. The discriminator path is system, a primitive uri fixed with (exactly), and for a fixed primitive the two discriminator types resolve to the same test: exact string equality. The two diverge only on complex types, where pattern is a partial match and value is not, and there is no complex type on this path. It is recorded here because it is a real edit to a published StructureDefinition, and a consumer reading the slicing metadata will see it change.
  • MedicationDispense no longer recommends RxNorm. The medicationCodeableConcept definition recommended "DIN, RxNorm, or other standard terminology". RxNorm is a US drug vocabulary, and a Canadian dispense record has no route to an RxNorm code that does not pass through a mapping this specification does not define; Canada uses DIN at the product level and CCDD at the ingredient level, and the definition now says that instead. The slicing is open, so a source that genuinely holds an RxNorm code may still carry it as an unsliced coding -- this withdraws a recommendation, not a permission.
  • BREAKING: the composition-signed-has-attester exemption admitted the wrong set, and rejected the documented one. The Composition profile's prose is explicit that category is the classifier and that type cannot be one -- "in converted source data, often a constant (every OSCAR casemgmt_note arrives as LOINC 11488-4 whatever it is)". The invariant then required the summary code in type. A converted summary written exactly as the profile documents was final, carried no attester, and failed. The IG's own example passed only because it happens to carry the code in both places, which is not what the prose asks for, so nothing surfaced the contradiction.

    The exemption now reads: EITHER classifier says summary, AND category says nothing else. The or admits the documented shape and the type-only shape; the and still closes the original hole, in which an authored note could buy its way out of a signature by gaining a second category. Consumer action: a generated summary classified on category alone now validates where it previously did not -- if you worked around this by writing the code into type as well, you can stop, and the documented shape is the one to write.

  • BREAKING: Task.note is now actually validated against nexus-emr-task-note, and 1.19.0's announcement of this was premature. That release said "every Task.note turn now REQUIRES author and time". The profile said so; nothing checked it. Task.note names nexus-emr-task-note in type[].profile, and the profile requires author[x] and time and carries an error-severity invariant -- none of which ran in any of the three ports. Profile selection keyed on kind === "resource" and the rescue path for the other case keyed on isExtensionSd; a datatype profile is complex-type and is not an Extension, so the IG's first and only one fell through both filters. typeProfiles was inert at the far end too: the IG model has always carried the field and no port has ever read it, so the reference pointed nowhere from both directions.

    Consumer action, and read this even if you already migrated for 1.19.0. If you took that release's word for it, changed your writer, and confirmed against a running validator that nothing failed -- the confirmation was worthless, because nothing could fail. Re-check against this release. Unattributed or undated turns now produce validation errors, and if you concluded from a green 1.19.0 run that your existing data was already compliant, that conclusion has not been tested.

    The rules are FLATTENED onto the element's own path (["note","author[x]"], ["note","time"]) rather than resolved from typeProfiles at runtime -- that is the nested-cardinality shape the validators already run and already test, and resolving instead would have added a resolution step to three ports that then had to be kept in agreement. The invariant anchors per note, the way base-FHIR backbone invariants already do.

  • Templates say why they carry no url or version, and name a rule the profile cannot enforce. The asymmetry with the new Questionnaire profile is deliberate: a Questionnaire stays relevant to the rendering of a response, while a template's result is self-contained -- a letter made from a template is a complete Composition and needs no template to render. The rule consumers must implement: at most one List with status = current may carry a given template identifier value, and more than one is a corrupt state in which a consumer MUST raise an error rather than choose between them. Not newest-by-date, not first-result, not both shown to the user -- every such heuristic makes the defect permanent and invisible while the picker keeps working. It is a cross-resource constraint, so no invariant can express it; the gateway check is prevention and the thrown error is detection, and both are needed.
  • The handbook and the reference IG now cross-reference each other. The first mention of each resource and element in a guide page links to its reference page, elements to the exact row anchor; profile pages link back to their handbook topic. Reader-facing only, no profile changed.
  • "Clinical Tasks" is now "Tasks" in the guide, with Conversation Threading nested under it rather than sitting as a sibling topic.
  • The site's About section is gone, and two published pages now 404. guide/about/version-history.md and guide/about/downloads.md are removed. Each was half an answer -- one listed which versions exist and what you could fetch, the other said what changed, and each opened by pointing at the other -- so both are now the Releases page, whose table carries version, date, the KIND of change the release contains and the package link on one row. Consumer action: update a bookmark to either retired page; the Releases page keeps the changelog.html URL it has always had, so an inbound link to THAT one, and every item already sitting in a feed reader, still resolves. The two About pages that survive, IG metadata and IP statements, keep their guide/about/ URLs and move only in the navigation: they now sit under a top-level Releases tab beside Reference and Handbook, rather than four levels down the Handbook, which is the wrong place for the page carrying a licence obligation.

TypedFhir libraries

  • BREAKING: 279 of the 736 typed accessors change their RETURN TYPE, in all three published packages. This is the largest consumer-visible change in the release. Counted by diffing every generated @property against 1.19.0 in the Python port; the TypeScript and C# ports move with it, name for name.

    Accessors Was Is What it is
    87 str \| None FuzzyDate \| None every date accessor -- authored_on, birth_date, recorded_date, issued, start, end, sent, expiration_date, ...
    85 Json \| None list[Json] \| None the *_raw twins of the repeating elements below
    62 Concept \| None Concepts repeating coded elements -- category, reason_code, specialty, body_site, interpretation, ...
    25 Reference \| None References repeating references -- author, performer, based_on, part_of, payor, focus, ...
    15 list \| None Notes / Names / Addresses / Telecoms note threads, names, addresses, telecoms
    5 str \| None Concept \| None coded statuses -- clinical_status, verification_status, scope, policy_rule

    None of the supporting types (Members, Concepts, References, Notes, Names, Addresses, Telecoms, Annotation, HumanName, Address, ContactPoint, FuzzyDate) existed in any port at 1.19.0.

    Why a repeating element had a singular accessor at all, and why that was the bug: the old accessor returned element[0], so a caller read the first category and, writing it back, erased every other one. A collection is what makes append expressible.

    Consumer action, and the read side is where the exposure is. The WRITE side is defended: a bare Concept or str assigned to a now-repeating setter raises a loud TypeError naming the expected type, in Python and TypeScript both, rather than silently replacing the element. The read side is quieter, because most of it keeps working:

    • x.category.display still works: a collection's display joins its members with "; ".
    • if x.author: still behaves for a one-author resource.
    • x.category.coding does NOT: it is AttributeError: 'Concepts' object has no attribute 'coding'. Take a member first -- x.category[0].coding, or iterate.
    • x.author.id likewise; x.author[0].id or x.author.to("Practitioner", id).
    • Arithmetic or slicing on a date accessor as a str now sees a FuzzyDate. In Python it is a str subclass so most of it keeps working; in TypeScript and C# it is not, and it wants .value / .Value. In exchange the value answers .precision, .year and .overlaps.

    Audit for .coding / .system / .code / .id reached directly off an accessor named in the table above. That is the shape that breaks.

  • BREAKING: validate() now reports an ERROR for any element that is not part of the resource type in R4. Previously an unrecognised top-level key was ignored, so a typo (effectiveDatetime for effectiveDateTime), a field carried over from another type, or a hand-built resource with an invented element all validated clean and were then rejected or silently dropped by the store. The message says which of the two you should expect. Consumer action: run your existing corpus through validate() before upgrading -- resources that passed may now report errors, and every one of them names something the store was never going to keep. fhir_comments is exempt: it is a serialization convention rather than an element, and is legal anywhere.
  • BREAKING (TypedFhir Python): five public modules moved, and there are no forwarding shims. typed_fhir.fuzzy, typed_fhir.units and typed_fhir.vocabulary are now typed_fhir.values.fuzzy, typed_fhir.values.units and typed_fhir.values.vocabulary; typed_fhir.import_ is typed_fhir.ingest.import_; and typed_fhir.facade_diagnostics is typed_fhir.diagnostics.facade. The root package had accumulated a module per concern and its own headers had stopped describing it, so each cluster is a directory now and the root is entry points and primitives. The TypeScript port moves the same way (values/, ingest/, lib/, diagnostics/, validator/); its published subpath exports are unaffected except ./ig-docs, below. C# is untouched -- Values.cs became Values/, and the namespace is still TypedFhir.

    Consumer action, and it divides in two. Four of the five re-export everything they hold from the top level, which did not move: from typed_fhir import FuzzyDatetime, normalize_quantity, SYSTEMS, partition_everything works exactly as it did, and the top-level import is the supported surface. Only a SUBMODULE import breaks, and it breaks loudly -- ModuleNotFoundError at import time, not a silent behaviour change. The exception is facade_diagnostics: to_issue_code and facade_to_issues have no top-level alias and must be reached at typed_fhir.diagnostics.facade. Grep for the five old paths.

  • BREAKING (TypedFhir TypeScript): the @awaremd/typed-fhir/ig-docs subpath export is removed. It was published as a package entry point in 1.14.0, when the renderer behind it became how this site's FHIR reference pages are built. It is a GENERATOR rather than a runtime API: it reads the compiled IG off disk, is Node-only, and its output is this specification's own pages. It now lives with the other generators, at codegen/ig-docs/, and is not published. Consumer action: if you import it, pin 0.14.0 -- it is not returning as a supported entry point, and a generator that reads our build tree was never a contract we could keep. The remaining subpaths are unchanged: ., ./ingest, ./ingest/ndjson and ./build-diagnostics.
  • BREAKING (behaviour): the compartment checker no longer calls a resource malformed for having no identifier, unless the IG says it must have one. Rule A6 raised an error for every resource type without a Nexus EMR identifier, on the strength of the guide sentence "Every resource stored in MedPlum must have a unique identifier with this canonical URL". Measured against the profiles, exactly one type requires one -- List, via nexus-template-list (identifier 1..*). Every other profiled type declares identifier as 0..* or 0..1, and 1.19.0 deliberately relaxed Task.identifier and Composition.identifier precisely so that populations which cannot carry one -- a human-created Task, an unidentified Composition -- are representable. The checker went on reporting those as errors, which is the checker disagreeing with the specification rather than enforcing it.

    Severity is now taken from the IG: error where a profile requires an identifier, warning where it permits absence, with the message saying which. Consumer action: if you gate on validateCompartment / validate_compartment returning no ERRORS, resources you previously had to special-case will now pass. If you gate on the issue LIST being empty, a warning will still stop you -- filter by severity. Rule B1 is unchanged: an identifier that IS present must still carry the Organization's product-instance, and a mismatch is still an error.

    All three ports agree on this as of 1.20.0. The C# checker was left on the old hardcoded verdict when TS and Python moved, so the same Task validated clean in two ports and hard-errored in the third; it now takes severity from a generated table derived from the same profiles the other two read at runtime. C# also now reads a 0..1 identifier -- a single object rather than a list, which is its cardinality on QuestionnaireResponse, Composition and Bundle -- where it previously saw such a resource as carrying no identifier at all and reported A6 against it.

    Still owed, and stated rather than quietly resolved: the identifiers guide page still asserts the blanket requirement, and says it "is specified on each Nexus EMR resource profile as an Invariant, so is enforced when we do validation". No such invariant exists on any profile. What would settle it is a decision on whether the requirement is real -- in which case the profiles should carry it -- or aspirational, in which case the page should say so.

  • BREAKING (TypedFhir C#): EffectiveDate is a FuzzyDate, not a string?. Every profile's synthesized effective date, ClinicalResource.EffectiveDate, Metadata.EffectiveDateOf and the date column of ToRows() all move. TS and Python have returned the typed value since the accessor was introduced; C# was left on the raw string across all 45 profiles under a comment saying "C# has no type alias -- the string IS a FuzzyDatetime", which stopped being true in this release, because this release ADDS Values/FuzzyDate.cs. The port was internally inconsistent as well: its 32 per-field date accessors were already typed with it. Consumer action: mostly none -- FuzzyDate converts implicitly to and from string, so assignment, interpolation and comparison keep working. Code that names the type (string? d = x.EffectiveDate; is fine; a List<string> built from a LINQ projection is not) needs ?.Value. In exchange the C# accessor answers .Precision, .Year, .Overlaps and the rest, which is what the other two ports have been able to do all along. Python's effective_date_of return annotation says FuzzyDate | None now too -- it always returned one at runtime, FuzzyDate being a str subclass there.
  • BREAKING (TypedFhir Python): field_names() reports only the accessors you can SET. It listed every property, including derived read-only ones (display, effective_date, fingerprint, raw, type_, and per-profile readers like value_text). That list is printed verbatim by new() as the answer to "what can I set", so a caller who read it and passed one back got a bare AttributeError: can't set attribute contradicting the list they had just been given. new() now names a read-only field and says to set the element it derives from. Consumer action: code enumerating field_names() to discover accessors for READING should read the class instead; the method answers a write-shaped question and now says so.
  • BREAKING (TypedFhir Python): four ways an idempotent write was not idempotent, or not safe. All four are in the client's write path and all four are silent in the direction that costs you data, so none of them announced itself.

    The request timeout was disabled rather than inherited. timeout=self._timeout was passed to httpx unconditionally and the docstring claimed None "leaves it to the transport". httpx reads an explicit timeout=None as NO TIMEOUT -- inheriting is what its USE_CLIENT_DEFAULT sentinel is for. So the DEFAULT configuration stripped the transport's timeout from every request, and a server that accepted a connection and then went quiet hung the caller forever. The kwarg is now sent only when a timeout was actually asked for. Consumer action: if you relied on requests never timing out, set it explicitly.

    Search values were never escaped. , | $ and \ are FHIR search operators -- OR, token separator, composite separator, escape -- so a literal containing one changed the query instead of being matched by it. A business key with a comma in it asked for "this OR that" and got the wrong resource back, silently, which for save_by_business_key means updating a resource that was never yours. Escaping is a named helper rather than something applied to every parameter, because those separators are how composite values are SPELLED: blanket-escaping would break identifier=system|value for every caller composing one.

    Composition.identifier is 0..1 and the client wrote a list. Almost every resource is 0..*, so the list was written unconditionally; Composition and QuestionnaireResponse are the two profiled types where that produces a non-conformant resource.

  • BREAKING (behaviour): a leading # no longer makes free text a coding token. codeableToTokens({text: "#1 Diabetes mellitus, type 2"}) round-tripped to {coding: [{code: "1 Diabetes mellitus, type 2"}]} -- the text lost and a code invented -- because the empty-alias form (#active, a coding with no system, which FHIR permits and source systems send) was recognised from the leading # alone. #1 ... is a live problem-list convention. The systemless form now also requires a code-shaped remainder: non-empty, no whitespace. Consumer action: a systemless code containing a space is now carried as text rather than as a code. That is the deliberate direction of the trade -- losing a code is recoverable from the text beside it, and a fabricated problem-list code is not.
  • TypedFhir: contain() deep-copies the resource it carries. Its contract has always been that the caller's resource is copied rather than adopted -- containment assigns a local id, and rewriting an identity the caller still holds is not something this layer may do. The copy was one level, which protected the top-level id and left every nested object (code, coding, note, extension) shared between the caller's resource and the contained copy, so a later edit to either was an invisible edit to the other. Consumer action: none for correct code, but if you relied on mutating a resource AFTER containing it and seeing the change inside the container, that never worked in the direction you think and now does not work at all -- contain it after it is complete, or edit contained directly.
  • FuzzyDate's part accessors no longer throw on an unparseable value. The type documents that a read never throws, because a server can send a dateTime the grammar does not cover and a getter that threw would make a whole chart unreadable over one field. The part getters did not hold to it: FuzzyDate("unknown").year raised ValueError in Python, threw FormatException in C# and returned NaN in TypeScript. All three now return the absent value. A partly unreadable value still reports the parts it plainly has -- "2024-XX-15" is year 2024, no month, day 15.
  • An Annotation now carries the element it was decoded from, so a note thread round-trips whole. The type lifts text, author and time, and re-emitted only those -- so task.note = task.note.append(...), which assigns the whole element back, stripped id, extension and anything else from every turn already on the thread. Nothing changes for an annotation this library built; a decoded one keeps what it arrived with.
  • Reference's external and display-only forms have a wire form in every port. In Python both emitted the literal string "None#None" -- silently, from the pydantic serializer, to_concise and encode_value, for any chart holding an absolute-URL reference or a display-only participant -- and neither decoded. In C# the display-only form emitted { display } alone, which no port could read back as a Reference; it now emits the { identifier: "", display } object TypeScript has always emitted. And urn:uuid: / urn:oid: references -- how a transaction or document Bundle names its own entries -- are now recognised as absolute in all three ports, where the old :// test missed them and they decoded as display-only or were dropped.
  • TypedFhir (Python): subscription date matching is correct on Python 3.10 again, this package's declared floor. An offset written without a colon (+0500, which R4 permits) could not be parsed on 3.10 -- full ISO-8601 parsing arrived in 3.11 -- and the failure was silent: the value fell through to the raw-text comparison path, so an event carrying such an offset simply did not match a criteria window it should have. Silent non-delivery, indistinguishable from nothing having happened, on the exact code path that exists to prevent it. Offsets are now normalised before parsing, so both spellings of one instant are indistinguishable to a query on every supported interpreter.
  • Well.Services.TypedFhir.Firely rejoins the version line. It had drifted to 0.8.0 against a core of 0.14.0 -- six minor versions -- under a comment asserting it was versioned in step with the core. It caught up to 0.14.0 in this line of work and ships 0.15.0 with the other three. The adapter converts Firely R4 POCOs into the DOM the core consumes, so the two are a matched pair by construction; a consumer taking 0.8.0 had no way to see the core had moved eight releases on. The Hl7.Fhir.R4 dependency is unchanged and stays pinned to match Well.Services.FhirR4, deliberately. All four published packages now carry one version and a gate holds them there.
  • TypedFhir's generator entry points collapse to one: npm run gen:all. The nine per-step scripts are removed from the published package.json -- gen:ig, gen:facade, gen:facade-py, gen:facade-cs, gen:ig-docs, gen:subscriptions, gen:patient-compartment, gen:facade-parity and gen:cache-parity. Each read the compiled IG and trusted that something else had compiled it correctly, which held only while a vendored copy of the IG was committed -- and this release deletes that copy. gen:all stages, compiles and generates in the one order that works, and now recompiles when the tree it would otherwise reuse has been rewritten underneath it: the IG Publisher re-runs SUSHI in the same directory without --snapshot, which strips the snapshot from every StructureDefinition and leaves the generators without the effective element view they read. Consumer action: none, unless you invoked one of the nine; gen:all replaces all of them and is cheap to run repeatedly.
  • Consumers pinning a floor: the NexusHarness POC's build-time requirement is typed-fhir[sources]>=0.12,<1.0, raised from >=0.4. Its runtime guard has refused anything below 0.12 since that line deleted public types from typed_fhir.sources._base; the build-time pin had not moved with it, so a resolver could select 0.4-0.11, the build would succeed, and the service would then refuse to start. The two halves now agree and a test holds them together.

Removed

  • Task.relevantHistory is no longer profiled. It carried a Must Support flag and a "MERT requirement" note, and what that bought was a hand-maintained list of back-pointers to Provenance resources that a consumer can already find by searching Provenance for a Task target. The distinction it forced -- notes are what people SAID, relevantHistory is what HAPPENED -- is a real one and is kept, in the prose beside note where it belongs. This does not forbid the element. Base R4 still defines it, this IG's own examples still carry it, and a consumer writing it stays conformant; what is gone is the obligation to maintain the list. Consumer action: none, unless you were populating it only to satisfy Must Support.

Fixed

  • ValueSet/nexus-emr-note-classification resolves again, which is what 1.19.0 said it would. That release retired the set in favour of nexus-emr-composition-classification and told readers the page "stays published with status: retired, so an old link resolves to an artifact that names its successor instead of returning 404". The rename had been done by deleting the file, so there was no artifact to resolve to and the URL returned nothing -- which reads as a broken site rather than as a deliberate replacement, the one outcome retiring an artifact exists to avoid. The retired ValueSet is published in this release: status: retired, naming its successor, carrying the single code (LOINC 34109-9) it held at the moment of the rename. Its content is FROZEN, not maintained -- duplicating the replacement's list would create a second definition that can drift from the first, and frozen and empty are different things. No profile binds it. Consumer action: none if you have already rebound. The stub will be dropped once no published version of this IG still references the old URL.
  • The Composition profile's dating guidance still called date its effective date, which stopped being true at 1.17.0. That release changed the declaration to event.period.start | date -- the clinical date first, the editing time only as a fallback -- and three published places were never updated to match: the FSH comment sitting directly above the declaration, the date element definition (which compiles into the StructureDefinition and into every TypedFhir port's generated documentation), and the profile page's "Dating a note" section, which described date as the effective date and did not mention event at all. All three now state what the profile declares, and the page leads with the clinical date instead of the editing time. No constraint changed and no consumer action is required; this is what the profile has meant for three releases.
  • All five Composition examples now carry event. The profile tells readers to sort a clinical timeline by event.period.start rather than by date, and then demonstrated that nowhere: every published example was dated only by date, including the amended note whose entire purpose is to show that date moves to the amendment while the clinical date does not. The examples now cover the three shapes that matter -- a point in time carrying the encounter it documents in event.detail, an interval with end for a summary spanning two visits, and an amended note whose event.period.start stays on the telephone call while date moves 85 minutes forward.
  • Both OSCAR note converters wrote the clinical date into date and emitted no event, so every converted note was undatable on a clinical timeline. typed-fhir's OscarRestSource mapped OSCAR's observationDate -- the date of the CARE, which a user sets and which back-dates to the visit -- straight onto Composition.date, the editing time, and OscarFhirGatewaySource carried DocumentReference.date across while dropping context.period on the floor. Both now populate event.period, and date carries the editing time it is defined as (updateDate for the REST source, still DocumentReference.date for the gateway). event appears only where the source states a clinical date; nothing is invented.

    This is visible to consumers. Composition.date on a REST-converted note now reports updateDate where it previously reported observationDate, and effective_date_of -- which has walked event.period.start | date since 1.17.0 -- now finds the first arm rather than falling through to the second. Concretely, last_encounter_note_date returns the visit on a converted chart where it used to return whenever the note was last edited. Consumer action: if you were reading Composition.date from these two sources as the clinical date, read event.period.start (or effective_date_of) instead; that is what it now is.

[1.19.0] - 2026-08-21

Generated by TypedFhir 0.14.0. Bumped from 0.12.0 (which generated 1.18.0) -- 0.13.0 is skipped, not missing, matching this IG's own absent 1.13.0. This release brings the Python FHIR client into line with the frontend's fhir-client.ts and adds the surfaces the harness writes through (the typed 412, the business-key write, vread/history, the attachment client), gives PatientCache the client-bound half the frontend's stores have, adds the facade constructor (new() / missing_required()), curates subject/patient, and teaches Reference the logical and contained forms. All three manifests move together -- ts/package.json, python/pyproject.toml and dotnet/TypedFhir/TypedFhir.csproj.

Two accessor changes reach what writers type. Complex datatypes are curated -- Annotation, Period, Address, HumanName, ContactPoint, Attachment, Money, Dosage and Identifier gain structured accessors across all three ports, 29 elements in all, so a Task.note turn is no longer hand-assembled as raw JSON on the harness's hot path and Patient.name/telecom/address are typed for the first time. And a Coding with a required binding now curates as a bare code (encounter.class_ = "AMB"), the same trade the CodeableConcept branch already makes: the accessor writes the system from the value set, so a caller cannot mistype or omit it. That reaches exactly one element -- Encounter.class is the IG's only top-level Coding with a required binding.

A contained reference now has a wire form, #proposed, where to_wire() used to raise. The base contract is that every value can re-emit its wire form and the generic encoders call it unconditionally, so a structure holding one -- which is what contain() hands back -- failed on the serialization path rather than where the reference was built. Spec §5.5 gains the form; it is unambiguous against Type#id by the absence of a type before the hash, and FHIR JSON is unchanged (the codec still leaves contained references verbatim).

Consumers pinning a floor: the NexusHarness POC's TYPED_FHIR_FLOOR moved to (0, 12). 0.12.0 deleted public types from typed_fhir.sources._base inside a 0.x bump, which broke the "every 0.x is rapid iteration" premise that guard was written on; a validated window is replacing the floor.

Versions here are sequential, not SemVer -- see the note at the top of this file. Read these three before consuming this release:

  • Two published vocabularies were renamed, and both breaks are in stored data. The suggestion code on Task.code is now suggestion, not ai-suggestion: a search for Task?code=...|ai-suggestion returns nothing after this release, and stored tasks carrying the old code fail nexus-sugg-2 until migrated. The note-classification value set is now nexus-emr-composition-classification: the old canonical is no longer bound by any profile, so rebind. Its page is NOT removed -- ValueSet-nexus-emr-note-classification.md is still published, status: retired, so an old bookmark still resolves and says what replaced it. Neither rename is recoverable by tolerating absence -- both need a migration.
  • Four elements that were required may now be absent, and readers must tolerate all four. Task.for, Task.identifier, Composition.identifier and NexusEmrCoreCareTeam.subject all relax in this release, each because the profile grew a population the requirement made unrepresentable. A reader that treats absence as malformed will reject valid records; one that lists a patient's care providers without filtering on subject will now show the nursing pool as a clinician. Going the other way, every Task.note turn now REQUIRES author and time, so a writer that emitted unattributed or undated turns must populate both.
  • A given seed does not reproduce the 1.18.0 dataset. The generator no longer opens a task's thread with a turn restating its description, and a requested follow-up task now emits no turns at all. Regenerate and re-baseline; 1.18.0 stays archived for byte-fidelity.

Changed

  • BREAKING: the suggestion code is suggestion, renamed from ai-suggestion (CodeSystem/nexus-emr-suggestion, used on Task.code). The old name asserted something the code does not carry. What made a suggestion "AI" was never this code — it is Task.requester, a logical Device reference under NamingSystem/nexus-harness-graph naming the graph that produced it. A deterministic CDS rule produces the same Task shape with a different requester, so the old name made every CDS recommendation read as machine-learning output.

    Consumer action: a search for Task?code=...|ai-suggestion returns nothing after this release; use ...|suggestion. Stored tasks carrying the old code do not match the nexus-sugg-2 invariant and must be migrated. Read Task.requester when the producer matters (AI graph vs CDS service) and Task.code when it does not. An earlier revision of the CodeSystem argued against this rename on the grounds that the code is a stored value — that traded a permanently misleading vocabulary for one migration, and while this specification is pre-release the trade was wrong.

  • BREAKING: every ClinicalTask.note turn now requires author and time. Each turn is typed to NexusEmrTaskNote, which makes required two elements that are optional in base R4. Consumer action: a writer that emitted unattributed or undated turns must populate both on every note, or the resource no longer validates.

    This entry used to lead with what did NOT change -- "typed to a profile and stays 0..*; the ask is description" -- and put the breaking half in the middle of the paragraph. That is worth correcting rather than quietly rewriting, because the entry's own shape caused the damage. This file's preamble states the contract: a breaking change is named as breaking, in bold, with the consumer action spelled out. A reader skimming bold headlines for what would break saw a reassurance about cardinality and moved on. The note-model fallout in this release traces back here.

    The cardinality does not change. An earlier revision of this release made note 1..* and declared note[0] the opening ask. That is withdrawn before it ships, and the reasoning is worth recording because it was wrong in a specific way: it treated description as a mere label when description is required on this profile and IS the ask — what is wanted and why the task exists, stable for the life of the task. Requiring a turn on top of it bought nothing and cost every writer a duplicated one, storing the same statement twice and opening the thread with something nobody actually said. note is what has been said since the task was created; a task nobody has commented on carries none, which is the ordinary state of new work.

    Consumer action: read description to answer "what is this task about" and note to answer "what has been said about it". Render an empty thread as new work, not as a malformed record.

  • BREAKING (seed data): a given seed no longer reproduces the 1.18.0 dataset. Two generator changes alter what a thread contains, and both follow from the Task.note model above rather than from any new decision: the generator no longer opens a task's thread with a turn that merely restates its description, and a requested follow-up task now emits no turns at all. A task nobody has commented on carries no notes, which is the ordinary state of new work.

    Consumer action: regenerate and re-baseline anything pinned to generated output -- fixtures, golden files, benchmark stores, screenshots. The change is in the CONTENT for a given seed, not in the generator's determinism: same seed still means same dataset, and test:seed-stability in the generator suite is what holds that. 1.18.0 stays archived for byte-fidelity if you need to diff against the old shape.

    This was listed among the three things to read before consuming 1.19.0 but had no entry of its own down here, so the detail a reader needed in order to act on it -- which two behaviours changed, and that determinism itself is intact -- existed only as a one-line summary.

  • TypedFhir (Python) gains a FHIR REST client (Jira PSD-1141, "Writeback of compositions" — not to be confused with GitHub PR #1141, which is the 1.18.0 release this branch is stacked on; the numbers collide by coincidence). typed_fhir.sources.fhir_client.FhirClient ports the frontend's fhir-client.ts API to Python — same operations, snake_case names, same error contract (a failed search returns a Bundle wrapping the OperationOutcome per R4; a failed direct request raises it). This is what makes generated-summary writeback possible: create_resource / update_resource arrive as part of a client rather than as another hand-rolled POST.

    No third-party FHIR library, deliberately: they all own their own HTTP call, and this package's determinism depends on that not being owned (connectors build requests and parse responses with pure code; only bytes cross the Transport seam, so a run replays against the frozen vectors). Credentials here also resolve per request from context — the harness sets a delegated bearer and tenant per run — which a client that binds authorization at construction cannot express.

    Both read postures moved onto the client with their measured knowledge intact: the degrading compartment sweep (one broken type must not cost the chart) and the loud single-query load (a truncated inbox is missing work items, not a smaller page), plus next-link rebasing with all three of its hard-won exceptions, the HTTP-200-carrying-a-refusal check, and a page cap that is always reported. Public API unchanged: search_paged, next_link and rebase_next keep their names and import paths. PHI: error messages carry issue.code only — a HAPI OperationOutcome echoes the offending resource into diagnostics, so the text rides on the exception, never in a log-bound message.

  • BREAKING (TypedFhir Python): the FHIR client's API is brought in line with fhir-client.ts. Its docstring has always said it mirrors the frontend client; it was about 70% true, and the differences were spelling rather than substance. get_resource_by_link now takes (resource_type, url) like the reference (and like every other method here) rather than the reverse; update_resource takes a FhirUpdateOptions bag instead of if_match: str | bool; search_errors and wrap_operation_outcome_in_bundle are instance methods, not @staticmethod; and every search entry point accepts an ordered sequence of pairs, not only a mapping — a date window (date=ge…&date=lt…) was previously expressible in exactly one of the four.

    Consumer action: update those three call shapes. There were no consumers outside this package's own tests when the change landed, which is why it was made now rather than deprecated.

  • TypedFhir (Python) gains the write and read surfaces the harness needs, all mirroring the frontend: FhirPreconditionFailedError (a 412 is the one write outcome a caller must handle differently — nothing was written and retrying unchanged will be refused again — and it was previously only recoverable by sniffing a status code); save_resource, routing create-vs-update on id presence; bundle_entries / bundle_entries_or_raise / has_error_issues, mirroring BundleUtils, where the _or_raise form puts the HTTP-200 refusal check in the same call as the entry filter so it is no longer one a caller can forget; get_version / get_history_page / get_history; and FhirDocumentDataClient for attachment bytes, which streams and enforces its size cap while downloading rather than after.

    save_by_business_key is the AgentWriteFhir contract in one call — search by identifier, then update in place or create — so re-running a capability updates the same Task or Composition instead of piling up a rival. It writes the key onto the resource (a key you searched by and did not store finds nothing next time), carries the stored meta.versionId forward so the read-then-write stays conditional, and refuses when a key matches more than one resource rather than picking a side.

    FhirClient(base_url) is now the whole call: the transport defaults, and credentials default to the delegated bearer and tenant in the ambient context.

  • TypedFhir (Python): PatientCache can drive a client, not only be handed a list of resources — search / load_all / count / load / get_or_load / load_by_link / load_version / load_history / save / save_as_global / delete_by_id / upsert / evict / clear, plus on_write / on_read / on_delete listeners. This is the frontend's FhirStoreBase half that had no analogue here. Two things the stores need that it does not: a store registry (one cache holds every type, so an _included resource lands beside the one that included it with no routing) and per-type store classes (the type is an argument and the local query is resources()). A search the server REFUSED raises rather than merging nothing — a tenant refusal arrives as HTTP 200 with an empty searchset, and merging that silently leaves a cache reporting an empty chart.
  • TypedFhir: starting a resource is a constructor call. Observation.new(status="final", …) and new_resource("Task", …) replace hand-writing {"resourceType": "Observation"} as the entry point to a typed API. Fields pass through the setters, so a code outside its required binding is refused at construction. What comes back is deliberately incomplete — you are still building it — and missing_required() answers "what does this still need", returning the IG path plus the accessor to set it with, read off the profile validator rather than the curated field table so it cannot report "nothing missing" about an element the facade does not happen to type.
  • TypedFhir: subject and patient are curated. No profile in the IG had a subject accessor — they sat on the generator's global skip list, a rule that only ever held for READING (inside a patient cache the subject is a given). It stopped holding when the facade gained a construction path: a new Observation must carry a subject, and with no accessor the only way to set one was raw JSON. 24 profiles gain a typed subject and 6 a typed patient, in all three ports. identifier stays uncurated — it is reached through save_by_business_key.
  • BREAKING (behaviour): Reference decodes the LOGICAL and CONTAINED forms. It handled only Type/id, so a reference by business identifier — with no resource on this server — read as None from the typed accessor and had to be written through .raw. That is the shape the AI stack always produces (Task.requester / Task.owner, Composition.author), so task.owner was None on every Task the harness had ever written. Spec §5.5 has specified the logical form all along; this is the implementation catching up with its own normative text. Reference.to(...), Reference.to_identifier(...) and Reference.to_contained(...) are the three constructors, in Python, TypeScript and C#.

    Consumer action: reference_from_fhir now returns a value where it previously returned None/undefined for identifier-based and #fragment references. Code branching on "no typed form" to reach for the _raw twin will now take the typed path — which is the point, but it is a behaviour change. Four shapes still stay raw deliberately, because no faithful typed form exists: an absolute URL (rebuilding it from Type/id would silently retarget it at this server), a display-only reference, an Identifier richer than {system, value}, and a non-resolvable relative reference.

  • TypedFhir ships a cross-verifier (backend/lib/TypedFhir/crossverify/). One scripted server, a catalogue of edge cases, and five FHIR clients answering the same questions: TypedFhir, fhirpy, smart-on-fhir/client-py, the frontend's own @awaremd/sc-fhir-client and bare SMART client-js underneath it. It is worth knowing about as a CONSUMER of this library, because it is the evidence behind claims this package otherwise just asserts -- that a paging link cannot walk your bearer token to another host, that a resourceType cannot rewrite a request path, that an HTTP 200 carrying a forbidden outcome is not reported as an empty chart. Each case states the question, why it matters, and how a verdict is decided; MATRIX.md is the output.

    Deliberately not in CI (it installs four third-party FHIR clients, and its answers change when other people release) -- it is step 15 of the owner-track pre-PR gate. Verdicts about the other libraries are recorded and never asserted on: the suite fails only when TypedFhir is the one behaving badly.

  • Known gap, stated rather than papered over: Composition publishes no searchParam. The generated-summary design needs three (subject, category, and identifier), and identifier is load-bearing — without it a producer cannot find the summary it wrote last time and update-in-place silently becomes duplicate-on-every-run. None of the three is published, and the guide says plainly that these are requirements, not guarantees. Measuring them against the store and declaring what works is owed.

    Be precise about what is and is not declared, because the two are easy to conflate. The CapabilityStatement DOES list the search-type interaction on Composition — as it does on all thirty resource types it covers — so a reader of the statement alone will conclude that searching Composition is supported. What it does not carry is a searchParam block naming which parameters work; today List is the only resource with one, and that block exists because the identifier parameter was measured against the NativeFHIR store on 2026-08-19 and its documentation records the measurement. The IG's rule — do not publish what has not been measured — therefore governs the PARAMETERS, not the blanket interaction list. An earlier revision of this entry said "nothing is declared here yet", which read as a claim about the whole statement and was contradicted by the statement's own #search-type.

  • BREAKING: nexus-emr-note-classification is replaced by nexus-emr-composition-classification, and the Composition profile now models three document kinds. The old value set carried its assumption in its name — it classified notes, because the profile modelled one thing — and the profile itself had already predicted the expiry date in prose: "that stops being true the moment a Composition is written for anything else — a generated summary, a letter, an assembled document." Both arrived. Letters shipped first and shipped off-vocabulary: PSD-1107 classifies them with a locally-minted http://services.well.com/fhir/categories/letter-template code, because the IG offered nothing usable. Generated summaries are next and are on the demo's first path.

    The set now carries three existing LOINC codes, no local code system minted (the IG's standing rule, and why the old set used 34109-9 rather than coining one): 34109-9 "Note", 34133-9 "Summary of episode note", 51852-2 "Letter". 34133-9 rather than IPS/PS-CA's 60591-5 "Patient summary Document", because that is a full patient-summary-for-exchange, a different artifact from "what changed since you last saw this patient". 51852-2 ratifies the type the app team was already writing.

    Consumer action: rebind to .../ValueSet/nexus-emr-composition-classification. The old canonical is no longer bound by any profile, but its page has NOT been removedValueSet-nexus-emr-note-classification.md is still published, now status: retired, keeping the one code it ever held so a reader arriving from an old link lands on an artifact that says what replaced it rather than on a 404. (An earlier revision of this entry said the page 404s. It was going to; retiring it instead was the better answer and is what shipped.)

    Populate category on every Composition — a surface that ignores it will render a machine-generated summary as though a clinician authored it, and author does not disambiguate (a scribe-drafted note also carries a Device author). The letter-template local category should migrate to 51852-2; coordinate with the letters work, since it is already in seed data.

    But do not write a reader that assumes category is present and in this set. The profile makes it 0..* with a preferred binding, so an instance that omits it entirely, or carries a code from somewhere else, is fully conformant and will not be flagged. That is deliberate — a source EMR's own classification should be neither rejected nor discarded — and the release ships a live example of it: the letters PSD-1107 already wrote are classified with a locally-minted letter-template code and sit in seed data today. So branch on the LOINC codes when they are there, and fall back to type and then to treating the document as an unclassified note when they are not; do not treat absence as malformed. Whether this element should instead be tightened to 1..1 and required once the letters migrate is undecided, and what would decide it is the migration: while an off-vocabulary code is still in stored data, tightening the binding would make existing conformant records invalid.

    A worked letter ships with this release: NexusEmrComposition-letter-example.json, a referral classified 51852-2 on both category and type, final with a legal attester, and carrying the letter itself as authored XHTML in section.text — letterhead, salutation, body and signature block are all narrative, exactly as composed. It is the counterpart to the generated-summary example and shows the contrast the attester exemption turns on: nothing attests to a machine-generated summary, but a letter goes out over a clinician's name and is never exempt.

  • Generated summaries are modelled, with a guide topic. A summary is a Composition the harness writes and maintains: subject is always the patient, author is the graph as a logical Device reference, and the business key under nexus-harness-key is what makes it update in place — one since-last-visit summary per patient, forever, regenerated by replacing content rather than by minting a second resource. The key therefore carries no period; superseded content is the resource's own version history. Semantically distinct summaries (day-sheet, since-last-visit, whole-chart) share the one FHIR classification and are separated by the capability prefix in the key, which is the thing that actually differs between them. New: features/generated-summaries/ in the guide, and a worked example.
  • composition-signed-has-attester now exempts generated summaries, and the exemption is mutation-tested (a summary passes, an unsigned final note still fails, drafts still exempt). Nothing attests to a machine-generated summary: final means "this generation is complete", not "a human is responsible for this". The alternatives were worse — permanently preliminary is false, and naming an attester who never attested fabricates a medico-legal signature. Letters are not exempt. This is what widening a profile's population costs: the invariant encoded an assumption true of every document the profile held when it was written, and the assumption had to be made explicit rather than silently broken.
  • Swept the last places that still tied Communication to a Task. The ruling below is only useful if nothing contradicts it, so: the NexusEmrCoreCommunication profile now states outright that it is not the conversation on a Task (it is where a reader looks to check, and it was silent); and the AI-assisted-triage example was rebuilt onto the note model. That example carried a latent defect worth naming — its focus pointed at a Communication, which is not in the typed focus target list, and nothing caught it because the validator does not enforce reference targets (FHIR-45). The patient's message is now a turn on the task (note[0], authored by the patient), which is what the one-rail model says it should be: there is no separate chart resource to point at, so the task carries no focus. The ask itself is the task's description.
  • RULED: a task's conversation is Task.note, and Communication is not used for task turns. Both were on the table and both were partly documented, which is worse than either. The turns are Annotation repeats on the Task, oldest first, and are what has been said since the task was created — the ask itself stays in description, which this profile requires. Rationale recorded on the profile: a turn is not independently meaningful outside its task; "say something and hand it over" is one atomic write rather than two needing a transaction Bundle; and a single ordered repeating element needs no threading model, where Communication's inResponseTo-vs-partOf question is explicitly unsettled. What this gives up is stated in the profile too — no per-turn security label, no per-turn Provenance, no structured attachment in a turn — with an additive graduation path to Communication if a consumer ever needs those. Guide correction: the design guide previously described task conversation as Communication resources with email-style Message-ID threading. That section is superseded and marked as such.
  • Guide correction — Task.intent on a suggestion is #proposal, not #order. The design guide still asserted that intent "stays #order throughout" with proposal semantics living only on the payload. That has been wrong since 1.16.0, when intent gained its required two-code binding and became the discriminator between work to do (#order) and machine output to decide (#proposal). Corrected, with the consumer consequence spelled out: a reader that ignores intent presents unapproved machine output as though a human had assigned it.
  • Task.owner accepts a CareTeam: a standing team can hold pooled work (PSD-949, adopted from PR #1148). owner stays 1..1 — the one-owner rule is about the CARDINALITY, not the target type, and owner is still exactly one reference when it names the nursing pool. The mechanism this replaces was groupIdentifier naming the work stream while some individual was named as holder, which could not say the true thing: groupIdentifier names the stream, not the holder, so a person had to be picked in advance to stand in for a queue nobody had picked up. A team owner says who actually has it, and a member's claim becomes a real ownership transfer with real Provenance instead of a status change on a task that always named one nurse. groupIdentifier keeps its own job (the batch or requisition a task belongs to) and is not the ownership mechanism. Organization remains a non-target: a task waiting on a lab is owned by the human or team waiting for it.

    This reverses a correction made earlier in this same release, which said CareTeam and Organization were both barred and that "a team cannot be accountable; a person can". The half about Organization stands. The half about teams did not survive contact with the chart-task work, and the reasoning was wrong in a way worth recording: it attributed to the target type an accountability guarantee that comes from the cardinality.

    Consumer action: a surface rendering owner must handle four target types (Practitioner, PractitionerRole, CareTeam, logical Device); one that assumes a person will mis-render pooled and agent-held work. Note the asymmetry with the conversation — a task can be OWNED by a CareTeam or PractitionerRole, but neither can AUTHOR a turn (R4 permits only Practitioner | Patient | RelatedPerson | Organization on Annotation.author), so a writer reusing the owner reference as the note author produces an invalid note.

    NexusEmrCoreCareTeam.subject relaxes 1..10..1 to make this usable, which is a change to a second profile and is called out separately here because it is easy to miss. The teams that hold pooled work — the nursing pool, the front desk, the referrals desk — are standing teams that serve the whole practice and have no single patient; leaving subject required would have made the new owner target unusable for exactly the population it was widened for. Two populations now live in that profile and subject is the discriminator: a team WITH a subject is that patient's care team (it backs the chart's Care Providers section), a team WITHOUT one is an operational work stream. A query listing a patient's care providers must now filter on subject rather than assume it, or it will show the nursing pool as one of the patient's clinicians.

  • Task.for relaxes 1..10..1, and Task.identifier 1..*0..* (PSD-949, adopted from PR #1148). BREAKING for readers: both may now be absent. for was required "for clinical safety", and that is right for the population it was written for — a clinical task whose patient link is missing IS a safety problem, and every chart-created task carries one. But the profile also holds work that is genuinely about no patient (restock the fridge, complete the cold-chain log), and a required for does not make those safer, it makes them unrepresentable, which pushes them into some system the EMR cannot see. identifier fails the same way: a task born in the EMR has no external record to be identified against, the server-assigned id already answers "which task is this", and requiring a business identifier forces a writer to mint a meaningless one — which is worse than none, because it looks like a key into something.

    Consumer action: treat a task with no for as a standalone work item, not a malformed record, and never infer the patient from focus. Tolerate tasks with no identifier at all. The expectation is now stated rather than enforced: chart-born tasks always set for, and a task corresponding to a record in another system should still carry that system's identifier.

  • Composition.identifier relaxes 1..10..1. BREAKING for readers: it may now be absent. It was required when this profile modelled ONE document kind — the encounter note, the unit source EMRs key on and the gateway must map onto (OSCAR casemgmt_note), where an unidentified note cannot be reconciled across a sync boundary. That case is unchanged and is still the reason to populate it. What changed is the POPULATION: this release widens the profile to three document kinds, and a document born here with no external record to reconcile against has no identifier to carry. The server-assigned id already answers "which document is this", and forcing a writer to mint a business identifier produces a meaningless value that looks like a key into something — the identical failure Task.identifier was relaxed for above. Letters had already shipped in exactly that shape, off-profile, because the requirement could not be met honestly; this is the profile catching up with what the widening had already made true.

    The obligation moves onto the element, stated per population rather than enforced profile-wide: a document synced from or reconciled to a source system SHOULD carry that system's identifier; a generated summary MUST carry its nexus-harness-key business key, or regeneration mints a second resource instead of updating in place; a document answering to neither MAY omit it. identifier.system and identifier.value remain required when an identifier is present, and there is still exactly one slot — a prose invitation to carry a source 'raw code' as an additional identifier was wrong for a singular element and has been removed.

    Consumer action: tolerate a Composition with no identifier, and do not use its presence to tell document kinds apart — category does that. A letter or summary that will be revised should still mint one: a document with no identity cannot be updated in place, only duplicated.

  • Task.businessStatus is bound (extensible) to a published vocabulary, and the vocabulary is documented for what it actually means (PSD-949, adopted from PR #1148). New CodeSystem/task-business-status (ok, needs-review) and ValueSet/task-business-statuses, consolidating under the IG canonical base the codes previously carried on the legacy http://well.company/task-business-status system.

    needs-review ships DEPRECATED, and businessStatus is reserved for what it is actually for. Settled 2026-08-19 with the chart-task author. The element is for CUSTOM LIFECYCLE SUB-STATES of the task itself -- a clinic that needs a nurse to pre-process incoming reports before a physician sees them transitions through its own pre-review before the task becomes active in the physician's inbox. needs-review is not that. It is a flag on the chart element the task is FILED AGAINST, and the obvious reading is wrong in a way that makes the code look redundant -- the existence of a task already means work is needed.

    The workflow it was built for is an Ocean referral that was COMPLETED, then received a new message from the referring physician, and had to resurface. That is now modelled as a new Task on the same ServiceRequest, because once a task is completed it does not come back to life -- the same semantics as a user creating a task on a document for a colleague, only arriving from outside. Inbound data carrying the old code is transformed at the C3 gateway.

    The code is RETAINED and marked status = deprecated rather than removed: roughly two years of stored data carry it and it must stay representable. Consumer action: do not write it. Do not confuse it with the AI/CDS suggestion review, which is intent = #proposal with the decision on Task.status. ok carries no information an absent businessStatus does not; it exists so a writer can clear the flag in place, and readers should treat the two as the same fact.

  • Guide correction — external systems as task owners. The guide showed owner → Organization for external-system handoffs. Organization is not a permitted target: a task waiting on a lab is owned by the human or team waiting for it. Also corrected: a lab-review workflow step that added its comment "via NexusEmrCommunication".

Added

  • New profile NexusEmrTaskNote — the IG's first datatype profile, on Annotation. A task turn carries enough load-bearing rules to deserve a name of its own rather than prose on the Task profile. It requires author and time on every turn (both optional in base R4: an unattributed or undated turn is a statement in the record with nobody behind it, and turns are read as an ordered thread), and it is where machine authorship is defined and enforced.

    PractitionerRole cannot author a turn. Annotation.author in R4 permits Practitioner, Patient, RelatedPerson, and Organization only — not PractitionerRole, even though Task.owner does permit it. A task can therefore be owned by a role while every turn on it is authored by a person, and a writer reusing the same reference in both places produces an invalid note. R5 adds PractitionerRole; R4 does not. This is not hypothetical: it was caught by the reference-target test against the synthetic corpus, which had 80 role-authored turns.

  • Agent-authored note turns are now specified and enforced: nexus-note-agent-author. An AI-harness graph can author a task turn, but Annotation.author[x] in R4 permits only Practitioner | Patient | RelatedPerson | Organization or a string — Device is not an allowed target, and a profile cannot add one, because profiling constrains and never widens (SUSHI refuses the attempt outright). So an agent author is written as an identifier-only logical reference: author.identifier under NamingSystem/nexus-harness-graph naming the graph, author.display carrying a readable name, and no author.reference. Reference.type is omitted deliberately rather than set to Device, which would assert a target the element does not permit; the identifier's system carries the identity instead.

    Chosen over an extension because an extension hides the author from every generic consumer, which is the wrong trade for a partner-facing spec: an identifier-only reference with a display degrades gracefully — a reader that has never seen this IG still gets a named author. The new error-severity invariant (on the Task Note profile) enforces exactly that shape and is silent on human authors, on authorString, and on tasks with no notes. Verified by mutation: a literal reference fails, a missing display fails, and all three benign cases pass.

  • The Guide gains a "Tasks, the inbox, and the AI harness" section, because implementers arrive at this model from three directions and it only makes sense as a unit: one rail for human work, agent-created proposals, and agent conversation; the inbox as a view over tasks rather than a second store; assignment-to-agent as the resume signal; gap-keyed idempotency; and a ranked list of what a consuming surface must get right. Agent identity is also now tabulated across the three positions it appears in (requester, owner, note.author) with the note.author exception and its R4 cause called out, plus why identity is the graph rather than the model, and why no Device resource is registered.
  • Machine-written tasks get an idempotency contract: NamingSystem/nexus-harness-key. ClinicalTask.identifier is "any system" and (as of this release) not even required, which left the one population that genuinely needs a named system without one: a task the AI harness or a decision-support service writes on its own behalf. Nothing stopped a rule that runs nightly from filing the same proposal every night. The new system carries a business key whose value is derived from the gap the task addresses, never from the run that produced it -- a key containing a run id, thread id or execution timestamp changes on every execution and defeats the purpose it exists for. Patient-scoped keys must include the subject, since two patients can present the same gap; values are opaque and must not be parsed.

    For implementers: a producer searches Task?identifier=https://fhir.apps.health/NamingSystem/nexus-harness-key|{key} before writing, updates the task it finds, and resolves that task when the gap no longer holds -- so a gap that closes on its own closes its own task. The same key travels onto the first-class resource when a suggestion is accepted, and that shared value is what makes traceback work across the promotion, since the accepted resource cannot basedOn the Task. Human-created tasks are unaffected — no system is mandated for them and no identifier is required at all; identifier is deliberately NOT sliced, so adding the key is additive for every existing writer and breaks nothing that ships today. Sibling of NamingSystem/nexus-harness-graph, which names the actor rather than the work.

Deprecated

  • ValueSet/nexus-emr-note-classification is retired, superseded by ValueSet/nexus-emr-composition-classification (see the entry under Changed for why the classification widened). Retired rather than deleted: the page stays published with status: retired and keeps the single code it held, so an old link resolves to an artifact that names its successor instead of returning 404. No profile binds it any longer. Rebind before the next release; a retired artifact is a redirect with an expiry, not a supported one.
  • task-business-status#needs-review ships deprecated on arrival, in the same release that publishes the CodeSystem holding it. It is documented under Changed above; it is repeated here because a consumer scanning for what not to build on should not have to read a paragraph about Task.businessStatus bindings to find it. Use a new Task on the same ServiceRequest for the resurfacing workflow it was built for. ok is unaffected.

[1.18.0] - 2026-08-19

Generated by TypedFhir 0.12.0. Bumped from 0.11.0 (which generated 1.17.0): this release adds the Python source-loader features (harness inboxes, the day-sheet cut, since= windowing and the last-encounter anchor), the typed_fhir.subscriptions package, the Template profile on List with its facades and armed invariants, and the three-port effective-date walker fix. All three manifests move together -- ts/package.json, python/pyproject.toml and dotnet/TypedFhir/TypedFhir.csproj.

Versions here are sequential, not SemVer -- see the note at the top of this file. Read these two before consuming this release:

  • Effective dates CHANGE for Compositions that carry an event. The walker fix below means event.period.start finally wins over date, as 1.17.0 declared it should -- so windowing, recency sorts and anything cached off an effective date will move for exactly the notes where the two dates differ. That is the fix landing, not a regression; recompute rather than reconcile.
  • A given seed does not reproduce the 1.17.0 dataset. The generator emits the letter template and the clinic-site Location, and profile identities moved. Regenerate and re-baseline; counts and shapes are stable, individual values and ids are not. 1.17.0 remains published and archived if byte-fidelity is needed.

Added

  • The Template profile's canonical is nexus-template-list, renamed before first publication -- and the reason is worth every implementer's attention. The HL7 validator (and the publisher convention it follows) IGNORES any file whose name ends in -template.json, reading it as an authoring template rather than a resource. Measured with byte-identical files: StructureDefinition-nexus-template.json is "ignored due to error: Unsupported format"; any other name loads. Under the original id, every consumer running validator_cli against this IG's package silently lost the profile -- including this IG's own conformance gate, which had been validating the template example against base R4 only. No released canonical moved: nothing template-related shipped before this release, and the NamingSystem canonical (NamingSystem/nexus-template) is a URL, not a filename, and is unchanged.

    With the profile actually loading, its invariants grew teeth AND a new one: nexus-template-entries-resolve makes a typo'd entry reference (#lettr for #letter) a validation ERROR rather than a template that renders nothing. It uses full FHIRPath (substring / in / %resource), which TypedFhir's runtime subset cannot yet verify -- it is enforced by the HL7 validator and any conformant external validator, and the runtime gap is tracked as FHIR-58.

  • Templates are served, validated, and discoverable: List joins the declared surface (FHIR-53, FHIR-52). Three things move together, because the profile's own gate said they must:

    • Declared. List enters the server CapabilityStatement (read, search-type, create, update -- no delete, matching the charting permission grant exactly) with the standard clinical-identifier search parameter, and the Template profile carries SmartExposedNoDelete(list). The gate that had held this back asked for measurement first, and got it: the discovery query List?identifier=https://fhir.apps.health/NamingSystem/nexus-template| was run against the live store -- served, and genuinely filtered (a bogus identifier returns zero, not an unfiltered list).
    • Validated. The Template profile's two error invariants were DEAD CODE: the facade set admitted only nexus-emr-core-* files, so no List validator existed and a nonconformant template passed every gate with an information-level "no profile" note. The facade set now admits the template profile by name; List facades generate in all three ports, and a template whose entry references a stored resource (or nothing) fails validation with the invariant's own message. Facade modules 43 -> 44.
    • Discoverable. The published NamingSystem and the instantiation recipe were corrected in the same release (the Bundle-era text and the missing identifier step -- see below).

    For implementers: list:read/list:write scopes now resolve, templates live in the patient compartment classification per R4 (CompartmentDefinition-patient lists List via subject/source), and destroying a template is deliberately not grantable.

  • typed_fhir.subscriptions: change notifications join the library (PSD-1039). A SubscriptionClient over a transport seam -- the Redis change stream (at-least-once, consumer groups, dead-letter stream) or a real FHIR Subscription rest-hook (FhirSubscriptionTransport

    • NotificationSink) -- with criteria parsed and validated against the subscriptions CapabilityStatement at construction, so an unsupported criteria raises before the loop starts and never registers as a subscription that quietly matches nothing.

    Hardened by four review rounds before this entry was written, and the last one matters to consumers: notification Bundles batching several events now deliver EVERY event (not just the first); a bare status ping is never delivered as a clinical event; on the rest-hook transport a failed handler dead-letters immediately (rest-hook cannot redeliver, so "retry by not acking" was a silent drop); criteria follow R4 escaping in full, + is a literal, and identifier matching requires the :identifier modifier (owner:identifier=system|value; keyword form owner__identifier=) -- the plain form is refused at construction because it matched over Redis and matched nothing on a conformant server. Date eq is containment (date=2026-08-14 does not match a resource dated 2026) and string search is case-insensitive-prefix, both per R4, so client-side and server-side evaluation of the same criteria string cannot diverge.

  • The reference loaders verify instead of trusting, measured against a live store (FHIR-52). The cross-patient searches refuse to present a security refusal as an empty result (the gateway answers a cross-tenant read with a 200 carrying an error OperationOutcome -- that raises now), refuse truncation at the page cap, and gate the inbox loader's client-side fallback on the exact status: 400 falls back, 404 raises, and a 403 -- both NativeFHIR's answer to the unsupported :identifier modifier AND the gateway's insufficient-scope answer -- is disambiguated with a one-request probe before any scan is licensed. load_daysheet treats the provider as two identities (Practitioner AND PractitionerRole) on both the appointment and schedule legs, and takes tz= (IANA zone) so a clinic's evening appointments survive UTC normalization. last_encounter_date reads category OR type against the IG's note codes, so OSCAR-converted charts (type-only, 11488-4) anchor correctly instead of answering "first visit" for every migrated patient; window comparisons normalize offsets first, so an Appointment.start anchor with a timezone offset windows correctly.
  • The effective-date walker descends arrays, in all three ports (FHIR-52). The declared precedence event.period.start | date (1.17.0) named the one ARRAY-VALUED path in the table, and no port's path walker could traverse an array -- so the declaration was inert at runtime and every consumer silently windowed on the authoring date, the exact wrong answer it was written to prevent. Found live: a note carrying both dates returned the wrong one. Fixed with an identical first-present-wins array-descent rule in TypeScript, Python and C#, pinned by tests in each.
  • load_harness_inboxes on the Python source connectors: every AI-harness actor's inbox in one call (FHIR-52). All Task resources owned by a harness graph -- an identifier under the nexus-harness-graph NamingSystem -- plus each Task's basedOn clinical targets and its for Patient, as one flat list of raw FHIR. Because harness actors are LOGICAL Device references (identifier-only, no Device resource on the server), the query is an owner:identifier search, and the connector VERIFIES the modifier was honoured rather than trusting it: a backend that ignored it would answer with every Task in the store, and presenting those as inboxes would be silently wrong, so an all-stray result refuses loudly instead.

    Only the WellServices FhirGateway connector implements it. The query is cross-patient, so it is not a compartment fetch; every other source raises NotImplementedError naming the connector that can. The returned resources can be wrapped with TypedFhir facades (typed_fhir.facade.generated.wrap) for typed access -- but note that nothing yet takes this FHIR and sorts it into per-actor inboxes for the AI harness; that consumer has not been written.

  • load_daysheet on the Python source connectors: one practitioner's day, appointment cut only (FHIR-52, SD-201). Appointments for the day, the practitioner's Schedules, and those Schedules' Slots inside the day — three standard R4 searches, paged, results VERIFIED against the practitioner and the day window rather than trusted (a backend that ignores a search parameter answers with everyone's appointments; an all-stray result refuses rather than presenting someone else's day). WellServices FhirGateway only, like the inbox load — the query is cross-patient.

    This supersedes the TS day-sheet read that 1.17.0 said had "moved to 1.18.0." That prototype tried to make one loader answer both "who is coming" and "what is their story", and hunted an AppointmentTask edge this IG forbids; it will not ship. The corrected model splits the two questions: load_daysheet answers WHO is coming, and opening a patient is a separate fetch_resources(..., since=...) anchored by last_encounter_date (below), per visit, when the chart is opened. Patient depth is deliberately absent from the day sheet.

  • since on Python patient loading: a history window over date-oriented elements (FHIR-52). fetch_resources / fetch_resources_lenient accept since=; a resource whose IG-declared effective date falls before it is dropped, and a resource whose profile declares NO effective date (Patient, Coverage, Device, ...) is always kept -- undated context is not history. The filter reads effective_date_of, the same event.period.start | date precedence every facade reads, so the window and the facade cannot disagree about when something happened. Precision is handled inclusively: a year-only date the anchor cannot place is kept, because a summary that silently dropped it would misinform where including it merely over-shares.

    Server-side date windowing was removed from this connector in an earlier release because no caller could supply the anchor. The anchor now exists (below); the filter stays CLIENT-SIDE, where one implementation serves every connector. Bandwidth is not saved; correctness is.

  • last_encounter_date: the anchor the "since you last saw" window opens from (FHIR-52, SD-189/SD-190). The clinical date of the patient's last SIGNED encounter note -- a Composition whose category carries LOINC 34109-9, dated by event.period.start | date -- strictly before a REQUIRED before anchor. before has no default, deliberately: the window's upper bound is the CURRENT visit (the appointment start at 36h pre-generation, the encounter start on chart load), and "now" is the one anchor that is always wrong during a visit -- the moment this visit's note is signed it would become "the last encounter" and the window would collapse to zero, precisely in the walk-in and chart-load cases the requirements call out (SD-189 03.05/03.06). Ties are excluded: a note dated exactly at the anchor is this visit's note. Returns None on a first visit -- a real answer, not a failure. Feed the result to since=.

    No TS or C# twins are owed for any of the three, and that is a statement, not an omission: both ports refused a sources layer by design (a host there already has a FHIR client and comes in through the in-memory ingest edge), so the fetch contract lives in Python, whose consumer -- the AI harness -- is the consumer of all three features. The primitive they lean on, effectiveDateOf, already exists in all three ports.

  • Templates are a first-class shape in the IG: the Template profile on List. A template is a List whose CONTAINED resources are prototypes of what it produces, cloned into ordinary resources when someone uses it. The first consumer is the clinical letter, which produces one Composition, so a one-entry template is the ordinary shape rather than a degenerate one; a template that later produces a letter and a follow-up task gains a prototype and an entry, and needs no new definitions. (FHIR-53)
  • A naming system for template identity, https://fhir.apps.health/NamingSystem/nexus-template. A template carries exactly one identifier under it -- both which template it is and the marker that it is one -- so templates are found with the standard List identifier search parameter. No custom SearchParameter, and no code system minted to mark a template. The identifier is identity, not classification: what a template produces stays stated by its prototypes, so there is no second copy of that fact to drift. (FHIR-53)
  • A Templates page in the handbook, a worked one-entry letter template example, and a letter template in the generated tenant corpus (clinic configuration, alongside the schedule and the EMR device).

Notes for implementers

  • A prototype is valid base R4; the instantiated resource is valid Nexus. This is the load-bearing rule and it is what keeps the clinical profiles unchanged. A prototype has no patient, and base R4 already permits Composition.subject to be absent -- so NexusEmrCoreComposition is not relaxed to 0..1, and no consumer of real letters has to carry a rule about templates. Conformance is checked where it matters, on the resource that reaches the chart.
  • Prototypes are contained, and two of the rules come free. R4 defines a contained resource as one with no independent existence, no independent identity and no transaction scope of its own, which is what a prototype is. dom-3 then requires every contained resource to be referenced from the container, so a prototype no entry points at is already invalid; dom-4 forbids meta.versionId and meta.lastUpdated on one, so a prototype cannot claim repository identity.
  • Do not write sentinel values into prototypes. Where base R4 requires a value (Composition.status, .type, .date, .author, .title), supply the template's own real one -- the date it was written, whoever wrote it -- and overwrite it at use time. A placeholder like 1900-01-01 validates cleanly and saves cleanly, which is exactly why it is dangerous: a letter dated 1900 is a defect no validator will report.
  • List, not a collection Bundle. The first draft of this profile was a Bundle, which expresses the same graph. Bundle derives directly from Resource rather than DomainResource (with Binary and Parameters, one of only three that do), so it has no extension, no text and no contained. A template needs a name, a lifecycle status and whatever stamp a deployment puts on stored resources, and a Bundle can carry none of them. List carries all three natively: List.title, List.status, and ordinary extensions.
  • Substitution is not specified yet, and instantiation is currently the client's job. For a one-resource template that is: read the List, take the contained resource its entry names, set subject / date / author, drop the contained id, and save it as an ordinary Composition. A declaration of the substitutions, and a server-side operation that applies it, are additive -- templates written against this release are their input.

[1.17.0] - 2026-08-16

Generated by TypedFhir 0.11.0. Bumped from 0.10.0 (which generated 1.16.0): this release teaches the FHIRPath subset ofType() so two IG rules start being checked at runtime, and folds extension invariants into the Python registry, which had been reporting a rule set two short of the IG's own. All three manifests move together -- ts/package.json, python/pyproject.toml and dotnet/TypedFhir/TypedFhir.csproj.

MINOR, and it contains breaking changes (Task.focus is typed, and a given seed no longer reproduces the 1.16.0 dataset). Under SemVer the first of those is MAJOR. This specification does not version by SemVer -- see the note at the top of this file -- because it is pre-release and still being remodelled, and 2.0.0 would claim a stability 1.x never had. The obligation the version number is not carrying is carried here instead: both breaks are named as breaks, in bold, with the consumer action, under Changed below. Read them; do not infer safety from the MINOR bump.

Consumer action for the seed break. If you pin generated seed data, regenerate it and re-baseline rather than trying to reconcile the diff: the values moved for four independent reasons (per-domain RNG streams, hashed resource ids, the lab-tier reconciliation, and the triangular normal-band sampler), so a value-by-value comparison against 1.16.0 will not tell you anything useful. Resource COUNTS and shapes are stable; individual values, ids and dates are not. If you need the old dataset byte-for-byte, stay on 1.16.0 -- it is still published and archived.

Scope on the ticket: the CapabilityStatements and the subscription criteria surface, the Task remodel FHIR-31 asked for, and the review findings and generator reconciliation those turned up.

⚠ The live site can be ahead of what has been reviewed. This specification is published before its PR is merged, deliberately, so readers are not blocked on review. The cost is that a version served from the site may still change before it lands: 1.17.0 was live while review moved Composition.type/.category from extensible to preferred and changed the declared effective date. If you pulled a package during that window you hold different bytes under the same version number. Splitting unreviewed releases onto a preview section of the site, leaving the main spec on the last merged release, is tracked as FHIR-54. Until then: check the PR before building against a freshly published version.

SD-201's day-sheet read is NOT in this release. It was built on this branch and moved to sd-201-daysheet-1-18-0 for 1.18.0. It is a read over the profiles, not a change to them, so holding it back costs the spec nothing, and it had open questions worth answering before it ships rather than after.

Changed

  • BREAKING -- Task.focus is typed, and focus vs input now mean different things (FHIR-31). focus was Reference(Resource), which documents nothing: a conformant writer could point it at an Organization and still pass. It is now Reference(QuestionnaireResponse | DocumentReference | Composition | Condition | DiagnosticReport | ServiceRequest | MedicationRequest | Observation), each constrained to this IG's profile of that type.

    Consumer action. If you write Tasks, focus must now name one of those eight and must satisfy the corresponding Nexus profile; anything else fails validation where it previously passed. If you read Tasks, you gain the thing that was missing: "which chart element is this task on" is answerable from the type alone, so a reader can switch on it rather than resolve the reference to find out what it is.

    The two elements were competing. input was ALSO an open reference to the chart, so the same fact had two homes and neither was wrong. That is why FHIR-31's question -- where do forms and inbox documents live -- had no answer. The split is now stated on the elements themselves: focus is what the task is about, input is what the task needs (chiefly the contained AI-suggested resource). Where a workflow genuinely spans several elements, focus names the one the task is filed against and the rest ride in input.

    Why forms are not stored inside the task. It reads as a storage question and is really a boundary question. A task carries the WORK -- who asked (requester), who owns it now (owner), what has been said (note), what happened to it (relevantHistory) -- and not the clinical content. If the form lived inside the task, every consumer that wanted the form would have to understand task workflow to reach it, the form would disappear when the task completed, and a document filed against two tasks would exist twice. So the form is a chart resource in its own right and the task points at it.

    note is documented as what it actually is: the conversation, oldest first. Annotation carries author and time as well as text, so the repeats ARE the thread that makes a forwarded task intelligible to whoever receives it. It stays deliberately distinct from relevantHistory, which is Provenance and records what HAPPENED rather than what was SAID -- an audit trail must not depend on someone having chosen to comment.

  • BREAKING for anyone pinning seed data: a given seed no longer produces the 1.16.0 dataset (PSD-417/PSD-412). The synthetic-patient generator was reconciled after PSD-417 was implemented twice, independently, against two different bases. Every change below reshuffles the random stream, so this is stated once, loudly, rather than discovered: regenerate any pinned fixtures. It is deliberately taken as one break in one release rather than three.

    Lab flags are now derived from the value, against clinical panic thresholds. Previously the flag was rolled and a value synthesized to fit, with "critical" derived from the reference SPAN -- so a sodium of 130 was labelled Critical low when the real panic bound is 120 or below, and a results viewer colouring criticals red cried wolf on it. Analytes now carry critLow/critHigh panic values, the flag is computed from the same bounds printed in Observation.referenceRange (so the range and the flag cannot disagree), and the tier distribution is chosen rather than inherited from each analyte's sampling spread. Measured on the new engine: about 16% of lab results abnormal and 0.8% critical, against a previous mix in which six of the eight analytes with panic bounds could only ever read Normal or Critical -- never a mildly high potassium, which is the most common abnormal a GP actually looks at.

    Review state and the review Task are now one decision. The nexus-emr-reviewed extension remains the authority for review STATE and the Task is the workflow that produces it, but the two were independent draws, which would have put completed Tasks on unstamped reports. They now move together. Review Tasks on imaging reports also stop being coded 182836005 "Review of laboratory test result".

    Not every resulted report is final, and the unverified ones now carry review Tasks too. The corpus previously emitted final reports and nothing else, so its inbox contained only settled work -- which had it backwards, since an unverified result is the one a clinician most needs to see. Reports now arrive preliminary (complete but unverified) and partial (some analytes back, the rest still running, and the report carries only the analytes that are), and both are reviewable. Observation.status agrees with its report by construction: an unverified report is not made of verified observations, and because partial has no Observation equivalent, the analytes that are back read preliminary. Unverified reports are deliberately less likely to be signed off, because reviewing a preliminary result often means deciding to wait for the confirmed one.

    Imaging is now an ordered flow, and the corpus contains no ImagingStudy. An imaging report gets a ServiceRequest order with a basedOn back-reference, can be pending or cancelled like a lab, and varies its impression and performing centre. DiagnosticReport.code for imaging becomes SNOMED rather than LOINC -- a preference call, not a conformance one, but visible to every consumer. The ImagingStudy profile stays in the IG and is excluded from the corpus with a stated reason: it carries series, instance and SOP identifiers only a PACS can mint, and an invented study UID asserts an acquisition that never happened. No canonical is removed.

  • The generator is additive again: per-domain random streams (PSD-412). Every emitter used to draw from one shared stream, and uid() consumed 30 values from it per resource, so ANY change moved every value downstream of it -- different medications, different appointment times, different ids on resources nobody touched. A one-line change produced a corpus-wide diff, which is why PSD-412's "same seed, same dataset, reviewable diff" kept failing on the third clause, and how PSD-417 came to be implemented twice without either author seeing the other's effect.

    Each domain now draws from its own sub-stream, addressed by NAME rather than by position, with the "does this patient get one at all" gate on a separate stream again. So adding a domain perturbs nothing, retuning a prevalence changes which patients have the thing without rewriting what it looks like for the ones that still do, and two people can add generators in separate commits without colliding. Resource ids are hashed from (chart, domain, counter) rather than drawn, which makes them unique by construction rather than by luck.

    What is NOT promised: isolation is per domain, not per resource. Adding a lab still moves later labs. test/seed-stability.test.ts pins all of this, and prints a per-resource-type fingerprint table so a reviewer can check that an imaging change moved only imaging.

Added

  • ofType() joins the emittable FHIRPath subset, and two IG rules start being enforced on real data (FHIR-33). value.ofType(CodeableConcept) IS valueCodeableConcept in FHIR JSON, so the type filter is a statically-known key rather than a runtime type test, and it compiles by collapsing two navigation steps into one -- exact, not an approximation.

    It was the only operator standing between the sociodemographic *-absence-exclusive invariants and being checked; everything else in them was already supported. Those two rules -- "if a patient answered Asked-But-Declined, that must be their only answer" -- are now verified by TypedFhir at runtime in all three ports, rather than only by the HL7 validator against this IG's own examples. They are what stops a decline being counted as a demographic on a field whose stated purpose is reporting.

    The sibling *-instances-clean rules (duplicate detection) remain unverifiable: they need select(), & and isDistinct(), which do not reduce to anything the compiler already has. No consumer action -- nothing about the published contract changes; two rules the IG already stated are now actually checked.

  • The IG now publishes CapabilityStatements (FHIR-35). nexus-emr-server declares the 29 resource types a conformant endpoint exposes and the interactions each honours, derived from the smart-exposed extension the profiles already carry — so "which types can an app reach, and to do what" is answerable from the artifacts instead of by reading 45 profiles. The 14 types this IG models but does not expose are deliberately absent from it: modelling a resource says the estate contains it, exposing it says an application may reach it. General search parameters are not declared yet — that surface varies by backing store, and this IG will not publish a list it has not measured against each one. nexus-emr-subscriptions declares the closed set of types that may appear in a Subscription.criteria and the exact search parameters each permits, so an unsupported criteria can be rejected when the subscription is created rather than matching nothing forever. It also states what the change feed cannot express — transition criteria ("changed into cancelled"), deletes, and unbounded replay — each of which an implementer would otherwise discover by not receiving a notification. Every parameter names a base R4 SearchParameter through definition; none is invented here, and the element each one reads comes from that SearchParameter's own expression rather than from anything this IG restates. That matters because a parameter name does not tell you which element it reads — patient on a DocumentReference reads subject, date on an Appointment reads start, and date on an Observation reads effective[x] in every type the profile permits. The client libraries' criteria vocabulary is generated by resolving those definitions, so a criteria cannot read a different element here than the same query would read on a conformant FHIR server.
  • Patient self-identification fields for sociodemographic capture (PSD-765): the Patient profile slices REUSE CA Core+'s patient-indigenousIdentity / patient-racializedGroup extensions (ca.infoway.io.core 1.2.0-dft-ballot, newly pinned as a dependency) rather than minting Nexus canonicals — repeatable, one answer per instance. Slice bindings tighten CA Core+'s preferred Infoway ValueSets to pinned enumerated mirrors (extensible) of IndigenousIdentityCode/RacializedGroupCode 1.3.0 — absence concepts are first-class members per the source composition — plus warning-severity exclusivity/cleanliness invariants on the Patient profile.
  • Composition.event is modelled: the note's CLINICAL date now has a home (FHIR-35). The profile previously offered only date, which R4 defines as the composition editing time -- it MOVES when a note is amended. So a timeline sorted by date re-positions an amended note to the date of its amendment, and no element said when the documented care actually happened.

    event.period is that element: event.period.start is required when the period is present, event.code says what kind of care it was, event.detail may reference it. Sort a clinical timeline by event.period.start, not by date. Both elements now carry a timing-relationships block and a worked example, matching the treatment DocumentReference already had.

    The seed corpus deliberately makes the two DISAGREE -- an amended note carries a date a week after its event.period.start -- because a corpus in which they always agree teaches a consumer the opposite of what the profile says.

    Composition and DocumentReference disagree about where these facts live. That is inherited from base R4, not introduced here, and it is now written on both profiles rather than left to be discovered:

    what Composition DocumentReference
    when the care happened event.period context.period
    what kind of care event.code context.event
    the visit encounter (top level) context.encounter (nested)

    The same two words nest the opposite way round, and the encounter link sits at a different depth in each. Consumer action: if you learned one of these profiles and assumed the other, check all three rows.

  • Composition.category becomes the preferred classifier, matching DocumentReference (FHIR-35). The two profiles gave OPPOSITE guidance on the same question: DocumentReference said "use category instead" of type, while Composition said type was the primary classifier and demoted category to optional. Composition now follows DocumentReference.

    Why category wins, and it is the reasoning DocumentReference already published: category supports multiple classifications per resource, admits clinical and administrative taxonomies side by side, and leaves type free to carry the source EMR's own note type without displacing the field a consumer keys off. Composition.type stays 1..1 because base R4 requires it, not because it is the right thing to classify on.

    That distinction is not academic. In converted OSCAR data every casemgmt_note arrives with type fixed to LOINC 11488-4 "Consult note" whatever the note actually is, so a consumer keying off type there learns nothing.

    The seed corpus previously emitted a category that CONTRADICTED its own type -- 11488-4 "Consult note" as the category of a note whose type said 11506-3 "Progress note", two document types competing rather than a general and a specific. Both now read LOINC 34109-9 "Note".

  • New value set nexus-emr-note-classification, bound preferred on BOTH Composition.type and Composition.category (https://fhir.apps.health/ValueSet/nexus-emr-note-classification)

    One code: LOINC 34109-9 "Note". That is what the estate stores, and a larger enumeration would be a guess about note taxonomy published as a contract.

    The same set is on both elements because there is nothing yet to distinguish. category is the preferred classifier (see the entry above); type is 1..1 because base R4 requires it, not because it carries a finer second fact. When note subtypes start being stored -- progress vs telephone vs discharge -- type is the element they belong on, and that is the moment to widen this binding back toward LOINC's document-type space. The type element's prose no longer lists four candidate LOINC codes, which a one-code set contradicted.

    preferred, not extensible or required, and the reasoning changed late in review, so it is worth stating plainly. required would make converted OSCAR data non-conformant on arrival: every casemgmt_note carries LOINC 11488-4 "Consult note" whatever the note is. extensible looked like the safe middle and is not, once the set is small -- an extensible binding over an enumerable set is a SHALL-unless-nothing-fits, and base R4 only avoided firing on that same data because its value set was a LOINC filter wide enough to contain 11488-4 already. preferred says the thing actually meant: this is the code we store, and yours is still conformant.

    A preferred binding is not silent in TypedFhir today. All three ports grade any binding that is not example at WARNING severity, so a note carrying a document-type code outside this set will warn despite being conformant -- which converted OSCAR data does, on every note. The warning is a tooling artefact, not a conformance statement. Aligning the ports with FHIR's severity ladder (preferred as information) is a three-port behaviour change and is tracked separately rather than folded into a spec release. Carry an unmapped local type in type.text rather than mis-coding it.

    Two notes for implementers coming from elsewhere. First, base R4 binds Composition.type only preferred; this IG has bound it extensible since before this release, so the tightening is not new here, only the value set is. Second, US Core makes the same split -- coarse classifier on category (a value set of exactly one code, clinical-note), fine slot on type -- so an implementer who knows US Core will recognise the shape. The differences are that this IG uses the LOINC code that already means "Note" rather than minting a local one, and binds extensible rather than required.

    DocumentReference.category is deliberately NOT bound to this set. That resource holds arriving binary documents (scans, faxes, PDFs), a genuinely wider space than authored notes, and a note-classification set would be wrong there.

  • Published page URLs are now locked by a checked-in manifest (FHIR-35). Since the one-page-per-tab split in 1.16.0, a reference page's filename IS its public URL, and it is derived from the tab's DISPLAY LABEL. Editing a label therefore renamed published pages. Measured rather than hypothesised: renaming one tab label ("Salient fields" -> "Salient elements") moves 67 published URLs.

    Nothing caught this, and the reason is the interesting part. mkdocs strict: true checks that links RESOLVE, and after such a rename they all still do, because every internal link is regenerated against the new name in the same pass. The site stays perfectly self-consistent while every inbound link from outside it dies. Internally coherent, externally wrong, and green.

    docs/published-urls.txt now records all 901 page URLs and is compared on every pre-PR run. Additions pass silently -- a new profile or tab is ordinary work, and gating it would make the manifest a chore that gets rubber-stamped, which is how a lock stops being one. Only removals and renames fail, and accepting one is a separate named action (npm run urls:accept) so that moving a public URL is something someone can be seen doing in the diff.

    What this means for you: the canonical-URLs-are-forever promise this IG makes about its published pages now has something enforcing it. Bookmarks and inbound links to a reference page will not be moved by an editorial change to a tab label.

Fixed

  • Task.basedOn and Task.reasonReference are no longer must-support. Both elements remain, both remain typed (base R4 leaves each open to any resource; this profile narrows basedOn to the four request types and reasonReference to Condition / Observation / DiagnosticReport / DocumentReference). What changes is the CLAIM.

    Must-support says a consumer of this IG will do something meaningful with a populated element. This IG is not yet in a position to say that for either: nothing populates them, and a surface that displays a task reads focus, owner, requester, status, priority and the due date.

    Advertising support that does not exist has a specific cost, and this release is the demonstration of it. Because both were must-support, the seed generator had to populate them, so it reached for the nearest resource in scope: basedOn got the Encounter and reasonReference got the Patient. "This task is because of the patient" says nothing -- every task is about the patient. Both are fixed above, but the fix people should take away is the demotion, not the patch: an element nobody reads is one producers fill with whatever is to hand.

    If you populate either today, nothing breaks -- they are still modelled, and the published StructureDefinition still narrows their targets, so a validator that checks reference target profiles will hold you to them.

    TypedFhir is not such a validator. Its IG generator reads type[].profile and never type[].targetProfile, so what reaches the ports for a narrowed reference is the bare datatype (types: ["Reference"]) and nothing enforces the target list in TypeScript, Python or C#. That is true of Task.focus above as well, and it is the reason FHIR-45 exists. Do not read "typed" as "checked by our client libraries" -- today it means "declared in the specification".

    What would make them must-support again: a consuming surface that reads them.

    Note the overlap with focus, which is the element to reach for first. focus is the chart element the task is filed AGAINST; reasonReference is why it exists. A task to review an abnormal potassium has that DiagnosticReport as its focus, and the Condition it relates to (if any) as its reasonReference. Must-support coverage in the seed corpus moves 575 -> 573 accordingly.

    The seed corpus no longer emits either element. Populating them with the nearest resource to hand is what produced the wrong types in the first place, and synthesizing a plausible order or a plausible clinical justification for a task that has neither would put the same fiction back, one type-check better dressed. If you were reading Task.basedOn or Task.reasonReference out of the seed data, they are gone -- they were never carrying real information.

    Wider question logged as FHIR-47: 20 of this profile's must-support elements are justified in published prose by the same source, and only these two have been examined.

  • Nearly half of all "normal" lab results sat exactly ON a reference-range boundary. The N tier drew from the wide near-normal sampler and CLAMPED into the reference interval. Because the sampler's spread is deliberately greater than 1 (the general lab path needs out-of-range values), clamping deposited every out-of-range draw exactly on a bound. Measured over 200,000 normal results: 22.4% landed on exactly 3.9 and 22.4% on exactly 5.5 for Glucose. Creatinine put ~17% on each of 60 and 110, about 9x what a flat distribution would give.

    The distribution was bimodal at the edges, which is the opposite of the mid-clustering the code claimed, and it is visible to anyone who looks at the seed data: a demo chart full of glucoses reading exactly 5.5 does not look like lab data.

    The N tier now draws a triangular in-band value (mass concentrated mid-range, tapering toward the bounds, no spike on them); the abnormal tiers stay uniform, since there is no clinical reason to prefer the middle of "high" to its edges. Guarded in interpretation-tiers.test.ts by two assertions -- no bound spike, and a modal value in the middle third -- both mutation-tested against the old clamping.

    This moves lab values for a given seed. It is part of the seed break already declared above.

  • Composition.event.period.start is now the DECLARED effective date, not just the documented one. The profile told readers in bold to sort a clinical timeline by event.period.start, and the machine-readable declaration three hundred lines above it still said date. Everything that actually reads the effective date reads that declaration -- the facade accessor, the PatientCache recency windows, and effective_date_of over raw JSON -- so a note written on the 10th and amended on the 18th still sorted to the 18th in all three ports, which is exactly the failure the prose was added to prevent. Now event.period.start | date, a precedence list, so the clinical date wins and the authoring date remains the fallback for notes that carry no event.
  • The reference-target guard was checking less than it claimed. Three holes, all found in review, all closed:

    • It keyed constraints per RESOURCE TYPE while reading each profile's snapshot, so two profiles of one type collided and whichever sorted last won. nexus-emr-core-app-consent narrows Consent.provision.actor.reference to a Device; item-consent does not narrow it at all, and its inherited eight-type list silently replaced the narrow one. Now keyed per PROFILE and selected by each resource's own meta.profile, the same rule the coverage tool uses, so both constraint sets coexist and each instance is judged against the profile it claims.
    • It filtered to kind === "resource", dropping every extension, so all 191 extension-borne references in the corpus were unchecked. Extensions are now derived too, keyed by (canonical, slice) since that is how the constraint is actually addressed.
    • It compared JSON paths against FHIR element paths, so a constraint on a choice element such as Consent.source[x] could never match an instance's sourceReference. The choice form is now tried as a FALLBACK after the literal path -- never as a substitution, because Task.reasonReference is a real element name rather than a choice, and rewriting it unhooked a constraint that was being enforced.

    Constraints derived: 290 -> 302. All four originally-fixed violations are now individually mutation-tested, so a future refactor that unhooks one fails here rather than in a release.

  • Four seed-data references pointed at the wrong resource TYPE, and the validator did not notice. Task.focus -- the element this very release types as a breaking change -- was itself pointing at an Encounter in the generated corpus. Found by auditing all 61,399 generated resources against the 290 typed-reference constraints the IG declares:

    Element Pointed at Allowed
    Task.focus Encounter ServiceRequest, DiagnosticReport, Composition, Condition, DocumentReference, MedicationRequest, Observation, QuestionnaireResponse
    Task.basedOn Encounter CarePlan, MedicationRequest, ServiceRequest, SupplyRequest
    Task.reasonReference Patient Condition, DiagnosticReport, DocumentReference, Observation
    Location.partOf Organization Location

    The last is wrong in base R4, not only under this IG: Location.partOf is the location hierarchy, and the owning organization is managingOrganization. The corpus now carries a site Location that the clinic building is part of, which is the smallest honest hierarchy.

    Why nothing caught it, and what you should take from that. TypedFhir's validator does not enforce reference target types. It checks cardinality, bindings, invariants and datatype shape, but a Reference is only shape-checked, so {"reference": "Encounter/e1"} in a focus typed to exclude Encounter validates clean in all three ports. If you are migrating for the Task.focus typing above, do not expect the validator to find your violations for you yet. Tracked as FHIR-45, which carries the design and the acceptance criteria.

    In the meantime the generator has a standing guard (fixtures/patient-generators/test/reference-targets.test.ts) that derives every typed-reference constraint from the IG's own StructureDefinitions and walks the whole corpus against them, so a new profile rule is covered the moment it is written. That guard covers the seed corpus only; it does nothing for your data.

  • "ICD-9 subset used by OHIP" was wrong, and it shipped. Read this one if you produce OHIP diagnostic codes (FHIR-32). The OHIP diagnostic code list was described in a published comment as an ICD-9 subset. It is not. OHIP diagnostic codes are Ontario's OWN three-digit set: ICD-9-like in shape and sharing many codes, but with a parallel Ontario space laid over ICD-9 numbers that are already occupied.

    OHIP 970 = "Other Acceptable Diagnosis: Prenatal Care"   ICD-9 970 = poisoning by CNS stimulants
    OHIP 967 = "Immunization: Influenza"                     ICD-9 967 = poisoning by sedative
    

    Eighteen codes carry two unrelated meanings inside OHIP itself (072 is both "Mumps" and "Gait Training"), so a code alone does not determine a concept.

    Why this was dangerous rather than pedantic. A reader could reasonably conclude that SNOMED -> ICD-9 -> truncate is a safe route to an OHIP code. Measured against the Provincial Health Care Vocabulary Standard: 69.4% reach, and 124 rows truncate into the 895-971 range and come out VALID AND WRONG. "Poisoning caused by opioid receptor agonist" becomes 970 Prenatal Care; "Pica" becomes 307 Sleep-Wake disorders. Nothing downstream rejects either, and they go on a claim.

    Consumer action: map explicitly from the concept list. Never compute an OHIP diagnostic code from ICD-9. The published comment now says what the set is, shows the collisions, and carries the measurement.

  • The task-intent value set's description of R4 was wrong, and it ships (FHIR-35). The comment claimed R4 has no task-intent code system and that directive was among the codes this IG declines. Neither is true: R4's task-intent value set composes from TWO systems, http://hl7.org/fhir/task-intent included whole (it defines unknown) plus eight request-intent concepts, and directive is not among them at all. FSH comments compile into the StructureDefinitions and render on the published profile pages, so this was a wrong claim about the base specification on its way to implementers.

    The corrected text lists all nine base codes and gives a reason for each exclusion, so the narrowing to order | proposal reads as a decision rather than an omission. One is worth stating here: unknown is excluded deliberately. A task whose intent is unknown cannot be triaged, because "is this mine to do, or mine to decide?" is exactly what a consumer reads this element to answer. A writer that does not know yet should not be minting the task. No consumer action -- the binding itself is unchanged from 1.16.0.

  • Published prose that contradicted this release's own artifacts (FHIR-35). Three partner-facing places still said the IG publishes no CapabilityStatement, after it began publishing two: the Subscription profile's criteria comment (which ships inside package.tgz), the "About this IG" page, and the Composition narrative. Corrected, and narrowed rather than simply deleted where only half the claim had gone stale -- the Composition warning now says what remains true, which is that the server statement declares Composition's interaction set but still declares no versioning policy, so whether prior versions are retrievable at all is server-specific. The "what this IG does not publish" list is now derived from the compiled artifacts instead of hand-written, so it cannot make that class of claim again.

    The Subscription criteria comment is the one to read if you consume subscriptions: the permitted resource types and search parameters are now DECLARED (in nexus-emr-subscriptions, and the client libraries' criteria vocabulary is generated from it) but are not ENFORCED by the profile, which still constrains criteria only to 1..1. Validate against the CapabilityStatement when accepting a subscription from an untrusted writer.

  • Five guards did not cover what they appeared to cover (FHIR-35), all found while closing out 1.16.0. Grouped because they share one signature: green, self-consistent, and blind.

    * The FHIRPath subset drift gate iterated the default IG only, so a second registered IG could ship compiled invariants that nothing had ever checked could compile. It now iterates every registered IG, driven off the registry itself so the test cannot drift from it. * isSupported now fails closed on boolean literals. active = false compiled to a navigation into a member named "false" -- always empty, so the predicate could never fire while reporting itself as supported. No IG invariant uses that form today; it is closed while still theoretical, which is the only comfortable time to close it. * extensionAppliesAtRoot reports unanchorable-context instead of silently dropping a wildcard context it does not understand. * check-fragments gains a zero-guard (an empty run reported success), matching check-extension-invariants. * The IG version lives in six places and only two were checked against each other. Cutting 1.16.0 found the rest one red gate at a time and still missed the Python registry, which shipped a package that could not be imported. One step now checks all six and names the fix.

  • The server CapabilityStatement's "derived" claim is now enforced (FHIR-35). It states that its resource list and interactions come from the smart-exposed extension the profiles carry. They did, but only because someone had checked by hand: 29 entries of hand-written FSH with nothing connecting them back. Since a scope naming a type the statement does not list is not granted, a stale entry there is an access-control defect rather than a documentation one. check-capability-derivation recomputes the list from the compiled profiles and fails on divergence in either direction -- an interaction the grant does not justify, a type listed that nothing exposes, a type exposed that is not listed, or two profiles of one type disagreeing.
  • Four analytes were reported to the wrong precision, and one could fall outside its own reference range (PSD-417). Decimal places were guessed from the unit string, which printed TSH as 1, INR as 2 and WBC as 7 -- none a value a laboratory reports. Worse, TSH's reference range is 0.4 to 4.0 while its values rounded to integers, so a result the generator had decided was Normal could be emitted as 0 and read Low. Precision is now stated per analyte where the unit does not imply it, and normal values are snapped to the representable grid INSIDE the reference range rather than clamped before rounding.
  • Duplicate resource ids in a generated tenant (PSD-412). tenant.ts hands one patient RNG to two generateChart calls (a mother and her newborn), so once ids were drawn from position-independent per-domain streams, both charts minted the same ones. Ids are hashed from a per-chart scope now, and a tenant-wide uniqueness assertion guards it. Caught before release by the new stability test; no published corpus carried it.
  • The Python port under-reported the IG's own rule set (FHIR-35). 1.16.0 taught TypedFhir to compile invariants declared on EXTENSIONS, and the TypeScript registry folded them into the loaded invariant list. The Python registry did not, so typed_fhir.ig.INVARIANTS -- exported public API -- was silently short by exactly the rules extensions declare (66 where TypeScript reported 68). Nothing on the Python side could go red for it: there was no twin of the TypeScript assertion, and the Python test fixture had no extensions at all. Both are added, so the two ports stay pinned to the same set.

    Consumer action: if you read typed_fhir.ig.INVARIANTS to enumerate what the IG requires, it now returns the complete set. LoadedIG also gains an extensions field, matching the TypeScript shape.

  • A drift gate could no longer fail for ten common invariant keys (FHIR-35). The allowlist of conformance rules TypedFhir cannot verify is keyed ResourceType:key, but the check matched on the bare key, which blanket-exempted status, type, intent, category, severity, mode, criticality, verificationStatus, confidentiality and lifecycleStatus on every profile in every registered IG. Nothing was being wrongly skipped when this was found -- the defect was that the gate could not go red for those keys, which reads as coverage. Now matched on the full key, with a mutation test that fails if the bare-key behaviour returns. No consumer action.
  • Two gates were demanding incompatible things (FHIR-35). One asserted that EVERY invariant compiles to the emittable subset; the other froze a list of the ones that do not. Allowlisting in the second left the first red, and the only state satisfying both was "support every expression the IG could ever contain" -- a policy nobody chose, just what two half-overlapping gates added up to. The allowlist now lives once and both gates read it.
  • Four invariants are newly declared unverifiable at runtime, and the cost is stated rather than buried (FHIR-33/FHIR-35). The sociodemographic *-instances-clean rules need select(), isDistinct() and &, which are not in the emittable subset. What this means in practice: they are enforced by the HL7 validator against the examples this IG publishes, so they hold for what we publish and NOT for what a writer sends us. Both are warning severity, so the effect is that a malformed sociodemographic answer can reach a store unremarked. Stated here because a consumer relying on TypedFhir's runtime validation should know which rules it is and is not checking on their behalf.

[1.16.0] - 2026-08-14

Generated by TypedFhir 0.10.0. Bumped from 0.9.0 (which generated 1.15.1): this release teaches the library to compile and run invariants declared on EXTENSIONS, in all three ports, and adds two FHIRPath forms (xor and the extension('url') shorthand) to the emittable subset so they can be written the way an author would write them. It also closes the last blind spot in the IG chain, where the vendored snapshot could go stale without any guard noticing. All three manifests move together -- ts/package.json, python/pyproject.toml and dotnet/TypedFhir/TypedFhir.csproj.

MINOR, and it contains a breaking change (ClinicalTask.intent is no longer fixed to order). That is the draft carve-out: while sushi-config.yaml says status: draft, a breaking change bumps MINOR and the changelog carries the obligation to name the break and the consumer action. Both are stated under Changed, below.

Scope on the ticket: the published prose surfaces and the one-page-per-tab reference split, attribution modelling for the AI harness (PSD-848), and the verification work those two turned up.

Added

  • The reasoning behind a constraint is now published (FHIR-32). definition, comment and requirements are where the WHY of a constraint lives, most of it hand-written in FSH carets, and none of it reached this site: it compiled into package.tgz and rendered only on IG Publisher pages. Elements now have a Details view, every element name in the Differential / Key / Snapshot tables links into it, and an element carrying a comment gets a marker in the description column, because comment is often the most useful prose on an element and it renders in no table. Patient alone gains 169 prose blocks and 199 markers across 102 elements.

    Salient fields carry short, definition and comment separately rather than one lumped description, so each consumer takes the text written for its audience: the table cell, the generated docstring, and the pydantic schema an LLM reads. 94 of 95 salient fields inherit short and definition with nobody writing a word.

  • One page per tab in the generated reference (FHIR-32). The tabs were one page carrying every view; each leaf view is now its own page and the tab bar is a strip of links. 273 generated pages become 890. Patient goes from a single 521 KB page to 14 pages, largest 184 KB, median 11 KB.

    Existing URLs are preserved -- the first leaf keeps StructureDefinition-<id>.md, so every link and bookmark to a profile page still resolves. The reason for the split is that hidden tabs were still in the DOM: a search hit or an inbound anchor could land on content the reader could not see. Every view is now separately addressable, and search results land visible.

Fixed

  • The vendored IG snapshot can no longer go stale unnoticed (PSD-848). The chain is doc/fhir -> SUSHI -> vendored snapshot -> the generated tables, facades and patient-compartment map, and every guard sat at or below the snapshot: they regenerate from it, or compare against it. None could see the FIRST link break. Editing the FSH without re-running refresh:ig left the snapshot unmoved, so every guard agreed with every other guard about an IG that no longer matched its own source, and CI stayed green.

    refresh:ig now stamps an igSourceHash into the manifest -- a content hash of src/fsh, src/examples and sushi-config.yaml, the inputs that determine the vendored artifacts -- and a new test recomputes it. A mismatch means the source has moved since the snapshot was taken, and says so with the command that fixes it. It runs in the npm test chain, so the required CI check enforces it rather than only the owner-track gate.

    No consumer action. A build-integrity check; nothing about the published contract changes.

  • Invariants declared on EXTENSIONS are now compiled and enforced (PSD-848). They previously were not, by any mechanism: extension StructureDefinitions never entered TypedFhir's IG model (only kind: "resource" was kept), so their constraints reached no table, no compiler and no gate. Such a rule rendered on its published profile page, read as enforced, and checked nothing -- and the gate that exists to flag an unverifiable invariant could not see it either, because it iterates the invariants the model collects. effective-date-declares-exactly-one was in that state for its whole life; it is now verified against all 45 profiles that carry the extension.

    Two FHIRPath forms were added to the emittable subset to make it compile as written: xor, and extension('url') as sugar for extension.where(url = '...'). The sugar matters beyond this rule -- it is what an author naturally writes (HL7's own invariants use it), and until now reaching for it silently produced an unverifiable invariant.

    Extension invariants are now also compiled into the RUNTIME validators. An extension's rules are not in the profile's snapshot -- a sliced extension element carries only ele-1/ext-1 -- so TypedFhir checked none of them on real data, even where the HL7 validator was checking them against our examples. gen-facade now resolves them BY URL, matching how an extension is identified in the data, so a rule applies wherever its extension appears rather than only where a profile happens to slice it. All three ports.

    Mostly no consumer action -- a rule the IG already stated is now actually checked. The one addition is below.

  • nexus-attributed-fact requires substantive citations (PSD-848): new invariant attributed-fact-citations-are-substantive -- the statement, and every citation's verbatim quote, must be non-empty. 1..1 requires those elements to be PRESENT; it does not stop them being "", and an empty quote is worse than a missing one: it is a citation that cites no text, passing every structural check while leaving the fact unauditable, which is the one property the extension exists to provide. Error severity. The extension is experimental and unreleased, so no published contract tightens.

Changed

  • BREAKING -- ClinicalTask.intent is no longer fixed to order (PSD-848). It now binds nexus-emr-task-intent (required): order for work already authorized and assigned, proposal for a suggestion that takes effect only once a human accepts it.

    Consumer action: code asserting intent === 'order', or relying on it being present with that value, must handle proposal. If you were identifying AI suggestions by code, the AIAST security label, or the presence of a contained resource, read intent instead -- those were proxies for it. Task instances already in a store are unaffected: order remains valid and no stored value changes meaning.

    Why: the fixed value described the population the profile was written for -- inbox items, follow-ups, to-dos, which genuinely are orders -- and prevented a scatter of intents for identical work. Modelling AI suggestions onto the same profile made it wrong: a suggestion awaiting a clinician's approval is exactly what "not proposals or plans" excluded, so intent could not distinguish "assigned to you to do" from "awaiting your approval". The binding is narrowed to two codes rather than reopened to R4's nine, because the order-flavour refinements describe a fulfilment chain this model does not have, and a task whose intent is unknown cannot be triaged at all. The FSH records the accept-or-reject for each of the nine.

  • Provenance leaves placeholder status (PSD-848): the profile now has a committed job -- attribution for AI-mediated and cross-boundary writes, in two worked shapes (standalone, and contained in an AI-suggestion review Task) -- and is marked experimental = false. No cardinality, type, binding or slicing changed, so nothing that validated before stops validating. One invariant did get real teeth: provenance-ai-action-names-agent was effectively vacuous (agent.exists()) and is now a genuine conditional check. Its severity is still warning, so it fails nothing -- but a producer relying on it never firing will start seeing it fire. Model/token accounting, the meta UpdatedBy relationship, and whether Provenance is written for ordinary human edits remain open and are documented as such.
  • Attributed facts on Provenance (PSD-848): new nexus-attributed-fact extension carrying, for AI-generated text, each factual statement with its backing verbatim quote(s) and the generating run's chunk label -- a lossless mapping of the harness's fact/citation wire schema, with an optional source reference tying a citation to the Provenance.entity it was drawn from. Sliced into the Provenance profile as attributedFact 0..*, with a worked pre-visit-summary example. The AI-suggestion Task example now also carries a CONTAINED Provenance (linked from relevantHistory) citing the passage its contained suggestion was extracted from -- the suggestion and its citation trail travel and are approved or rejected as one unit.
  • A suggestion's review state belongs on the Task (PSD-848): the ClinicalTask handbook page now states the rule and the contained-suggestion example follows it. A contained suggested resource is written as the clinical fact its citations support, so the example's Condition moves from verificationStatus = unconfirmed to confirmed and gains the required category. Using unconfirmed to mean "not yet approved" reads as clinical doubt to every consumer -- verificationStatus is a modifier element -- and the wrong value survives into the chart on approval, because approving moves Task.status and changes nothing inside the contained resource. The same rule keeps a suggestion promotable: it should already satisfy its profile.

[1.15.1] - 2026-08-12

Generated by TypedFhir 0.9.0. Bumped from 0.8.0 (which generated 1.14.1): this release adds sibling-profile-aware validators, pydantic schema generation, seven profiles' worth of typed accessors and facades in all three ports, and the since-checkpoint delta fetch. All three manifests move together -- ts/package.json, python/pyproject.toml and dotnet/TypedFhir/TypedFhir.csproj.

Scope on the ticket: AI-harness modelling (the suggestion/Task model), TypedFhir work the harness depends on, and terminology-server-related binding work.

Added

  • The tenant stamp is declared: the tenant-id extension (FHIR-29). Every resource stored through the FHIR Gateway carries http://services.well.com/fhir/auth/tenant-id on DomainResource, and the IG did not declare it -- so it appeared on every payload with nothing to resolve and no explanation. It is now a published extension, documented as what it is: the gateway stamps the caller's tenant on every create and update and strips any client-supplied value first, so tenancy follows the token and a client cannot choose it.

    Both this and the userId identifier are private infrastructure and must not be depended on -- unresolvable URLs, opaque values, no breaking-change guarantee, and deliberately kept off the governed fhir.apps.health host to say so. If you need to know which clinic a resource belongs to, use the modelled Organization / Location references instead. See the Identifiers page in the handbook.

  • Seven new first-pass profiles close the parallel-efforts modelling gaps (FHIR-29, from the 2026-08-10 gap triage; every one has a named consumer): ImagingStudy (the imaging half of results viewing, PSD-417; DiagnosticReport.imagingStudy now targets it), Coverage (payor context for billing/demo -- the Ontario shape, payor as a logical Organization reference), FamilyMemberHistory and MedicationStatement (the harness's Patient Summary Domains read list), Goal and CarePlan (CDM flowsheets and care gaps; CarePlan also resolves the corpus's no-model-but-generated inverse gap by profiling rather than un-emitting), and RiskAssessment (computed risk scores; replaces the chart-module registry's profileAbsentReason placeholder). ALL seven are CLOSED to SMART apps pending an exposure decision (the FHIR-28 policy for newly profiled types).
  • Result review state: the nexus-reviewed extension (FHIR-29) on DiagnosticReport and Observation -- who acknowledged a result and when; ABSENT means unreviewed. The Task inbox is the review workflow; this is the queryable state it stamps, so a results viewer needs no Task join. The seed corpus emits a realistic reviewed mix with unreviewed volume on recent results.
  • ICD-10-CA and CCI get named slots, not content (FHIR-29): system-sliced secondary codings on Condition.code (icd10ca) and Procedure.code (cci). Codes are deliberately NOT enumerated -- both classifications are CIHI-licensed and the licence paperwork is in flight (the prevention-catalog pattern: the IG declares the slot and the system; content governance lives outside).
  • The seed corpus covers the grown surface: emitters for all seven new types (payor context incl. a cancelled prior plan, family history incl. a health-unknown relative, patient-reported OTC meds incl. a discontinued one, the CDM bundle tying Goal + CarePlan

    • RiskAssessment to a live chronic Condition, imaging studies with their reports) plus the review-state stamp. Coverage guarantee after the growth: 43/43 profiles, 577/577 must-support, both directions clean with the CarePlan allowlist entry REMOVED.
  • The seed corpus got the content the parallel demo efforts were missing (FHIR-29). Abnormal/critical lab flags (H/L/HH/LL interpretations on a realistic minority of results, deterministic on the anchors -- PSD-417's ask), ranked encounter diagnoses spread across the record instead of one flat rank-1 (PSD-606), authored Composition notes on ~85% of encounters INCLUDING the GMF follow-up majority, amended-note snapshots (the version CHAIN stays a load-path concern -- a flat corpus cannot carry _history), a meta.tag synthetic marker on every resource (corpus-local URN system; one _tag query finds or purges fixture data), per-patient collection-Bundle output behind --bundles, and OHIP fee-code variety (six schedule entries, occasional add-on line item) instead of a single hardcoded A007.
  • The coverage guarantee now fails in BOTH directions (FHIR-29). A new inverse check: any resource type the generator EMITS that the IG does not profile fails the guarantee unless it has a documented ALLOWED_UNMODELED entry (same visible-reasoned style as the existing exclusions). The motivating example -- 293 un-modelled CarePlans quietly shipping into benchmark stores -- is now the one allowlisted entry, explicitly flagged as pending the CarePlan modelling ruling rather than resolved.
  • bin/pre-pr.sh: the mandatory owner-track pre-PR gate (FHIR-29). One entry point chaining SUSHI build -> example conformance -> version-drift check (sushi-config vs ig-specs.ts, automating the hand-bump that trailed a release) -> IG vendoring + code generation for every generated consumer (TS, Python, C#, and the docs tree) -> npm run verify (the WHOLE TS suite, i.e. what CI runs, not a slice of it) -> the data-generator typecheck -> the bidirectional data-generator coverage guarantee -> a git-clean lockstep check on every generated tree. Documented as mandatory in the IG owner release process: regenerating code from the IG and proving data-generation coverage are now ONE event, so the corpus can never again trail the spec the way the note model did.

    The gate now covers EVERY generated consumer of this IG -- the TypeScript, Python and C# libraries and this documentation site -- and refuses to pass unless all of them regenerate clean from the same FSH. What that buys a consumer: the artifacts published under one version cannot silently be a release apart.

  • The seed-data generator covers the whole 1.15.0 IG surface again (FHIR-29). The coverage guarantee is green across every dimension (profiles, must-support, bindings, invariants, extensions): the anchors gained the Composition encounter note (the 1.14.0 note switch), both consent shapes (app-consent grant + deny, item-consent provided + declined-with-evidence), the AI-suggestion Task (contained payload, thread-id, logical Device requester), the prevention-status Observation, the boolean app-state, and the heritage-inactive Patient (record-inactive-reason) -- plus a latent app-state extension URL bug fixed. TypedFhir's generated validators are now SIBLING-PROFILE AWARE: a resource declaring a sibling profile of its type via meta.profile (Consent app/item, Device app/emr) is validated against that profile's own rules instead of whichever profile sorted first -- previously every item-consent instance failed app-consent's pinned category, in every port that validates. The dispatch is emitted by all three facade generators, so TypeScript, Python and C# behave identically; a conformant item-consent and a conformant emrdevice are pinned in the cross-language parity corpus (vectors/facade-adversarial.json), so a port that loses the dispatch fails the parity gate rather than silently rejecting valid resources. nexus-sugg-1 was reformulated inside the compilable FHIRPath subset (plain choice access instead of ofType; same semantics), so it is machine-enforced rather than prose. The vendored IG, generated tables, facades (TS + Python + C#) and registries now stand at 1.15.1.
  • AI suggestions are modelled as resources CONTAINED in their review Task (FHIR-29). A suggested chart entry is never a first-class chart resource pre-approval: it rides Task.contained, referenced from the new pattern-sliced suggestion input (input.type = nexus-suggestion#suggested-resource), with Task.code = ai-suggestion (enforced by the new nexus-sugg-1/2 invariants: the suggestion reference SHALL be local, and a suggestion Task SHALL declare itself). Type-level queries cannot see suggestions by construction -- confirmed empirically on both the WellServices store (MS FHIR Server for Azure) and Medplum. Lifecycle rides Task.status natively (requested -> accepted/completed, or rejected -- rejected Tasks and their payloads are RETAINED as the record of what was declined). New: the nexus-thread-id extension (harness thread traceback), the nexus-suggestion CodeSystem, and a full contained-suggestion example (AIAST-labelled Task carrying a suggested Condition).
  • AI agent identity: logical Device references, no registration (FHIR-29). An AI graph acting in the EMR is a Device referenced BY IDENTIFIER (the new nexus-harness-graph NamingSystem -- the tree's first declared identifier-kind NamingSystem; Reference.type = "Device"); the identifier points into the harness's graph registry and need not resolve to a Device resource on the server. Granularity is deliberately graph/feature level. Task.requester and Task.owner now admit Device: an AI-created Task names its graph as requester, and REASSIGNING a Task to an agent Device is the approve/resume contract (the harness watches task-assignment changes and resumes the thread named by the thread-id extension).
  • TypedFhir (Python): since-checkpoint delta fetch + PatientCache.refresh (PSD-731). fetch_resources_delta answers "what changed since this opaque checkpoint" (_lastUpdated-windowed on the WellServices gateway, with an overlap so out-of-order commits are never lost; full-fetch fallback on every other source), and PatientCache.refresh merges the delta in place by (resourceType, id). Checkpoints never advance past a window a covered type failed to sweep; a query built before a refresh raises rather than answering from stale bits. Hard deletes are invisible to FHIR search, so a refresh converges between full loads rather than replacing them. A cross-patient resource inside a delta is treated as an ERROR (backend corruption, dropped + error issue + held checkpoint, so it re-reports every refresh until fixed) -- never as a quiet removal.
  • The C# port catches up: facades for all seven new profiles (FHIR-29). CarePlan, Coverage, FamilyMemberHistory, Goal, ImagingStudy, MedicationStatement and RiskAssessment had TS and Python facades but no C# ones, so a Well.Services.TypedFhir consumer could not touch any of the seven types this release added. Fourteen generated files were simply missing. The cause was the pre-PR gate not running gen:facade-cs (above); the port had been a release behind with nothing reporting it. dotnet build is clean on the backfill.
  • bin/site.sh zip packages the human-readable IG for sending (FHIR-29). npm run site:zip builds the MkDocs site and zips it to ~/Downloads (or ZIP_DEST) as <date>-<version>-nexus-fhir-ig-mkdocs.zip, entries zipped from inside the site root so extracting drops index.html into the cwd with assets/ resolving, and the same self-containment proof publish.sh --zip runs: every local asset index.html references must be an entry in the zip. use_directory_urls is off, so the extracted site works straight off disk with no server. This is the artifact to send someone who just wants to read the IG -- the -mkdocs suffix keeps it distinct from the publisher's -full / -slim renders. Those two also drop -emr- from their filenames (<date>-<version>-nexus-fhir-ig-full.zip), so all three variants now share one naming scheme; anyone globbing the old name needs to update it.
  • The seed-data generator is type-checked (FHIR-29), with npm run typecheck wired into the pre-PR gate alongside the existing coverage guarantee.
  • IP statements are published, and the IG now declares who publishes it (FHIR-29). New IP statements page listing the licence terms of every code system this specification binds. Read it before implementing: SNOMED CT content requires an appropriate SNOMED International Affiliate licence in your jurisdiction, and that obligation passes through to anyone deploying against this IG. It was previously stated nowhere.

    publisher, contact, jurisdiction (Canada) and a copyright statement are now set on the IG. copyright compiles into every StructureDefinition and ships inside package.tgz, so the licence terms now travel with the artifacts to a tool that never loads the site. The licence itself is unchanged: CC0-1.0 for this specification's own text, which is why the third-party terminology terms needed saying separately.

    The dependency table, cross-version analysis and globals table are also published for the first time, on the publisher render's new IG-metadata page. All four were being generated and discarded.

Changed

  • DocumentReference narrows to attached binary documents; encounter notes move to Composition (FHIR-29). A re-cut rather than an edit, so no constraint tightened in the diff and nothing fails validation -- but the BOUNDARY between the two profiles moved, and that is a consumer-facing change. Composition is now the model for an authored encounter note; DocumentReference is for a document that arrives as a file (PDF, scan, Word, fax). If you write textual notes to DocumentReference today, that shape is no longer the modelled one, and the same rule is restated on both profile pages. No migration is forced in this release: nothing rejects an existing DocumentReference-borne note, and both profiles remain valid to read.
  • Practitioner.identifier[userId] now names the authority the platform actually stamps (FHIR-29). The slice pinned https://fhir.apps.health/NamingSystem/user-id; every real Practitioner carries http://services.well.com/fhir/NamingSystem/user-id. Because identifier slicing is open, that mismatch never failed validation -- the identifier fell into the open remainder and simply never matched the slice, so anything reading identifier[userId] (or a generated accessor for it) found nothing on every Practitioner in the estate. The slice is now pinned to the real system and matches data. If you looked for this identifier by the old system and found nothing, that is why.

    The authority stays on the internal host. Moving it to the governed one is a data migration across every stamped Practitioner plus its search parameter, not a spec edit, and it is not in this release.

  • appointment-participant-provider is renamed to appointment-participant-practitioner (FHIR-29). Consumers matching on the invariant key must update it -- for the second time in four releases. 1.12.0 renamed this key from appointment-participant-practitionerrole to -provider while widening the rule underneath it. -provider names an abstraction with no resource type behind it, while every sibling key in the profile (appointment-participant-patient, appointment-participant-location) names the referenced FHIR resource type. Practitioner is this IG's primary provider identity, so the key now says so. The rule itself has not changed since 1.12.0 and no data or code change is needed beyond the key string: at most one participant whose actor references a provider, counted across both permitted spellings (Practitioner or PractitionerRole), which is what the Description still states. Raised in review -- in a published artifact, a key that makes the reader guess what it constrains is a defect rather than a matter of taste.

Removed

  • Dead internal tooling removed: 39 files, 20 npm scripts (FHIR-29). The seed-data generator package carried a lane of scripts built on the typed-fhir codec that was retired in July 2026 -- the MedPlum load harness, the Synthea patient source, the benchmark drivers and round-trip gates -- none of which could run any more. All of it is gone, along with the npm scripts that only pointed at it (emit:*, bench:*, medplum:*, test:corpus, test:reconstruct, test:fidelis*, ref:roundtrip, generate:fidelis*).

    No effect on this IG or on anything generated from it. The seed corpus is unchanged -- still 51,155 resources at 43/43 profiles, 577/577 must-support, 48/48 bindings, 60/60 invariants, 22/22 extensions -- and npm run generate remains the supported way to produce IG-conformant synthetic data. Previously published benchmark measurements are retained in the generator's docs/, marked as historical, and now point at that generator for any new measurement.

Fixed

  • AI-scribed note authorship had no conformant shape (FHIR-29). Composition.author and section.author constrained their Device targets to the two PROFILED Device types (nexus-core-appdevice, nexus-core-emrdevice), while an AI agent is named by a LOGICAL Device reference -- Reference.identifier under the nexus-harness-graph NamingSystem, with no resource on the server. A logical reference cannot conform to a profiled target, because conforming to a profile presupposes a resource that exists to validate, so an AI-authored note could not name its author conformantly at all. Both elements now admit bare Device alongside the profiled types, matching ClinicalTask.requester / .owner, which already did. The profiled targets stay for a Device genuinely registered in this store.

    The author prose was stale in the same way: it still described agent identity as an open question, which this release settled. It now states the mechanism, and says why bare Device is a requirement of that mechanism rather than laxity in the target list.

  • Eight conformance rules on the new profiles are NOT machine-verified (FHIR-29) -- worth knowing if you rely on the generated validators rather than a full FHIR validator. CarePlan.intent, Coverage.status, FamilyMemberHistory.status, Goal.lifecycleStatus, ImagingStudy.status and MedicationStatement.status carry required bindings whose R4 value sets cannot be expanded from the vendored IG, so the generated validators report them as unverifiable rather than checking against a partial expansion. RiskAssessment ras-2 uses FHIRPath type operators (is / as) outside the compiled subset, and ras-1 is anchored below the resource root. All eight are recorded in the IG's frozen unverifiable-rule allowlist with their reason, so the set cannot grow silently; a full validator still enforces every one of them.
  • Internal build tooling corrections (FHIR-29): a dead option removed from two internal validation calls (no change to what is enforced -- strictness is unconditional), the coverage report now naming the invariants it counts as uncovered, and a date defect in a generator diagnostic. None of these change the IG, the generated libraries, or the corpus.

[1.15.0] - 2026-08-11 -- WITHDRAWN

Do not use 1.15.0. It was published on 2026-08-11 and withdrawn the same day. Its content is released unchanged as 1.15.1, minus one item: 1.15.0 renamed every identifier in this IG's namespace, dropping the -emr- infix, which changed canonical URLs. Those URLs are not only definitions -- CodeSystem URLs are Coding.system and NamingSystem identifier systems are Identifier.system, so they exist in stored data. The rename is retracted and the identifiers in 1.15.1 are the same as in 1.14.1.

If you fetched 1.15.0, move to 1.15.1 and revert any identifier constants you changed. Nothing else differs. If you are on 1.14.1, no identifier changed and there is nothing to do.

The archived package for 1.15.0 is deliberately still served, so a tool pinned to it keeps resolving; it is a real artifact that was really published.

[1.14.1] - 2026-08-09

Generated by TypedFhir 0.8.0.

Added

  • SMART on FHIR exposure is now declared per profile (the new smart-exposed extension). Whether a resource type can be reached by a SMART app is an authorization decision, and it is now recorded in the IG rather than inferred from whether the resource happens to be profiled. A profile carrying the extension is reachable, and names the platform permission each operation costs (observation:read, observation:write, observation:delete). A profile without it is not reachable at all, so a newly profiled resource stays closed until someone decides otherwise. 29 resource types are exposed, Composition among them, since an encounter note is core clinical content for an app. The seven experimental profiles (Communication, DetectedIssue, MessageHeader, Provenance, QuestionnaireResponse, Specimen, Subscription) are deliberately left closed. Six of the 29 are exposed read-onlyPractitioner, PractitionerRole, Organization, Location, Schedule and Slot — because they are reference and directory data the EMR maintains rather than something a third-party app should rewrite, and a delete there is data loss rather than a workflow step.

    For app authors this is the answer to "what do I ask for": every resource page now carries a SMART tab listing the scope strings that work against that resource and the permission each one resolves to. The permission tokens are referenced here, not defined: they belong to the EMR's own role model (the same tokens an administrator sees when building a role), and code generation fails if the IG names one that does not exist there. SMART's scope grammar itself (the c r u d s interaction letters, .read/.write folding, wildcard expansion) is deliberately not modelled in the IG: translating it is the gateway's job, and the IG answers only "is this exposed, and what does each operation cost". (FHIR-28)

  • TypedFhir 0.8.0 surfaces SMART exposure in all three ports and now publishes as a NuGet package. SmartExposedResources, IsSmartExposed() and GetSmartScope() (snake_case in Python) are generated from the smart-exposed extension, so a consumer asks the library what an app may reach rather than transcribing the answer. Generation refuses to emit a table naming a permission the platform does not define, and a test asserts the same on every CI build, which is what keeps the IG's published permission tokens and the EMR's role model from drifting apart. The C# port ships as Well.Services.TypedFhir with an optional Well.Services.TypedFhir.Firely adapter for hosts that hold Firely POCOs; the core takes no Firely dependency. (PSD-721)

Fixed

  • effective-date is now a declared extension. 38 profiles stamped https://fhir.apps.health/StructureDefinition/effective-date via the EffectiveDate / NoEffectiveDate RuleSets while nothing defined it, so the canonical resolved to nothing and the IG Publisher objected on every single use. The declaration changes no stamped data; it only makes the URL resolve, and adds the constraint the RuleSets always implied but could not state: a profile declares either which paths carry its date (26 do) or that it deliberately has none (12 do), never both and never neither. Its sibling salient-field was declared all along, which is what made the gap visible. (FHIR-27's open item, closed here alongside the new smart-exposed.)
  • 18 CodeSystem and ValueSet ids no longer contradict their own canonical URLs. Each carried a cs- / vs- prefix its URL did not (cs-nexus-emr-province-state against .../CodeSystem/nexus-emr-province-state), which the IG Publisher rejected three times over per resource — 54 errors. The id was corrected to match the URL, never the reverse: a canonical URL is the published contract and every existing binding and reference points at it, while the id is local and the resource type already disambiguates a CodeSystem from a ValueSet of the same name. No canonical URL changed, so nothing downstream needs updating. (FHIR-28)
  • A second, dead copy of four FSH files is gone. doc/fhir/input/fsh/ held duplicates of the province-state CodeSystem and the gender-identity, pronouns and province-state ValueSets. The build reads src/fsh/ (staged into .build/ by bin/build.sh), so the copies shipped nothing and nothing referenced them — but they had already silently rotted: their titles were the pre-rename originals, so anyone reading or editing them was working from a stale file that looked authoritative. (FHIR-28)
  • The Invoice example's practitioner reference resolves. It pointed at Practitioner/dr-navarro where the example's actual id is dr-emil-navarro, so the reference dangled in every validation run. (FHIR-28)

[1.14.0] - 2026-08-09

Added

  • Item consent (nexus-emr-core-item-consent) — a patient-level Consent profile for the Patient Profile's Consent section: one Consent per (patient, item) for the fixed phase-1 catalogue (email, SMS, AI scribe — the new nexus-consent-item CodeSystem/ValueSet), updated in place with the decision read from status alone (active = provided, rejected = declined, no resource = not recorded) and the full decision history served by FHIR versioning (_history/vread); includes the optional nexus-consent-status-reason extension, evidence via sourceReference to a NexusEmrCoreDocumentReference (the Attachment choice is removed from source[x] so evidence stays behind DocumentReference read permission), examples, and the item-consent feature guide. Unlike its siblings the profile carries no raw-identifier invariant — an item consent is born in the EMR, so there is no source system to point back at and the rule could only ever be satisfied by inventing one — and instead warns (item-consent-status-is-defined, item-consent-performer-is-attributable) where the prose asks for more than cardinality and bindings can express: a status outside the three codes it defines, or a decision with no attributable performer.display. The guide adds the migration caveats this model creates: version history does not survive a bulk export/import and must be replayed per version, and the deterministic identifier embeds the Patient's logical id, so re-keying patients means recomputing every one. (PSD-501, epic PSD-316)
  • Composition is now the model for authored clinical notes — encounter notes, consult notes, telephone notes: anything an EMR user writes or an AI scribe drafts and a clinician signs. The narrative lives in FHIR as XHTML in section.text, so a note is readable, searchable and diffable without dereferencing a blob, and Composition.status carries the lifecycle (preliminary draft → final signed → amended) instead of an invented extension. Amendments are version history over one resource — edit in place and set status to amended; the business identifier does not change and relatesTo is deliberately left unprofiled so it stays available if a true addendum workflow later appears. A signed note must name its signer (composition-signed-has-attester). author admits a Device, so an AI-drafted note can say so on the resource, while attestation remains the clinician's alone. Two examples ship: a signed progress note, and a scribed telephone note that was signed and then corrected. Decided on the August 2026 SMART-launch review. (FHIR-27)

    This re-introduces a profile removed four days earlier, deliberately. bde96cee4 (1.11.0) deleted an experimental NexusEmrCoreComposition on the grounds that the surface was "experimental and unused, and publishing it invited implementers to build against a shape the IG was not committing to." That reasoning was sound and is now spent: there is a committed consumer. What returns is only the encounter-note subset — not the four-section-type (text/form/canvas/file) surface with recursive containment and coordinate annotations, whose removal also cleared two unevaluable-slicing defects. section.code is left unbound for the same reason: no producer populates it yet.

    Known dependency, stated plainly. The amendment model rests on version history, and version history is server-local — _history does not travel in bundles, $everything, or sync pipelines. This IG also publishes no CapabilityStatement, so no versioning policy is declared anywhere, and meta.versionId is server-opaque in R4 and unconstrained here: consumers must not sort or parse it to determine which version was signed. Reconstructing the as-signed note is reliable only on the authoring server today. Declaring the version-history contract is outstanding work and a prerequisite for relying on this across boundaries.

  • IG HTML publishing is scriptedbin/publish.sh renders the IG to browsable HTML and optionally zips it for sharing (-full / -slim, both labelled so neither can be mistaken for the other or overwrite it). This is release step 6 of the IG Owner Release Process, which until now said only "note the deferral". Also establishes bin/ as the home for IG maintenance scripts, with the conventions written down. (FHIR-27)

Changed

  • DocumentReference narrows to attached binary documents — scans, faxes, external PDFs, Word files, consent forms: content that arrives as bytes. It is no longer the home for textual encounter notes (see Composition above), and the profile now states the boundary rule explicitly: did we author it, and is the content text we control? A consult letter received as a PDF stays here; a consult note typed in the EMR does not. This is a deliberate divergence from US Core, which models clinical notes as DocumentReference; the rationale is recorded on both profiles. (FHIR-27)
  • DocumentReference content documentation now matches what the gateway actually does. The profile described content as inline-base64-first and claimed external URLs were unsupported — both untrue since the gateway grew document storage, so implementers were reading a contract the runtime no longer honoured. Now documented: submit inline base64 with no url; the gateway stores the bytes and rewrites the attachment (sets url, computes size and SHA-1 hash, clears data), so readers get a URL and no inline data. The URL is an absolute File Storage Service address, not a FHIR Binary reference and not a pre-signed/SAS link — fetching it needs the caller's own bearer token and resolves within the caller's tenant. It is stable for the life of the document and survives updates. Sending data and url together bypasses storage and persists the base64 inline permanently; the profile now warns against it. Client-supplied URLs are host-validated and rejected with a 400 when untrusted. (FHIR-27)
  • Removed the prose salient accessor from DocumentReference. It decoded content.first().attachment.data and offered it to chart and model readers as long-form text. Wrong on two counts once the scope narrowed: the content is now a PDF or TIFF, so decoding it yields binary rather than prose; and the gateway clears data on write, so it resolved to empty for every gateway-ingested document anyway. Replaced with title and url. The narrative reader surface is Composition.section.text. (FHIR-27)
  • Dropped the boilerplate "Future Considerations" sections from 16 profile intro pages — speculative "potential enhancements being evaluated" lists that read as commitments the IG had not made. 156 lines removed; no profile, element, or binding changed. (FHIR-27)
  • The tree is restructured around src/ and a contained build. FSH now lives in src/fsh/ (was input/fsh/), examples in src/examples/, per-profile narrative in src/pages/, and the IG configuration (sushi-config.yaml, ig.ini, ignoreWarnings.txt) in src/ig/. The FHIR toolchain's hard-coded conventions no longer shape the tree: bin/build.sh stages src/ 1:1 into the gitignored .build/ and runs SUSHI (and, at publish time, the IG Publisher) in there, so all generated output and tool scratch (fsh-generated/, temp/, template/, input-cache/, the rendered site) is contained in one disposable directory. conformance/run-conformance.sh folded into bin/conformance.shbin/ is the single home for tooling. No profile, example, or canonical URL changed — this is a layout-and-tooling change only. (FHIR-27)
  • The IG is titled "Nexus IG" (was "Nexus EMR FHIR Implementation Guide") — nothing else in the Nexus ecosystem defines an IG, and the spec serves more than the EMR. The formal title, the site name (which now carries the version, e.g. "Nexus IG 1.14.0"), and site prose all use the short form. Identity is untouched: id: nexus-emr, the canonical base, and all profile slugs are unchanged. This CHANGELOG is now published on the site and linked from the front page. (FHIR-27)
  • Artifact display titles normalized — three authoring generations of title styles ("X Profile Core", "X Core", "X (Minimal) Core", unprefixed extensions) collapse to one pattern: "<X> Profile" with qualifiers trailing ("Slot Profile (Minimal)"), and no Nexus EMR/Nexus brand prefix (the canonical URL carries the namespace). 53 titles changed. Display-only: no id, URL, name, or element change; meta.profile values and references are unaffected. (FHIR-27)
  • The human-readable IG is now one MkDocs site, not the IG Publisher's HTML. bin/site.sh builds (or serves, with live reload) the guide plus a FHIR reference generated from the compiled IG — profile pages with differential tables, bindings, invariants and linked examples — by TypedFhir's new ig-docs renderer (@awaremd/typed-fhir/ig-docs). The IG Publisher survives as the package + QA factory (bin/publish.sh): validators and generators consume its package.tgz, published at the site root next to package-list.json; its Jekyll HTML is not published anywhere. Profile pages present the resource as five tabbed views — Differential, Key elements, Snapshot, FSH source, Examples — and example JSON is hyperlinked (references to other examples and canonical URLs are clickable, with "Referenced by" backlinks), so instances browse as a graph. The site carries the Nexus EMR frontend's WELLSTAR Stardust branding (blue-on-clay light, navy-clay dark, IBM Plex, the Nexus mark). Profile pages also carry a Salient fields view (the synthesized-accessor contract: key, type, FHIRPath expression) and a TypedFhir API view with TypeScript / Python / C# sub-tabs showing the generated facade for the profile's resource type. Extensions used by exactly one profile nest under that profile in the sidebar; only shared/cross-cutting ones remain top-level. Also fixed en route: sushi --snapshot . silently dropped the flag (0/51 snapshots) — the staged build now runs sushi build . --snapshot (51/51), which is what TypedFhir's vendored artifacts are regenerated from. (FHIR-27)

Fixed

  • Organization references in three Patient examples used a NamingSystem the Organization example does not own (.../cedarbrook-clinic-organization-identifier vs the organization-identifier system the Organization profile's invariant names) — the references were unresolvable as data. Normalized to organization-identifier. (FHIR-27)
  • Four declared-but-unwired extensions are now wired to their profiles. An extension audit found extensions the IG declared but no profile referenced: the App State string/boolean value carriers (the App State profile's entire payload surface — one was already used by the shipped example), Prevention Status (carried by both worked prevention Observation examples), and Patient Record Inactive Reason (declared intent for heritage record-status migration, referenced nowhere). Each is now an explicit optional slice on its profile — App State stringValue/booleanValue, Observation preventionStatus, Patient recordInactiveReason — so consumers can rely on the slice names and a reader can tell a decision from a leftover. None are must-support. (FHIR-27)
  • Appointment Modality's Encounter context removed — nothing consumes modality on an Encounter; extension contexts follow observed consumption. Re-add with evidence. (FHIR-27)
  • The IG had never been publishable as configured. ig.ini named ./nexus-emr.template, a directory the fork/rename from Polaris never created, so any attempt to render HTML failed on a missing template before doing any work. It now uses fhir.base.template#current. (FHIR-27)

1.13.0 does not exist, and never will

There is no 1.13.0. It was skipped deliberately, going straight from 1.12.0 to 1.14.0, for exactly the reason buildings skip the thirteenth floor: none. There is no technical reason, no withdrawn release, no aborted branch, and nothing to go looking for.

To be plain about it, since a changelog is the wrong place to be coy: we have no interest in the superstition itself. But a version string is not private. It gets read aloud in meetings, quoted in vendor email, pasted into conformance statements and printed in RFP responses, and somewhere along that chain is a reader for whom the number is a small distraction. Skipping one integer costs nothing. Explaining it every time costs more.

Recorded here so nobody reading the history later assumes a release was pulled, hunts for a lost branch, or "corrects" the gap. If you need it as a rule: version numbers ending in 13 are not used. The successor to 1.12.0 is 1.14.0; the successor to 1.19.0 will be 1.20.0.

[1.12.0] - 2026-08-05

Never released as a standalone version. 1.12.0 was authored and reviewed (PR #858) but never merged: the merge window fell during a GitHub Actions outage on 2026-08-06 (workflow runs failed to resolve their own actions and could not be made green), and rather than fight the platform the work was folded forward into 1.14.0. Everything in this section ships as part of 1.14.0. The section is kept for the record so the modelling history reads continuously; there is no 1.12.0 tag or published artifact.

Version note. This release contains changes that are technically breaking — two renamed invariant keys and two new required bindings — and is still numbered as a minor. That is deliberate and now written down: while the IG carries status: draft, a breaking change bumps minor and the migration note rides in the CHANGELOG entry instead. See IG Owner Release Process §4. The carve-out ends when status: leaves draft. Every break below is called out explicitly with the consumer action; that is the obligation the carve-out comes with.

Added

  • Seven new draft profiles, for the resource types this stack demonstrably reads or writes with no profile behind themDetectedIssue, Communication, QuestionnaireResponse, MessageHeader, Subscription, Specimen, and Provenance. All are #draft + experimental = true first passes: each declares its identifier contract, its effective date (or NoEffectiveDate), its patient/subject reference, and the constraints load-bearing for the surface already consuming it — and each names inline what it deliberately does not settle, so a reader can tell a decision from a placeholder. Highest-consequence of the set: DetectedIssue (the prescribing override audit trail, previously ungoverned), QuestionnaireResponse (the chart's journey timeline could not date it), and Provenance (a deliberate placeholder for AI-Harness action attribution — token/model accounting and the gateway UpdatedBy reconciliation are explicitly left open).

    Scope note for IG consumers: profiles are added for observed consumption, not for reachability. Types that are merely granted a SMART app scope, or merely traversed as a containment field, do not get a profile on that basis — the app scope is the thing that should narrow. Specimen qualifies on a narrower basis than the others and says so in its own notes: one real site resolves ServiceRequest.specimen references in eReferral bundles, and nothing reads a Specimen's own fields, so it is governed as a pass-through type. Still deliberately NOT profiled on the same rule: CarePlan, Goal, Coverage, MedicationStatement, ImagingStudy, Media, MedicationAdministration, Medication. (FHIR-25)

  • Heritage-EMR situations are now documented on the affected element definitions, and flagged as UNRESOLVED — so the caveat travels into the IG pages and the generated typed API rather than living in a reviewer's head. Covered: Patient.address.state / .country (heritage EMRs store province as unvalidated free text, so the new required bindings are an open migration risk — the distribution has not been measured); Specimen.identifier / .accessionIdentifier / .subject (heritage lab results carry an accession and little else, with no cross-lab namespacing for it and a patient link that is inferred rather than asserted); and Communication.identifier / .category (heritage message/inbox history is not FHIR Communication and whether to migrate it at all is undecided — every Communication in this IG is currently either WELL-native or external eReferral, never heritage-migrated). Each note says what is unknown and what would resolve it. (FHIR-25)
  • Provenance.agent.type is 1..1 — tightened from the base 0..1 to match US Core 9.0.0 us-core-provenance, which also sets it min 1. It is the only element distinguishing a non-human generating agent from the human who accepted its output, which is the whole reason the Provenance placeholder exists; two untyped agents are an ambiguous record. Note this IG deliberately does not adopt US Core's must-support on target.reference — that would require literal references, and the Nexus logical-reference model permits identifier-only. (FHIR-25)
  • New Patient example for the 1.12.0 identity surfacePatient/rowan-mercier-no-health-card exercises reasonForNoHCN (on the MRN identifier), genderIdentity and pronouns in their correct complex form, name.prefix, and both required address bindings. Added because the new must-support elements previously shipped with no conforming instance, and the complex-extension shape (extension[value].value[x]; the extension root's value[x] is 0..0) is the one contributors get wrong. (FHIR-25)
  • Reason for no health card number — Ontario's CA-eReC extension, ADOPTED not re-mintedPatient.identifier.extension[reasonForNoHCN], using http://fhir.infoway-inforoute.ca/io/CA-eReC/StructureDefinition/CA-eReC-reason-for-no-HCN-EXT. Ontario eReferral requires Patient.identifier to carry either the health card number or a documented reason for its absence; this is the "or" half, and it completes the PHN identifier work begun in #768. New IG dependency: ca.on.erec.r4 pinned at 0.12.6-TU-snapshot1 (exact pre-release pin — Ontario's own artifact is draft 0.10.1 and will move).

    Two things consumers should know. (1) The context is Patient.identifier, not Patient — so it rides on an identifier the patient does have, in practice the MRN slice, because in exactly the case it describes there is no jurisdictional identifier to attach it to. SUSHI propagates the element to the base identifier and to both the jurisdictional and mrn slices. (2) Ontario's value set holds one clinical code (#no-HC) plus data-absent-reason #not-asked / #asked-declined, so it cannot distinguish newborn-awaiting-registration from refugee-on-IFHP from card-not-presented; carry those specifics in .text, or as an additional coding from a CodeSystem the IG declares. A Nexus-canonical equivalent was drafted and withdrawn in favour of the jurisdictional canonical, so later Ontario/Union-IG conformance is re-badging rather than remodelling. (FHIR-25)

  • Encounter.appointment is now modelled — the AppointmentEncounter link, with the primary-care 1:1 expressed as a warning invariant plus prose rather than as cardinality. appointment stays 0..*: R4 permits several Encounters to reference one Appointment (a visit that splits into multiple encounters — routine for specialists) and one Encounter to fulfil several Appointments, so tightening the cardinality would foreclose both and have to be unwound. Documents that Encounter is the association anchor for clinical resources, and that R4 has no reverse element on Appointment, so that direction requires a search. (FHIR-25)

Changed

  • Address.state and Address.country are now bound — the only prior statement was non-computable prose ("valid province codes"), and the divergence it permitted is already realized in production: one writer emits ISO alpha-3 while the address UI emits alpha-2, consumers do positional string surgery on the result, and outbound jurisdictional HL7 has been observed carrying "Ca-io" and "CANio". state binds required to the new NexusEmrProvinceStateVS (bare two-letter subdivision codes, 13 CA + 56 US, mirroring the frontend roster); country binds required to iso3166-1-2 (alpha-2), with alpha-3 an edge form only. Both are deliberately required against this IG's usually-lenient posture, because a closed roster is the only thing that makes the corruption fail loudly; patient-address-country-alpha2 (warning) names the observed bug so the diagnostic survives a lenient validator. ⚠ Producers writing spelled-out province names or alpha-3 country codes become non-conformant.

    ⚠ Migration caveat, carried in both elements' definitions so it reaches the IG pages and the generated typed API: these bindings were validated against the IG's own examples only — legacy inbound source-EMR data has not been surveyed, and source systems emit uncoded free-text on adjacent address fields, so spelled-out province names are plausible in existing records. Implementers migrating a corpus SHOULD profile their state/country distribution before enforcing, and MAY treat the bindings as extensible during migration (the warning invariant still reports the alpha-3 case). The caveat comes out of the definitions once the survey is done. (FHIR-25)

  • genderIdentity and pronouns now carry bindings — the IG adopted both HL7 extensions and bound neither, leaving real terminology governance in frontend TypeScript on must-support elements. genderIdentity binds required to NexusEmrGenderIdentityVS, deliberately broader than the HL7 value set it inherits: 10 of its 13 SNOMED concepts are Canada Health Infoway namespace (pan-Canadian GSSO) and are not members of hl7.org/fhir/ValueSet/gender-identity. pronouns binds extensible to NexusEmrPronounsVS (LOINC answer list), since pronoun sets are open-ended. Note both are complex extensions: the bindings sit on extension[value].value[x], not on the extension root, whose value[x] is 0..0. Absence reasons remain a data-absent-reason extension on the value and are not value-set members. (FHIR-25)
  • No generated directory is version-keyed any more, so a release renames nothing. The generated registry subpackage moves from src/ig/generated/nexus_emr_v<major>_<minor>_<patch>/ to src/ig/generated/nexus_emr/ (and the Python twin), finishing what 1.11.0 started when it de-versioned the vendored ig/nexus-emr/ directory. Exactly one version is generated at a time, so the version in the path carried no information the artifacts did not already hold — and it forced a git mv on every release, which made git record a whole-directory delete-plus-add and drowned the real spec diff in a release PR. The registry selector key is unchanged (nexus-emr-v1.12.0); the version still travels in the generated file headers, in IG_META, and in that key. Consumers importing the generated modules by path must update the path once; consumers resolving through the registry are unaffected. (FHIR-25)
  • Three profiles documented a PractitionerRole requirement their own constraints had already droppedDocumentReference.authenticator read "Reference must be to a NexusEmrCorePractitionerRole" two lines above a constraint permitting both; DocumentReference.author said authors are "represented by NexusEmrCorePractitionerRole"; Patient.generalPractitioner said it "links to PractitionerRole". The same defect class 1.12.0 fixed in resource-relationships.md, and worse in effect, because these strings are baked into TypedFhir's generated FieldDoc and so mis-teach every consumer of the generated facade. Prose only — no constraint changed. (FHIR-25)
  • Practitioner is the primary provider identity; the Appointment provider participant is no longer PractitionerRole-shaped — this release relaxed every provider reference element to accept Practitioner alongside PractitionerRole, but Appointment was missed. The invariant appointment-participant-practitionerrole is renamed to appointment-participant-provider and now counts the provider participant regardless of which resource names them. The old rule was vacuous — a Practitioner/-shaped participant scored zero and passed — while reading as though PractitionerRole were required. Appointment.participant's definition and comment, and the createdBy extension's reference-target order and prose, now put Practitioner first. The walk-in example's provider changed from PractitionerRole/dr-navarro-familymed-role to Practitioner/dr-emil-navarro, so a Practitioner-shaped provider finally has a canonical example; the with-slot example deliberately keeps its PractitionerRole provider so both permitted spellings are exemplified. Consumers validating against the old invariant key must update it. (FHIR-25)
  • Schedule.actor now accepts a raw Practitioner as the providerSchedule was the only place a PractitionerRole reference was a hard requirement rather than a target-type constraint: schedule-actor-required demanded one of PractitionerRole / Location / HealthcareService, so a Practitioner-shaped Schedule — the shape a primary-care writer would naturally emit — failed outright. The invariant now accepts a provider under either spelling, and schedule-actor-practitionerrole-max is renamed to schedule-actor-provider-max, capping the provider actor at one whichever resource names them. actor's definition documents both. Consumers validating against the old invariant key must update it. (FHIR-25)
  • The "PractitionerRole (Primary) / Practitioner (Secondary)" guidance is correctedguide/patterns/resource-relationships.md said PractitionerRole was primary and that Practitioner was "always accessed through a PractitionerRole." That was the origin of the PractitionerRole-primary reading in this IG, and it kept being re-derived from that page after the model changed. Practitioner is now documented as the primary identity (it carries identifier[userId], per the SMART fhirUser precedent) and PractitionerRole as the tenant-scoped role binding — still required where the clinic/role relationship is the assertion, since Practitioner has no organization element. (FHIR-25)

Fixed

  • The validator rejected two shapes the IG itself models. Both were FALSE POSITIVES on conformant data — the worse failure direction, since a validator that rejects valid instances gets routed around. Both are fixed in all three ports (TS / Python / C#), and both were found by putting the new 1.12.0 profiles through the synthetic corpus.

    • A partially-expanded value set was enforced as if it were complete. The facade generator baked whatever codes it could enumerate and checked against them, so any code reachable only through an include it cannot expand read as "not in the value set". Concretely: Consent.category binds required to consent-category, which unions v3-ActConsentType — a filter-based (concept is-a _ActConsentType) include — so v3-ActCode#INFA was dropped from the enumeration even though it carries subsumedBy: _ActConsentType and IS a member. The IG pins exactly that code, so every conformant app-consent instance was reported as violating its own profile. A partial expansion is now treated like no expansion: the binding is reported as unverifiable rather than enforced against an incomplete list, matching what the runtime registry already did. Five bindings moved into the unverifiable set as a result (see invariant-skip-allowlist.test.ts) — the enforcement was never real, only the reporting changes.
    • A required primitive carrying only a data-absent-reason extension read as absent. The cardinality check counted values, not element presence, so the standard FHIR form — _occurrenceDateTime: { extension: [ data-absent-reason ] } with no value — failed required (min=1). That is the exact escape hatch this IG models on Immunization.occurrence[x], so the validator was rejecting the pattern the profile provides. Presence now counts the _-prefixed primitive-extension sibling (choice forms and repeating primitives included); value-shape and binding checks still see no value, which is correct. Deliberately scoped to the cardinality check — the shared FHIRPath navigator is unchanged, so exists() / count() semantics inside invariants are untouched. (FHIR-25)
  • Strict validation now checks datatype shapes — it previously checked none — the generated per-profile validate() (what the fixture gate, CI and harness ingest all call) verified cardinality, value-set bindings, fixed/pattern equality and the compiled invariants, and nothing about datatypes. So a string inside Quantity.value reported zero issues: valueQuantity was present so obs-6 was satisfied, a Quantity carries no bindable code so binding rules stayed silent, and nothing on value[x] was fixed. Shapes were checked in the accessor layer, but bulk validation never calls it. ELEMENT_RULES now carries each element's declared FHIR type (choice elements resolve theirs from the instance key, valueQuantity → Quantity) and the loop checks JSON type plus the primitive members of the common complex types, so Quantity.value, Period.start and friends are reached. New issue code: datatype-shape. Implemented once per port (datatype-shape.ts / datatype_shape.py / DatatypeShape.cs) and held in parity by the facade-validation vectors. Consumers should expect new errors on data that was always malformed — this reports it rather than introducing it. (FHIR-25)
  • FHIR primitive lexical forms are validated — "banana" was a valid dateTime — no ISO-format check existed anywhere in the validation stack: the accessor layer's dateTime test was "is it a string", and the bulk validator had no test at all. The R4 spec regexes for date, dateTime, instant, time, id, code, oid, uuid and base64Binary are now applied by both layers from one shared table, and integer-family types are range-checked (positiveInt >= 1, unsignedInt >= 0). This immediately caught two long-standing defects in the frozen vector corpus: DocumentReference.date is an instant, and two fixtures carried a date-only value — corrected to full timestamps. (FHIR-25)

[1.11.0] - 2026-08-02

Fixed

  • 12 examples were never validated against their profiles — their meta.profile named a canonical that does not exist (nexus-emr-emrdevice, nexus-emr-allergyintolerance, nexus-emr-appdevice, nexus-emr-appstate, nexus-emr-clinicalimpression, nexus-emr-diagnosticreport, nexus-emr-documentreference, nexus-emr-medicationdispense, nexus-emr-servicerequest, nexus-emr-document-bundle-core), all missing the -core infix the profile Ids actually carry. The validator cannot resolve an unknown profile, so it silently falls back to base FHIR: those examples reported "0 errors" while never being checked against the profile they advertise. Corrected to the declared Ids, so every example is now checked against the profile it claims. (FHIR-25)
  • CareTeam, Flag and RelatedPerson now declare an effective date — the EffectiveDate/NoEffectiveDate sweep covered 26 profiles but not the three added alongside it, so consumers fell back to guessing. All three declare NoEffectiveDate: they are parties and administrative assertions, not dated events. Without it, RelatedPerson derived birthDate as its effective date — the exact invented-date failure the ruleset exists to prevent. (FHIR-25)
  • identifier documentation no longer claims "required" on optional profiles — the FHIR-22 relaxation dropped identifier to 0..* on 26 profiles, but 21 still carried ^short/^definition text saying "at least one business identifier is required" (and DocumentReference documented the deleted invariant as normative: "At least one identifier MUST follow the Nexus EMR naming pattern"). Those strings render on the IG profile pages and travel inside the StructureDefinitions, so implementers saw "required" on an optional element. Reworded to "recommended but not required". ClinicalTask (1..*) keeps a floor, so its wording is unchanged. (FHIR-25)
  • Patient identifier slicing is now evaluable — the jurisdictional and mrn identifier slices now carry a patternCodeableConcept on identifier.type. The slice used a type pattern-discriminator but fixed only type.coding.* (grandchildren the matcher does not traverse), so every Patient instance failed slice evaluation in the HL7 validator. No change to what conforms; the slicing now actually validates. (FHIR-25)
  • Example instances corrected to validate cleanly — fixed pre-existing errors across the example corpus: Task reasonReference cardinality (0..1) + input/output type code systems + a task-code display; MedicationRequest identifier/timing/dose; Consent attachment size/hash; a stray Invoice line-item extension; a double-LOINC BP Observation; and the ROUTINE/WALKIN appointment display names. The full example set now passes the new conformance/run-conformance.sh gate: 50/50 instances, 0 errors. (FHIR-25)

Removed

  • The experimental clinical-document surface is goneNexusEmrCoreComposition and NexusEmrCoreDocumentBundle are removed from the IG, along with the extensions and value sets that existed only to serve them: nexus-emr-markdown-content, nexus-emr-embedded-section, nexus-emr-embedded-section-answer, nexus-emr-embedded-section-response, nexus-emr-canvas-annotation, nexus-emr-canvas-metadata, nexus-emr-file-metadata, and the nexus-emr-embedded-section-types / nexus-emr-canvas-annotation-types / nexus-emr-coordinate-systems / nexus-emr-file-categories value sets. The clinical-document guide pages and the document-bundle example go with them. The surface was experimental and unused; keeping it published invited implementers to build against a shape the IG was not committing to. Clinical documents remain representable through DocumentReference. Profiles drop 31 → 29, extensions 22 → 15, value sets 17 → 11. (FHIR-25)
  • NexusEmrCoreRosterPanel (the FHIR Group-based panel profile) and its CPAR-specific panel/submission extensions (PanelFacilities, PanelConfirmationDate, PrimaryProviders, SubmitDuringNextWindow, AutomatedSubmission) — superseded by the primary-care-attachment model above.
  • The deprecated contained-Medication pattern: the NexusEmrContainedMedicationCore profile and the medicationReference / mandatory contained wiring on MedicationRequest.
  • The CPAR and CII feature areas (mapping guides + CPAR-specific FSH). These were dependent on the old Polaris estate; they can be reintroduced against the new model if needed.

Changed

  • ClinicalTask input/output remodelledTask.output is removed; Task.input is now the reference(s) to the chart element(s) the task is about (a report, requisition, prescription, problem, ...), with input.value[x] constrained to Reference (any resource). Non-reference input parameters are no longer carried (use note/description for free text). input.type is left as a free-text label for now (no controlled vocabulary bound). This resolves the undefined task-input/output-type code systems the examples had referenced. (FHIR-25)
  • Encounter: class binding corrected to the canonical ValueSet http://terminology.hl7.org/ValueSet/v3-ActEncounterCode (was a documentation HTML URL).
  • Identifier requirements relaxed across all profiles, in two ways: dropped the requirement for a Nexus EMR canonical identifier (removed the *-has-nexus-emr-id invariants), and dropped the cardinality floor. identifier is 0..* MS on 26 of the 28 profiles that carry it: a business identifier is not required, and where one is supplied any system is accepted (no specific canonical is mandated). Two profiles deliberately keep a floor and are the exceptions to this bullet: ClinicalTask (1..*, since a task is tracked and audited by its identifier). (The experimental Composition was the other exception; it has since been removed from the IG.) Carrying a source/raw identifier is now recommended (the *-has-raw-identifier invariants softened from #error to #warning; use the most appropriate system URI for the source system).

Added

  • Per-profile effective-date declaration — every profile now declares, via the new EffectiveDate(paths) / NoEffectiveDate RuleSets, which element carries the one effective date for that resource type (onsetDateTime | recordedDate for Condition, period.start for Encounter, and so on), or that it deliberately has none. It rides on the StructureDefinition as an effective-date extension. A resource has exactly one date concept and which element that is differs per resource type, so it is declared in the IG rather than guessed by each consumer; readers that previously inferred a date from whatever date-typed element they found should read the declaration instead. (PSD-322)
  • Examples exercising the release's new must-support elementsname.prefix / name.suffix on the Patient and Practitioner examples, and two new Patient examples for the new attachment-status codes: nexus-emr-patient-not-enrolled-example and nexus-emr-patient-fee-for-service-example. (FHIR-25, FHIR-26)
  • Primary-care attachment status: new codes + extensible binding — added not-enrolled (NE, a decided "not rostered", distinct from pending and from an absent attachment) and fee-for-service (FS, seen FFS / not rostered) to NexusEmrAttachmentStatus, and relaxed the status binding from required to extensible so source-EMR clinic-custom statuses (e.g. OSCAR customizations) can be carried without a breaking change. No other escape code is defined: the extensible binding lets a source system carry its own code directly, which preserves the original value instead of flattening it to other. attached retained (Alberta shared panels). Consumer guidance in the rostering guide updated for the expanded enum. (FHIR-26)
  • Provider references accept Practitioner, not only PractitionerRole — every IG element previously constrained to only Reference(NexusEmrCorePractitionerRole) (Patient.generalPractitioner, Immunization.performer, Encounter.participant, MedicationRequest.requester, DocumentReference.author/authenticator, ClinicalTask.owner/requester, primaryCareAttachment provider) now also permits NexusEmrCorePractitioner, and the multi-target AppState.subject and MedicationDispense.performer.actor lists gained Practitioner too — realigning with base R4 (which allows a bare Practitioner) and matching how source EMRs reference providers by person. (FHIR-25)
  • Appointment authoring (createdBy / created) — a new createdBy extension on Appointment records the authoring agent (a deliberately broad target set: PractitionerRole | Practitioner | Device | Patient | RelatedPerson | Organization, so user-created, self-booked, and system-created appointments are all representable), and native Appointment.created is now must-support for the creation timestamp. Participants continue to model attendance, not authorship. (FHIR-19)
  • Name prefix/suffix on Patient and Practitionername.prefix and name.suffix are now explicit must-support subfields (standard R4 HumanName), so honorifics (Dr., Mr., Ms.) and post-nominal credentials (MD, RN, NP) have a first-class home rather than being folded into given/family or text. (FHIR-25)
  • Contribution guidelines — a two-track contributor / IG-owner model is now documented (docs/guide/contributing/), referenced from README.md and conventions.md, and encoded for agents in doc/fhir/CLAUDE.md: contributors edit FSH only; derived artifacts (compiled resources, TypedFhir types, synthetic corpus) and version bumps are regenerated once by the owners at release. (FHIR-25)
  • Prevention status extension (nexus-emr-prevention-status-extension) on Observation only — carries the Nexus Preventions product status (completed | completed-externally | refused) where Observation.status's required binding cannot distinguish completed-externally from completed nor represent refusal-as-declined; Immunization is deliberately out of context (natively representable via status + primarySource/reportOrigin/statusReason), and native entered-in-error overrides the extension. Includes the status CodeSystem/ValueSet (draft/experimental). The FHIR-25 review round added worked prevention examples (PAP completed-externally, smoking-cessation refused, and a catalog-coded COVID Immunization) and clarified that a prevention is marked on code.coding under the prevention-catalog NamingSystem, not by category. (PSD-465, epic PSD-303; modelling review FHIR-23)
  • NamingSystem nexus-emr-prevention-catalog — declares the EMR-local code system identifying prevention catalog entries (the Preventions module's grouping key on Immunization.vaccineCode / Observation.code rawCode codings). Codes are app-catalog-owned and deliberately not enumerated in the IG; no Health Canada governance implied. SNOMED CT CA / DIN slices are a phase-2 commitment. (PSD-465; FHIR-23)
  • Appointment modality extension (nexus-emr-appointment-modality-extension) on NexusEmrCoreAppointment (and Encounter) — carries the delivery channel (in-person | video | telephone | secure message) as a distinct axis from appointmentType (booking style), since one CodeableConcept cannot hold both. Value is CodeableConcept (raw source code + normalized Nexus code co-exist); binding is extensible so a pan-Canadian/Ontario code can be added as a parallel coding without a breaking change. Includes the modality CodeSystem/ValueSet and a telephone example. Draft/experimental. (FHIR-21)
  • Primary Care Rostering / Attachment modeling proposal + FSH (docs/guide/features/rostering/primary-care-attachment.md) — a Patient-level primaryCareAttachment model chosen as the IG's rostering direction. The FSH has shipped (PrimaryCareAttachmentExtension.fsh); the status enum is expanded in FHIR-26.
  • Standards Alignment guides (docs/guide/alignment/): HALO IG alignment, the Union IG (what it is + the pan-Canadian stack it consolidates), and a WellServices R4 FHIR-consumption audit — 29 findings across R1-R6 shape mismatches plus a new R7 "un-modeled surface" pass (off-canonical WELL extensions/code systems). Point-in-time references; profiles may have changed since.
  • Care Providers & Relationships modeling: two new profiles — NexusEmrCoreCareTeam (a patient's care providers: physician/MRP, nurse, midwife, referring physician) and NexusEmrCoreRelatedPerson (an alternative contact — a person the practice can reach on the patient's behalf, with a personal relationship, contact purposes, contact details, a note, and an optional primary/call-first flag). The published nexus-emr-primary-care-attachment extension is extended in place (nothing renamed or removed): the attachment-status code system gains not-enrolled and fee-for-service (OSCAR roster_status coverage), and terminationReason now binds (extensible) the new nexus-emr-roster-termination-reason value set. Official codings are used wherever an official set covers the need: CareTeam.participant.role binds (extensible) to the FHIR participant-role value set (SNOMED CT), with v3 ParticipationType REF for the referring physician the set has no concept for, and the alternative contact's relationship draws from the official relatedperson-relationshiptype value set (v3 PersonalRelationshipRoleType plus v2-0131 O for Other, with v3 RoleCode GUARD covering legal guardian). Local terminology is limited to sets with no official equivalent — nexus-emr-roster-termination-reason (Ontario MoHLTC enrolment-report reasons, ministry numeric codes recorded per concept), nexus-emr-contact-purpose (dual-coded with v2-0131 / v3 RoleCode / v3 RoleClass where an official concept exists), and nexus-emr-patient-record-inactive-reason (moved / transferred / duplicate, bound extensible; a death stays on the native Patient.deceased[x]) — plus extensions (nexus-emr-contact-primary / nexus-emr-contact-note on RelatedPerson, nexus-emr-contact-point-phone-extension on ContactPoint, and nexus-emr-patient-record-inactive-reason on Patient). Backs the Patient Profile Care Providers and Relationships sections.
  • Notes modeling: new NexusEmrCoreFlag profile — an administrative "sticky note" about a patient, Flag being FHIR's official home for patient notes. The Patient Profile Notes section stores its two singleton free-text fields (booking alert, patient note) as one active Flag each, discriminated by the new nexus-emr-patient-flag-type code system on Flag.code with the free text on Flag.code.text; the category is the official flag-category#admin. Flags are never edited in place or deleted: a text change retires the current Flag (status = inactive, text kept) and creates a replacement, a cleared field retires without one, and readers filter to active flags — a field's history is the chain of its retired flags. No extensions.
  • Must-Support / terminology gap analysis vs the Ontario / pan-Canadian stack (CA Core+, PS-CA, OPS, CA:eReC) — the dual-conformance edge obligations (meta.profile, Ontario identity, tightened coded-element bindings). Point-in-time; target specs are draft/ballot. (The analysis itself moved out of this repo in 1.15.0 — it described what this IG would need in order to claim dual conformance, which is internal planning material rather than spec guidance.)
  • primaryCareAttachment extension on Patient (nexus-emr-primary-care-attachment) — the FSH for the rostering model: providers[provider, confirmedAt], status, effectivePeriod, consentStatus, terminationReason, externalIdentifier, jurisdictionalStatus, plus the attachment-status CodeSystem/ValueSet and an example. Supersedes the removed Group-based RosterPanel.
  • Patient mrn identifier slice — Medical Record Number / chart number (identifier type MR, HL7 v2-0203; system .../NamingSystem/patient-mrn).
  • Promoted ClinicalTask from experimental to a first-class profile (trial-use), with an example and intro page. Straight promotion — all clinical-safety and MERT-compatibility constraints retained (owner/for/requester/authoredOn/ description required, intent fixed to order, status/priority bindings, and the full MERT Must-Support set); the identifier requirement is relaxed in line with this round's IG-wide identifier relaxation (no mandated canonical Nexus system), but unlike most profiles it keeps the 1..* floor — a task still needs at least one identifier, from any system.

[1.10.0] – 2026-07-23

Inception as a fork of the Polaris FHIR IG (Well-Polaris/fhir-polaris-spec @ 12f75cd8 — the dev/stardust-integration tip, i.e. Polaris 1.10.0 plus the unreleased constraint work below). Canonical base https://fhir.apps.health retained (WELL owns apps.health). Content inherited unchanged: 30 profiles, 18 extensions, 9 value sets, 3 code systems.

Added

  • conventions.md — PR review checklist / Definition-of-Done, seeded from Ryan Watson's Polaris conventions.md.

Changed

  • Renamed polarisnexus-emr throughout: profile names (PolarisCore*NexusEmrCore*), canonical slugs (polaris-core-*nexus-emr-core-*), FSH aliases, invariant ids, file names, and example instances.
  • Reworked identifier/code enforcement from unevaluable slices to portable FHIRPath invariants (the "evaluable-slicing" convention, inherited from dev/stardust-integration and never released in Polaris): has-raw-identifier required (#error) on all top-level profiles; Appointment raw-code shape invariants for cancelationReason, reasonCode, serviceCategory, and serviceType (provide-what-you-have — codings optional, but code + display required when present).

Fixed

  • app-consent: corrected the Consent.category pin from the invalid consentcategorycodes#infa (a code that does not exist) to the valid v3-ActCode#INFA.

Removed

  • Inherited Polaris contributing/governance docs and the "Ask Fira" nav entry, in favour of the lighter maintainer-review + file-a-ticket model.