/** * Expand a value set the terminology server refuses as the pieces it is composed of, and merge. * * Snowstorm will not expand a value set spanning SNOMED CT and another code system - it declines to * COUNT across two systems, not to answer - and will not apply a `descendent-of` filter to SNOMED. * Both refusals are about the PARENT, never the content: every child below expands 200 on its own. * Without this, the bound fields render as empty pickers, which look identical to "no matches". * * THIS IS NOT A RE-BINDING. Re-binding a clinical field to a narrower value set changes what a * clinician can record, which is a terminology decision rather than an engineering one. Every * substitution here reproduces the published value set's OWN definition - the same concepts, from * the same sources, computed a way this server implements. The meaning is unchanged. * * Each entry records the server's refusal verbatim and the parent version it was derived from. */ export type ExpansionSubstitution = { /** What the published value set is called on the server, so a reader can find it. */ readonly name: string; /** The version observed when this was derived - the thing to re-check if VSAC republishes. */ readonly observedVersion: string; /** The server's own words when asked to expand the parent. */ readonly refusal: string; /** Expand these instead, in order, and merge. */ readonly expand: readonly string[]; }; const VSAC = 'http://cts.nlm.nih.gov/fhir/ValueSet/'; /** * THE ONE WAY THIS GOES WRONG, stated so it is not discovered instead: these decompositions were read * off each parent's own `compose` at the version recorded below. **If VSAC republishes a parent with * different children, this table keeps expanding the OLD ones and nothing says so** - the field still * fills, with a stale set. There is no automated guard for that, because checking it means calling the * terminology server, and a test that reaches the network is a test that fails on a bad morning. * Re-read each parent's `compose` when US Core or VSAC moves version. */ export const EXPANSION_SUBSTITUTIONS: Readonly> = { // The "interpreter required" field, and the advance-directive documentation field // // The parent is a plain union of two enumerations: two SNOMED codes (Yes / No) and two // data-absent-reason codes (Unknown / Asked but unknown). Four concepts in total, and the server // expands each half without complaint - it simply will not put them in one bundle. // THE REFUSAL IS ABOUT PAGINATION, NOT ABOUT MEANING, which is why merging is faithful rather than // a shortcut: the server is declining to COUNT across two code systems, not declining to answer. [`${VSAC}2.16.840.1.113762.1.4.1267.16`]: { name: 'AnswerSetWithYesNoAndUnknowns', observedVersion: '20241005', refusal: 'This server does not yet support ValueSet$expand on ValueSets with multiple code systems ' + 'if any are SNOMED CT, because of the complexities around pagination and result totals.', expand: [ `${VSAC}2.16.840.1.113762.1.4.1267.15`, // YesOrNoSCT - 2 concepts, SNOMED `${VSAC}2.16.840.1.113762.1.4.1267.14`, // UnknownAndAskedButUnknownDAR - 2, data-absent-reason ], }, // The encounter TYPE field // // A DIFFERENT CAUSE FROM THE ONE ABOVE, and the two are easy to conflate. This parent does not // fail on multiple code systems; its SNOMED half fails on a FILTER OPERATOR: // "Unexpected operation 'descendent-of' for SNOMED CT ValueSet 'concept' filter." // `descendent-of` is valid FHIR R4 and this server does not implement it for SNOMED. What it does // implement is ECL, so the same definition is expressed as ECL and expands 200 with 98 concepts. // // `<` AND NOT `<<`, AND THE DIFFERENCE IS THE POINT. FHIR's `descendent-of` EXCLUDES the root // concept; `is-a` includes it. ECL `<` excludes, `<<` includes. Using `is-a`/`<<` would have been the // obvious translation and would silently add "Patient encounter procedure" and "Indirect encounter" // themselves to a clinician's picker as selectable encounter types, which the published value set // deliberately does not offer. // // THE HONEST GAP, and it is NOT caused by this substitution: the parent also unions // `EncounterTypeCPT`, and that child expands 200 with a total of ZERO - CPT is licensed content and // is not loaded on our server. So this field offers the SNOMED half whether or not the parent ever // expands, and it did so before this change too. Recorded here rather than left to be rediscovered. [`${VSAC}2.16.840.1.113762.1.4.1267.23`]: { name: 'EncounterType', observedVersion: '20250510', refusal: "Unexpected operation 'descendent-of' for SNOMED CT ValueSet 'concept' filter.", expand: [ // EncounterSCT (2.16.840.1.113883.11.20.9.52) states exactly this: descendants of // 308335008 |Patient encounter procedure| and of 185316007 |Indirect encounter|. 'http://snomed.info/sct?fhir_vs=ecl/(<308335008 OR <185316007)', ], }, [`${VSAC}2.16.840.1.113762.1.4.1099.30`]: { name: 'CareTeamMemberFunction', observedVersion: '20240605', refusal: 'This server does not yet support ValueSet$expand on ValueSets with multiple code systems ' + 'if any are SNOMED CT, because of the complexities around pagination and result totals.', expand: [ `${VSAC}2.16.840.1.113883.1.11.10267`, // v3-ParticipationFunction - 49 `${VSAC}2.16.840.1.113762.1.4.1099.27`, // SNOMED half - 633, via the ECL override ], }, // The allergy substance picker. THE BIGGEST ONE HERE: 826 concepts a clinician could not reach. [`${VSAC}2.16.840.1.113762.1.4.1186.8`]: { name: 'AllergyIntoleranceSubstance', observedVersion: '20240221', refusal: 'This server does not yet support ValueSet$expand on ValueSets with multiple code systems ' + 'if any are SNOMED CT, because of the complexities around pagination and result totals.', expand: [ `${VSAC}2.16.840.1.113762.1.4.1114.14`, // 107 `${VSAC}2.16.840.1.113762.1.4.1186.5`, // 7 `${VSAC}2.16.840.1.113762.1.4.1186.4`, // 15 `${VSAC}2.16.840.1.113762.1.4.1186.1`, // 563 - RxNorm, the bulk of it `${VSAC}2.16.840.1.113762.1.4.1186.3`, // 134 ], }, // Pregnancy status. 3 + 1. [`${VSAC}2.16.840.1.113762.1.4.1240.12`]: { name: 'PregnancyStatus', observedVersion: '20240221', refusal: 'This server does not yet support ValueSet$expand on ValueSets with multiple code systems ' + 'if any are SNOMED CT, because of the complexities around pagination and result totals.', expand: [ `${VSAC}2.16.840.1.113762.1.4.1099.24`, // 3 `${VSAC}2.16.840.1.113883.1.11.10612`, // 1 ], }, // Sexual orientation. 4 + 1 + 2. [`${VSAC}2.16.840.1.113762.1.4.1240.11`]: { name: 'SexualOrientation', observedVersion: '20240221', refusal: 'This server does not yet support ValueSet$expand on ValueSets with multiple code systems ' + 'if any are SNOMED CT, because of the complexities around pagination and result totals.', expand: [ `${VSAC}2.16.840.1.113762.1.4.1021.33`, // 4 `${VSAC}2.16.840.1.113762.1.4.1240.4`, // 1 `${VSAC}2.16.840.1.113762.1.4.1240.10`, // 2 ], }, // Smoking status codes. 2 + 2. Distinct from usCoreSmokingstatusOptions, which the terminology // server answers on its own through an ECL override - these are different value sets. [`${VSAC}2.16.840.1.113762.1.4.1267.6`]: { name: 'SmokingStatusCodes', observedVersion: '20240222', refusal: 'This server does not yet support ValueSet$expand on ValueSets with multiple code systems ' + 'if any are SNOMED CT, because of the complexities around pagination and result totals.', expand: [ `${VSAC}2.16.840.1.113762.1.4.1267.4`, // 2 - LOINC `${VSAC}2.16.840.1.113762.1.4.1267.5`, // 2 - SNOMED ], }, // The sex field. 4 SNOMED + 1 data-absent-reason. [`${VSAC}2.16.840.1.113762.1.4.1240.3`]: { name: 'Sex', observedVersion: '20230628', refusal: 'This server does not yet support ValueSet$expand on ValueSets with multiple code systems ' + 'if any are SNOMED CT, because of the complexities around pagination and result totals.', expand: [ `${VSAC}2.16.840.1.113762.1.4.1099.53`, // 4 - SNOMED `${VSAC}2.16.840.1.113762.1.4.1240.4`, // 1 - data-absent-reason, shared with gender identity ], }, // The gender identity field. 6 SNOMED + 1 data-absent-reason + 2 v3-NullFlavor. // // THREE HALVES, NOT TWO, and the third is easy to lose: v3-NullFlavor supplies `OTH` and `UNK`, // which is how a clinician records "other" and "unknown" here. Dropping it because two children // already cover the named identities would remove both, and the field would look complete. [`${VSAC}2.16.840.1.113762.1.4.1021.32`]: { name: 'GenderIdentity', observedVersion: '20240215', refusal: 'This server does not yet support ValueSet$expand on ValueSets with multiple code systems ' + 'if any are SNOMED CT, because of the complexities around pagination and result totals.', expand: [ `${VSAC}2.16.840.1.113762.1.4.1021.101`, // 6 - SNOMED `${VSAC}2.16.840.1.113762.1.4.1240.4`, // 1 - data-absent-reason `${VSAC}2.16.840.1.113762.1.4.1240.10`, // 2 - v3-NullFlavor ], }, // The route-of-administration field. 161 SNOMED + 125 NCI Thesaurus. // // The SNOMED half needed a server-side ECL override before it would expand at all - the // published child filters on `descendent-of`, which this server does not implement - and the // NCI half needed its code system loaded, because VSAC enumerates 125 NCI codes without // shipping anything that can validate them. Both are done; this makes the parent whole. [`${VSAC}2.16.840.1.113762.1.4.1267.22`]: { name: 'USCoreRouteCodes', observedVersion: '20241015', refusal: 'This server does not yet support ValueSet$expand on ValueSets with multiple code systems ' + 'if any are SNOMED CT, because of the complexities around pagination and result totals.', expand: [ `${VSAC}2.16.840.1.113762.1.4.1099.12`, // 161 - SNOMED, via the ECL override `${VSAC}2.16.840.1.113883.3.88.12.3221.8.7`, // 125 - NCI Thesaurus ], }, // Why an immunization was not given. 4 ActReason + 24 SNOMED. ['http://hl7.org/fhir/ValueSet/immunization-status-reason']: { name: 'ImmunizationStatusReason', observedVersion: '20190815', refusal: 'This server does not yet support ValueSet$expand on ValueSets with multiple code systems ' + 'if any are SNOMED CT, because of the complexities around pagination and result totals.', expand: [ // Published by our own terminology server precisely so this half can be requested. 'http://akashichealthrecords.com/fhir/ValueSet/immunization-status-reason-actreason', // is-a 310376006 -> ECL '<<' INCLUDES the root, which is what is-a means. Using '<' // here would silently drop the root concept the published value set does offer. 'http://snomed.info/sct?fhir_vs=ecl/<<310376006', ], }, // What a reference range applies to. v3-Race + three SNOMED concepts. ['http://hl7.org/fhir/ValueSet/referencerange-appliesto']: { name: 'ReferenceRangeAppliesTo', observedVersion: '20190815', refusal: 'This server does not yet support ValueSet$expand on ValueSets with multiple code systems ' + 'if any are SNOMED CT, because of the complexities around pagination and result totals.', expand: [ 'http://terminology.hl7.org/ValueSet/v3-Race', // 921 - already a real URL // Three enumerated concepts, expressed as an ECL disjunction rather than published as // a value set of our own: Male, Female, Pregnancy. Exactly the parent's three. 'http://snomed.info/sct?fhir_vs=ecl/(248153007 OR 248152002 OR 77386006)', ], }, }; /** * The URLs to actually ask for. Normally the one you were given. * * DELIBERATELY NOT A RETRY-ON-FAILURE. Falling back after a 400 would fire the doomed request on every * single load of the encounter form, and - worse - it would quietly paper over the NEXT value set that * starts failing for an unrelated reason. A declared substitution is one request path and one thing to * read. */ export const resolveExpansionUrls = (valueSetUrl: string): readonly string[] => EXPANSION_SUBSTITUTIONS[valueSetUrl]?.expand ?? [valueSetUrl]; export const isSubstitutedValueSet = (valueSetUrl: string): boolean => valueSetUrl in EXPANSION_SUBSTITUTIONS; type Concept = { system?: string; code?: string; display?: string }; /** * Merge the pieces back into one expansion, in the order they were requested. * * DEDUPED ON `system|code`, never on `code` alone. These unions cross code systems on purpose, and * two systems can and do use the same code string for different things - collapsing on the code would * silently drop a concept and leave a picker one option short with nothing to notice it. */ export const mergeExpansionContains = ( contains: ReadonlyArray, ): Concept[] => { const seen = new Set(); const merged: Concept[] = []; for (const list of contains) { for (const concept of list ?? []) { const key = `${concept.system ?? ''}|${concept.code ?? ''}`; if (seen.has(key)) continue; seen.add(key); merged.push(concept); } } return merged; };