Troubleshooting
Common symptoms when integrating @cosyte/terminology, and how to read what the engine is telling
you.
translate returned unmapped: where's my code?
That is the engine working as designed. When a source concept has no target in the supplied map, the
result is a typed unmapped, never a guessed code:
const result = translate(coding, map);
if (result.unmapped) {
result.code; // "TERM_TRANSLATE_UNMAPPED"
result.mode; // "fixed" | "provided" | "other-map" | "none"
result.source; // your original coding, surfaced untouched
}
If the map declares a group.unmapped fallback, its mode (and, for fixed, fixedTarget, or for
other-map, otherMapUrl) is reported on the result, but never silently applied. You decide
whether to trust a fallback.
resolveSystem returned { unknown: true }
The identifier (URI, OID, or mnemonic) is not one the engine recognizes. It returns a typed unknown
rather than guessing a canonical URI. Check the value, and canonicalize known systems by their OID or
HL7 v2 mnemonic (LN, SCT, I10C, …).
loadConceptMap threw
loadConceptMap throws a TerminologyError carrying TERM_CONCEPTMAP_MALFORMED when the resource is
not a usable ConceptMap (wrong resourceType, a target missing its equivalence, and so on). Catch
it and branch on err.code. This is deliberate: a structurally broken map is refused rather than
loaded partially and translating some codes while silently dropping others.
Can I translate a target code back to its source?
Not through a forward map. Steward maps are directional and non-invertible, so translate only
matches the map's source side. Reverse translation needs an explicit inverse ConceptMap.
validateCodeInValueSet returned undetermined: is my code a member or not?
The engine could not prove membership either way, so it refuses to guess. This happens when a part
of the value set cannot be evaluated: an intensional filter/system include whose code system you
did not pass in the ExpansionContext, a referenced value set that is not supplied, an unimplemented
filter operator (regex / generalizes), a truncated pre-computed expansion, a pre-computed
expansion the server marked unclosed, or a component that pins a version the CodeSystem you
supplied for that system does not agree with. The result carries
code: "TERM_VALUESET_CANNOT_EXPAND" and a diagnostics list naming each gap.
const r = validateCodeInValueSet(coding, valueSet, { codeSystems });
if (r.undetermined) {
r.diagnostics; // each carries a stable code and a `path` into your own compose
}
Supply the missing CodeSystem (or referenced ValueSet) in the context and re-check. A truncated
server expansion must be re-fetched in full: the engine never treats a truncated snapshot as complete
membership (a false "not a member" is a clinical error). An unclosed expansion cannot be re-fetched
into completeness at all: the marker says the value set is unbounded because it includes
post-coordinated content (SNOMED CT, UCUM), so codes beyond the listed ones may be valid and only a
terminology server that understands that content can decide the ones it did not list. A code that IS
in such an expansion still validates true: the marker bounds what absence means, not presence.
For a version disagreement, supply the release the value set actually pins: re-checking against the
release you already have is what the diagnostic exists to stop.
expand dropped a code my value set lists explicitly
A compose component that enumerates codes is admitted only where the evidence you supplied does
not contradict it. When you pass a CodeSystem for that component's system (and the component's
declared version, if it has one, agrees with that release), a code the release does not define is
not a member: expand leaves it out of contains and reports one
TERM_VALUESET_ENUMERATED_CODE_UNDEFINED diagnostic on that component, and
validateCodeInValueSet decides it a non-member rather than a member.
The result is still complete: true, so this is a decided answer rather than a lower bound, and
a complete expansion can carry diagnostics: read diagnostics on both.
const r = expand(valueSet, { codeSystems });
r.complete; // => true
r.diagnostics.filter((d) => d.code === "TERM_VALUESET_ENUMERATED_CODE_UNDEFINED");
// each carries a `path` into your own compose, such as "compose.include[0]"
If the release you passed is the incomplete party rather than the value set, supply the release the
value set was written against: the rule reads the evidence you hand it and nothing else. If you want
the enumeration honored as written, do not pass a release for that system, or pass one whose
version the component does not pin to: with no usable release the engine holds no contrary
evidence and every enumerated code is carried, exactly as before.
expand returned complete: false
The contains set is a lower bound, not exhaustive membership: some intensional part could not be
computed (see above). Read each diagnostic's path ("compose.include[2]", "expansion") to
find the part that did not resolve in your own resource; do not treat the partial contains as
the whole value set.
Diagnostics and logs
The message, detail and reason fields are value-free: nothing you configured and nothing
your release or resource contained reaches one, or an err.stack, at any length. The locus beside
them is a line number, an index path into your own resource, or a fixed token: never a URI or a
column name. String(err) is safe.
The objects those codes ride on are not the same thing, and this is the part to get right. A
TERM_CODE_UNKNOWN, TERM_TRANSLATE_UNMAPPED, TERM_CROSSWALK_UNMAPPED, TERM_RXNORM_UNKNOWN_RXCUI
or TERM_RXNORM_NDC_UNMAPPED outcome names what you asked about (input, source, rxcui,
ndc, coding) because that is how a never-fabricate answer says which thing it refused to guess.
A loaded model likewise carries the displays and canonical URIs from your release. So
JSON.stringify(result) into a log is a decision about PHI, and a code in patient context can be
PHI: log the code and the locus, log a value only if you would log the code you passed in, and
never the surrounding record.
Known limitations (this release)
Status:
@cosyte/terminologyships the code-system identity resolver, the ConceptMap$translateengine, the CodeSystem load layer with$lookup/$validate-code, the ValueSetcompose/$expand/ binding operations, UCUM validation, the crosswalk resolvers, and the RxNorm drug graph.
- BYO data:
$expandand binding operate over theCodeSystemreleases and referencedValueSets you supply in theExpansionContext; an intensional part with no supplied code system is a typedTERM_VALUESET_CANNOT_EXPAND, never a fabricated member. - An enumeration is checked against the release you supplied, where there is one: an explicit
conceptentry a usable release does not define is not admitted, reported as a typedTERM_VALUESET_ENUMERATED_CODE_UNDEFINEDon that component while the answer stayscomplete: true. Bounded to that case on purpose: with no release for the system, or a declaredversionthe supplied release does not agree with, the engine has no contrary evidence and every enumerated code is carried. - A declared code system version is checked, not selected on: where an intensional part resolves
a supplied release, a component's
versionis compared against that release's ownversion. A disagreement (or a release that declares no version to confirm the pin against) is a typedTERM_VALUESET_CANNOT_EXPANDlocated on that component, and its members are withheld or shipped without the unconfirmed version stamp: never membership computed from the other release. TheExpansionContextholds one release per system URI, so the engine cannot hold several releases and pick the one a component asks for. What is compared is that the two declarations agree, never that either is true: a mislabelled release is still trusted. - Subsumption is the release's own hierarchy:
is-a/descendent-ofread the loaded code system'sparentedges (nestedconcepts or an explicitparentproperty). Subsumption across two separate releases is not computed. - Intensional filters are best-effort:
is-a/descendent-of/is-not-a/=/in/not-in/existsare implemented;regex/generalizessurface asTERM_VALUESET_CANNOT_EXPAND. - RxNorm graph is BYO:
loadRxNormGraphoperates over the RxNorm RRF release you supply; the engine bundles no RxNorm release. NDC↔RXCUI resolution is release-scoped and carries the as-of release; obsolete/alien NDC statuses come from RxNav NDC-history data (a differential source), not the base RRF concept files, and are never fabricated. Approximate matching is opt-in and always labeled. AnRXCUIwhose supplied atoms could not establish a term type is left out of the graph and reported asTERM_RXNORM_UNTYPED_CONCEPTongraph.warnings, never loaded under a synonym'sTTY; check that list if a concept you expected reads asTERM_RXNORM_UNKNOWN_RXCUI. - No bundled code-system release: every code-system release is bring-your-own, including the content packs (RxNorm Prescribable, ICD-10-CM) and SNOMED CT / CPT / LOINC / UMLS / VSAC.
- What is bundled includes the UCUM unit table (
ucum-essence.xmlv2.2, copyright © Regenstrief Institute, Inc., reproduced verbatim under https://ucum.org/license; notice atvendor/ucum/NOTICE.md, which ships with the package), the code-system identity pairings, the SNOMED CT concepts the crosswalk resolver names: the map-category concepts and the two gender findings (SNOMED CT is copyright © International Health Terminology Standards Development Organisation), and RxNorm's relationship and term-type names (RELA,RELA_INVERSE,TERM_TYPES), published by the U.S. National Library of Medicine. Individual SNOMED CT, ICD-10-CM and RxNorm identifiers also appear in the API documentation examples. The full list, with its copyright, is in the package'sLICENSE. So the UCUM operations andresolveSystemwork with no release at all;$lookup,$validate-code,$expand,$translateand the crosswalk and RxNorm resolvers need one you supply.
The API Reference always reflects exactly what this release ships: treat it as the source of truth over any prose above.