Skip to content

FHIR IG Tools

A general, IG-scoped toolset built on the multi-IG fhir.* store, used to explore Implementation Guides, read profiles and terminology, perform terminology validation, and produce and validate FHIR R4 resources through skeleton-fill authoring.

Maintenance mode: while the admin console has the ig module in maintenance mode, all 22 tools of this group (including fhir_check_grounding / fhir_check_completeness) calls return an isError result immediately with error code MODULE_MAINTENANCE (error.message in English, error.message_zh in Traditional Chinese, error.module the module key), effective within about 5 seconds; the tools stay in tools/list. See the API reference.

Shared conventions

  • Every tool accepts an optional package_id + version. When package_id is omitted, the target package is chosen by this precedence, and provenance.resolved_by says which rule applied:
    1. explicit_version — a canonical argument or meta.profile pinned as url|version → the package holding that version;
    2. package_arg / latest — package_id was given (with version, that version; without it, the default IG's version, else the highest release — pre-releases only when no release exists);
    3. meta_profile / canonical_owner — the package that owns the meta.profile or the canonical argument (profile / ValueSet / system), manually-installed IGs first;
    4. default_ig — the default IG (isDefault);
    5. sole_manual_ig — no default IG and exactly one manually-installed IG. If none applies the call fails with NO_DEFAULT_IG (no arbitrary package is picked).
  • A |version is always honoured: a version that is not installed returns ARTIFACT_NOT_FOUND (the message lists the installed versions) — never a silent fallback to another version.
  • Responses use a shared envelope: {ok, data, warnings, provenance, error?}. provenance carries packageId / version / fhirVersion / source plus resolved_by; when the dependency closure holds several versions of one package it also carries closure_versions (which version canonical lookups use).
  • When declared dependencies of the target IG are not installed, data.deps_missing lists them as {package_id, version, required_by} (incomplete closure — results may be missing content).
  • List / search tools (fhir_list_artifacts, fhir_search_artifacts, fhir_list_resource_profiles) query only the default IG when package_id is omitted; all_packages=true queries every installed package instead (rows carry package_id / package_version; package_id is then ignored). Zero hits come with data.hint naming the other packages that match.
  • Compact output compact=true (fhir_list_igs, fhir_list_artifacts, fhir_search_artifacts, fhir_list_resource_profiles; default false, and the default output is unchanged) is for LLMs with a tight tool-output budget. Each type becomes an {id: title} map, the common canonical prefix is written once (canonical_base; canonical = canonical_base/<ResourceType>/<id>), and only canonicals that break that rule are listed in canonicals; profiles also list non-active ones in status; data.compact is a one-line reading guide. With all_packages=true the groups sit under packages (packageId#version → that package's group). fhir_list_igs returns default plus manual / dependency maps by install origin (packageId#version → title). Measured on TW Core: fhir_list_resource_profiles 12.9k → 4.8k characters, fhir_list_artifacts (50 rows) 19.9k → 2.8k, fhir_search_artifacts (20 rows) 8.6k → 1.5k, fhir_list_igs (58 packages) 19.6k → 3.7k.
  • For the module concepts and the suggested workflow, see FHIR IG Service.

IG discovery

Tool Description Key parameters
fhir_list_igs List installed IG packages (packageId / version / title / canonical / fhirVersion / status / isDefault / installOrigin (manual = installed by an operator, dependency = fetched for another IG's dependencies) / dependencies) compact
fhir_get_ig Details for a single IG: dependencies and artifact counts per resource type; data.deps_missing lists uninstalled dependencies package_id, version
fhir_list_artifacts List a summary of the IG's conformance artifacts resource_type, grouping_id, all_packages, limit (50 by default, 200 maximum), compact
fhir_search_artifacts Full-text search over artifacts by id / canonical / name / title / description keyword, resource_type, all_packages, limit (20 by default, 100 maximum), compact

Profile selection and reading

Tool Description Key parameters
fhir_list_resource_profiles List selectable resource profiles, grouped by the base resource type they constrain base_type, all_packages, compact
fhir_rank_resource_profiles Rank by how well source field keys match profile element paths (advisory only, selectionRequired:true) keys, base_type, limit (5 by default, 20 maximum)
fhir_get_profile Summary of a single profile (identity / base / derivation / element count), resolvable by id, canonical, or key; a canonical may be pinned as url|version (an uninstalled version is ARTIFACT_NOT_FOUND) identifier
fhir_get_profile_elements Read the profile snapshot; view = elements / element / slices / choices / binding / examples profile, view, path, slice_name, limit
fhir_get_profile_required_elements List the elements a profile effectively requires: required only when the whole parent chain is required, required slices, choice types, invariant-implied value[x]-or-dataAbsentReason (vs-2 / vs-3 / tw-core-4 style, detected from the expression), and target-implied primary content; each row carries grounding_class (auto / coding / source_fact / workflow_default …), required_by, allowedCodes for small required bindings and a fill hint; derived rows (e.g. Condition.category) also carry the binding's allowedCodes (extensible / preferred too, up to 20 codes) and a derivation rule (see "Derived elements and Must Support" below). Profiled extensions are expanded from their definitions (url, value[x] via ext-1, required sub-extensions), base-datatype children of populated elements are added (Narrative.div, Extension.url …), and profile-specific error invariants are listed under invariants. With instance, slices are matched by their discriminators, so absent slices add nothing profile, instance (draft: populated optional parents add their required children), include_target_implied (true by default), include_mustsupport (false by default; "defaults" = only the mustSupport elements with a fixed / default / derivable value; true = every mustSupport element; added rows carry optional: true), verbosity (compact by default: id / required_by / grounding_class / fixed·pattern·value / choices / allowedCodes / hint, profile-pinned subtrees folded into one copyable value; full adds min / max / types / binding / mustSupport)

Derived elements and Must Support

Elements with grounding_class: "derived" are not copied from the record but follow from other filled values. For well-known derived elements, hint starts with derive: and states the rule (each rule paraphrases the published definitions of the codes it names; the citation is derivation_source in verbosity="full"). A rule applies only when the element's binding is the ValueSet the rule describes:

Element Rule (summary) Source
Condition.category diagnosed or assessed in this visit (outpatient, admission, discharge) → encounter-diagnosis (and point Condition.encounter at that encounter); an entry of the longitudinal problem list kept across visits → problem-list-item; a diagnosis written in a visit note is encounter-diagnosis even when the disease is chronic FHIR R4 condition-category; US Core Condition Encounter Diagnosis / Problems and Health Concerns
Observation.category by what was measured: lab test → laboratory, BP / heart rate / temperature / height / weight … → vital-signs, imaging, exam, survey, social history … FHIR R4 observation-category; R4 Vital Signs profile
Encounter.diagnosis.use / .rank admission AD, discharge DD, chief complaint CC, comorbidity CM …; principal diagnosis rank 1, the rest in record order FHIR R4 diagnosis-role; Encounter.diagnosis.rank
MedicationRequest.category / MedicationStatement.category / courseOfTherapyType by the setting of the order (inpatient / outpatient / community / discharge); long-term continuous, short course acute FHIR R4 code systems
AllergyIntolerance.category / .type / .criticality, Observation.interpretation by the substance; immune vs non-immune reaction; life-threatening → high; against the reference range N / H / L / HH / LL FHIR R4 code systems

Derived elements without a rule get the CodeSystem definitions of the binding's codes in hint (pick the code whose meaning fits the record: …).

mustSupport is not required. include_mustsupport="defaults" adds only the mustSupport elements that need no source fact (a profile fixed value / pattern, a conventional default such as verificationStatus, a derivable value from the table above) and whose parents are all listed; true adds every mustSupport element. Rows added either way carry optional: true and a "may be left out, never refuse over it" hint, and guide says the same. The default (false) output is unchanged except for the derived rows.

Terminology / ValueSet

Tool Description Key parameters
fhir_get_valueset ValueSet definition summary identifier
fhir_expand_valueset Expand a ValueSet and list its member codes (resolved locally where possible; whole large external code systems are not enumerated — TOO_BROAD) identifier, limit (500 by default, 2000 maximum)
fhir_lookup_code 1.4.0 (v15.2: accepted_by_validator, defined_in, alternatives); Look up a single code's display name and properties in a CodeSystem (RxNorm from the local rxnorm.concepts); the response carries accepted_by_validator (whether fhir_validate_* accepts the code); when the code is not found, hint (use fhir_normalize_code); a code missing from the package's excerpt CodeSystem but defined in an installed full copy gets defined_in (package and name), reason (validation reports Unknown code) and alternatives (the concept's linked codings of other systems that the validator accepts, e.g. ATC); near_codes (similar spellings) only when no copy defines the code system, code
fhir_validate_code Validate whether a system+code belongs to a given ValueSet; returns valid / invalid / unverifiable; value_set may be pinned as url|version system, code, value_set
fhir_normalize_code 1.9.0 (v15.2: accepted_by_validator / reason, related accepted candidates first, alternatives); Free text (Chinese or English, abbreviations, chart wording) → candidate codes: SNOMED CT / LOINC / ICD-10 and any CodeSystem of an installed IG (e.g. the NHI payment codes medical-service-payment-tw, see IG Concept Search). IG candidates carry matched_term, matched_language, group/siblings and linked (codes of other systems the concept links to, e.g. a drug's ATC code); system may be a canonical, id or alias (nhi_payment). Drugs: Taiwan NHI drug codes are supported — system="medication-nhi-tw" (TW Core) or "nhi-medication" (TWPAS), text = brand or generic name in English or Chinese (Glucophage 500mg, 脈優錠 5mg); when the default IG ships only an excerpt of a CodeSystem (TW Core 1.0.0 medication-nhi-tw: 35 codes) and another installed package holds the full list (TW Core 0.3.2: 44,709 codes), the full copy is searched unless package_id / version is given (noted in warnings and data.codesystem_copies). ATC: no searchable WHO ATC is held (TW Core medcation-atc-tw has only 27 Taiwan-specific codes) — look up the NHI drug; its candidate's linked is its ATC code (from the bundled NHI medication supplement) Search and validation agree (v15.2): every candidate carries accepted_by_validator (+ reason when false), decided by the validator's own code check for the resolved package; codes only the full copy has are still listed (they identify the drug) but flagged false. When the best candidate is not accepted, accepted candidates related to it (same linked code, a shared rare word, same strength) move to the front (unrelated codes never do), and data.alternatives lists the codings of other systems those candidates link to that the validator accepts (e.g. {system: "http://www.whocc.no/atc", code: "C08CA01", display: "amlodipine", accepted_by_validator: true, value_sets: [...medcation-atc-tw]}, matching the MedicationRequest atc-medication-code slice); keep the drug name in CodeableConcept.text text, value_set, system, limit (10 by default)

Authoring, assembly, and validation

Tool Description Key parameters
fhir_get_resource_skeleton Generate an empty draft from a profile containing only required / mustSupport structure; required is the element's own min≥1, effectiveRequired also requires every ancestor to be required profile, candidate_limit (20 by default, capped at 100), include_examples (true by default)
fhir_finalize_resource 1.12.0 (v16 R4: structure repairs — empty arrays removed, resourceType inferred from a meta.profile string, a lossless reshape that only exposes existing errors is no longer withdrawn; validation as fhir_validate_resource 1.14.0); 1.11.0 (v15.4: record checks in validation as fhir_validate_resource 1.13.0 — empty shells); 1.10.0 (v15.3: record checks in validation as fhir_validate_resource 1.12.0); 1.9.1 (v15.2.1: as fhir_finalize_bundle 1.5.1); 1.9.0 (v15.2: alt-coding; validation as fhir_validate_resource 1.11.0); Finalise a draft already written as FHIR JSON against its profile and validate it: pins fixed / pattern values only where the draft has the parent node (slice members and _slice tags at any depth included), never adds clinical content; repair="content" (default) first applies the same content repairs as fhir_finalize_bundle (see "Content repair") and returns changes / repair_issues; with field values use fhir_author_resources (see "Authoring from field values") profile, draft, context_id, key, generate_narrative (true by default), repair (content / ids), timezone, site_config
fhir_resolve_reference Resolve resource references by temporary key (for linking inside a Bundle) key, resource_type, context_id, display
fhir_build_bundle Assemble several resources into a Bundle: a key becomes a deterministic UUID v5 fullUrl (the same input always yields the same Bundle), references written inside resources as key, urn:uuid:<key> or <Type>/<key> are rewired to the generated fullUrl, and an invalid fullUrl is normalised by the fhir_finalize_bundle rules. data.changes lists every rewrite, data.unresolved the references no entry matches (1.2.0) entries, bundle_type (transaction by default), context_id
fhir_finalize_bundle 1.8.0 (v16 R4: structure repairs completed — empty arrays (ele-1) removed, a missing resourceType inferred also from a meta.profile string, lossless reshapes (string → array, single ↔ array, one level up / down) no longer withdrawn when they only expose errors the bad shape hid; an inferred resourceType is withdrawn only by an unknown top-level property); 1.7.0 (v15.4: record_issues as the record checks of fhir_validate_bundle 1.16.0 — empty shells); 1.6.0 (v15.3: record_issues as the record checks of fhir_validate_bundle 1.15.0 — absence statements in text fields, text results where a number is needed, a provisional primary diagnosis); 1.5.1 (v15.2.1: alt-coding also for resources without meta.profile); 1.5.0 (v15.2: content repair rule alt-coding); Call before emitting the final Bundle: it normalises fullUrl, reference and id (see "Bundle finalisation" below) and, with repair="content" (default), corrects content errors whose fix is uniquely determined — never guessing, undecidable ones reported with candidates (see "Content repair"; repair="ids" = ids only); numbers keep the caller's spelling (10.0 stays 10.0). Returns the finalised bundle, every change as changes[{path, from, to, rule}], the references it could not decide as unresolved[{path, reference, candidates}], notes and id_map. bundle_type sets Bundle.type (a profile-fixed type is never overridden) bundle, package_id, version, bundle_profile (canonical or id of the target Bundle profile; defaults to meta.profile), bundle_type (R4 bundle-type code), repair (content default / ids), timezone (the site timezone: a UTC offset or an IANA name), site_config (facility settings text; a timezone: line gives the timezone)
fhir_validate_resource 1.14.0 (v16 R3, the slice / profile mismatch explanations of fhir_validate_bundle 1.17.0); 1.13.1 (v15.4 verification fixes, as fhir_validate_bundle 1.16.1); 1.13.0 (v15.4, as fhir_validate_bundle 1.16.0); 1.12.0 (v15.3, as fhir_validate_bundle 1.15.0); 1.11.1 (v15.2.1, as fhir_validate_bundle 1.14.1); 1.11.0 (v15.2, as fhir_validate_bundle 1.14.0); Validate a single resource (structure + terminology bindings); falls back to meta.profile when profile is omitted. An optional source_text adds a grounding check (see below) resource, profile, source_text, site_config, evidence_map, grounding, grounding_verbosity
fhir_validate_bundle 1.17.0 (v16 R3: slice / profile mismatch explanations — slice_match / profile_match: the closest slice / profile, the discriminator that failed and how, blocking (the first 5 errors that stop the match), plus set_profile / add_resource suggestions); 1.16.1 (v15.4 verification fixes: SNOMED Chinese-name comparison uses complete noun phrases only, a diagnosis that is an ancestor / descendant of the coded concept is never "something else" (a Chinese name with no exact ICD name relates through its head-final tail, e.g. 社區型肺炎 → 肺炎, to broader concepts only), the IG Chinese-name table loads in the background at startup; without source_text the "result-slot note says no result" rule is a warning); 1.16.0 (v15.4 record checks: empty shells — an Observation without value, a medication without structured dose / timing, a Condition without a code, whose side fields say not out / undecided / rejected / cannot tell apart: information-absent error when the record confirms it, else warning); 1.15.1 (v15.3 R3 follow-up: SNOMED codes compared with a Chinese diagnosis slot through data-derived Chinese names, see record checks); 1.15.0 (v15.3 record checks: information-absent also for text fields — CodeableConcept.text without / with placeholder codings, Dosage.text, valueString / note where the code needs a quantitative result; a provisional / differential / text-only primary Condition standing in for the primary diagnosis the record leaves unestablished → error "the record has no established primary diagnosis"; coded-element G-UNGROUNDED / G-VALUE-MISMATCH say what was compared, carry closest_record_phrase and become warnings without clear counter-evidence); 1.14.1 (v15.2.1: alternative-coding suggestions also for resources that declare no profile); 1.14.0 (v15.2: relatedness gate on Unknown-code suggestions, replace_coding / remove_coding alternative codings, ATC checked against the bundled tx.fhir.org table); Validate an entire Bundle (each entry against its meta.profile, plus reference integrity inside the Bundle). ID-class errors (UUIDs must be valid…, unresolved references, missing fullUrl, bdl-7, invalid id) carry suggestions and a Bundle-level id_map / id_changes; with normalize_ids=true the Bundle is finalised first and validated as normalized_bundle (see "Bundle finalisation" below). An optional source_text adds a grounding check and merges the results (see further below) bundle, normalize_ids (false by default), repair (ids default / content, with normalize_ids), source_text, site_config, evidence_map, grounding, grounding_verbosity

fhir_validate_resource / fhir_validate_bundle also report whether validation was complete: checked_fully (true when every profile and ValueSet was checked and the dependency closure is complete), profiles_unchecked, valuesets_unchecked. valid keeps HL7 semantics (an unresolvable profile is only a warning), so valid: true with checked_fully: false means part of the content was not checked. When definition / terminology lookups failed during the run because of database trouble, lookups_failed (the count) is added; such a run always carries an error issue with code: "exception" and valid is false (1.6.1). |version canonicals (meta.profile, binding, targetProfile, type profile, extension url) are resolved like HL7 validator_cli 7.0.1.

The builtin validator also checks, like HL7 validator_cli 7.0.1 (same release, 1.6.0):

  • Where an extension may be used: the extension definition's context (element / extension types; contexts restricted to other FHIR versions by version-specific-use are skipped) and contextInvariant. A violation is an error at the host element: The extension <url> v<version> is not allowed to be used at this point (this element is [...]; allowed for this version = e:...) or ... (based on context invariant '...'). fhirpath contexts are not evaluated locally (never an error).
  • An unversioned StructureDefinition resolves to the highest-version copy in the closure (e.g. the hl7.fhir.uv.extensions.r4 extension over the R4 core one with the same URL), so the extension's own invariants (e.g. var-1 of variable) are checked too.
  • Unrecognized property is judged against the core definitions for the whole resource (as the HL7 JSON parser does), including below a choice type the profile does not allow and inside the value of an unknown extension; like HL7, nothing is checked below an element with the wrong JSON shape (an array for a single element).
  • RxNorm: code and display are checked against rxnorm.concepts (tx.fhir.org accepts every SAB=RXNORM name of the RXCUI, case-insensitively): Wrong Display Name '...' for http://www.nlm.nih.gov/research/umls/rxnorm#<code>. Valid display is '...'. An unknown code is an error only when the local RxNorm release is not older than tx.fhir.org's (03022026), otherwise a warning; when only the preferred name is held (imported before migration 20261030) a display mismatch is a warning; without RxNorm loaded: warning Display not checked (RxNorm).
  • Unknown code in an IG CodeSystem with content = complete stays an error; when another installed package has a fuller copy of the same CodeSystem containing the code (e.g. TW Core 0.3.2 medication-nhi-tw), an information issue names that package and version.
  • Unknown-code suggestions (v15.2): a replacement code of an IG CodeSystem must be related to the original — a shared linked code (the same ATC), a shared rare word (an ingredient, not a dose form or salt) or a near-identical name, at the same strength; when none is related, none is given (no more Amlodipine → unrelated-drug suggestions). When the original code is defined in an installed full copy, no spelling-based near codes are offered either; instead replace_coding offers its linked coding of another system that the validator accepts and the element's bindings (coding slices included) allow (the patch replaces the Coding, text is kept), or remove_coding when the CodeableConcept already carries it (validator_cli rejects the CodeableConcept as long as the unknown Coding is in it).
  • WHO ATC (v15.2): ATC is not held locally; a bundled tx.fhir.org verdict table (node-server/data/terminology/atc-tx-check.json.gz, covering only the ATC codes of TW Core medcation-atc-tw and of the NHI medication supplement, no names) decides: a code ATC 2026 lacks gets validator_cli's error Unknown code '…' in the CodeSystem 'http://www.whocc.no/atc' version '2026'; a display is checked (case-insensitively) only where TW Core's ValueSet display was verified to be the server's; codes outside the table stay unchecked.
  • A Coding element bound directly to a ValueSet (e.g. Encounter.class) whose system is a ValueSet URL only gets the binding warning, no The Coding references a value set error (as HL7).

1.7.0 (validator v13, evaluation report v13) adds:

  • A ValueSet canonical written as Coding.system: an IG ValueSet (e.g. https://twcore.mohw.gov.tw/ig/twcore/ValueSet/laboratory-code-tw) written as the system of a coding inside a CodeableConcept (or of a Coding that is not directly bound), when the same resource / Bundle has an element bound to that ValueSet (or to a ValueSet that includes it — laboratory-code-tw includes loinc-observation-code), gets the error The Coding references a value set, not a code system ('<url>') at …coding[i].system (HL7 Terminology_TX_System_ValueSet2), replacing the warning A definition for CodeSystem … could not be found. 1.6.x reported it for HL7 core / THO ValueSets only (those still error in any context). Basis (confirmed item by item with validator_cli 7.0.1 and 7.0.3): the CLI recognises an IG ValueSet as such only once its run has loaded it — after it checked an element bound to it, the coding is an error (a previous coding of the same CodeableConcept, another element of the resource, an earlier Bundle entry, an earlier file of a batch); the same resource is only a warning the first time it appears in a JVM; a ValueSet never checked as a binding (medication-path-tw; medication-nhi-tw / medication-fda-tw, bound only on medication[x].coding slices) stays a warning even in a batch. We apply the order-independent version: an error whenever the validated resource / Bundle checks a binding to that ValueSet anywhere, otherwise the warning. A directly bound Coding element (Encounter.class, ImagingStudy.series.modality) keeps only the binding warning. These errors carry suggestions: candidates from normalising the code / display within that ValueSet and the CodeSystems it includes (replace_coding), and the CodeSystems it includes (replace_system, the one holding the code first).
  • LOINC property filters decide membership: = / in filters of a ValueSet compose on http://loinc.org (CLASSTYPE (number or a name such as Laboratory class), CLASS, STATUS, COMPONENT, PROPERTY, TIME_ASPCT, SYSTEM, SCALE_TYP, METHOD_TYP, concept in) are evaluated from the local loinc.concepts; codes not held locally and other operators (regex, is-a …) stay undecidable. Binding checks of ValueSets such as laboratory-code-tw against LOINC codes are therefore no longer just "could not be confirmed".

New in 1.6.1 (validator v8, evaluation report v6):

  • An unresolvable CodeSystem URL is an error on every element: a Coding.system / Quantity.system in the HL7 spaces (http://hl7.org/fhir…, http://terminology.hl7.org/…) that no installed package defines gets No definition could be found for URL value '…' at the system element (Coding, CodeableConcept, Encounter.class, Identifier.type, Meta.tag, extension values, contained resources and nested Bundles alike). Under http://hl7.org/fhir/sid/… only the HL7 validator's SIDUtilities list is known (ndc, icpc-2, icd-9, icd-10, cvx, srt, icd-10-cm, icd-9-cm, dsm5 …); icd-10-pcs, us-mbi etc. are not, so they get this URL error on any element (1.6.0 treated them as known and missed them). The code itself keeps the warning A definition for CodeSystem '…' could not be found (HL7 UNKNOWN_CODESYSTEM); an unknown system outside the HL7 spaces (an IG's own URLs …) also only gets that warning, as in validator_cli.
  • Whitespace in a reference (Patient/ 123, a trailing blank): error The reference '…' is not a valid reference (HL7 REFERENCE_REF_INVALID_REF).
  • An RxNorm display naming another drug: when the display shares no drug-name word with any known name of the RXCUI (preferred name + alt_names; strengths, units, dose forms, routes, salts and function words are stripped first, a ≥5-letter common prefix counts as the same word), or contains no Latin letters at all (a Chinese brand name), it is an error even when only the preferred name is held locally: Wrong Display Name '…' for …#<code>. Valid display is '…'; the display denotes a different concept: …. Another wording of the same drug (synonym, salt, form) stays a warning until rxnorm_import has loaded alt_names, after which it is an error as at tx.fhir.org. SNOMED / LOINC / ICD / CVX displays were already errors (validator_cli too) and are unchanged.
  • Failed lookups are never a pass: when any definition / terminology lookup fails during a validation because of database trouble (connection lost, recovery mode, timeouts; missing tables / columns excepted), the result carries an extra error issue (code: "exception", Validation incomplete: N … lookups failed …), valid: false, checked_fully: false and lookups_failed; an exception inside the validator itself now returns a normal result with a fatal exception issue instead of a failed tool call.
  • fhir_validate_bundle: an entry without a usable resource (written as an array, no resourceType) has no entries row; its issues (e.g. must be a simple value, not an Array) are now listed in the Bundle-level issues instead of being lost.

Bundle finalisation (fhir_finalize_bundle / normalize_ids)

Models — weaker ones especially — write readable but malformed identities: fullUrl: "urn:uuid:patient-1", references as Patient/patient-1 or a bare id, Bundle.id as a urn. None of this is clinical content, yet the official validator rejects the whole Bundle. fhir_finalize_bundle hands that numbering-and-wiring work to the tool. Every rule comes from the FHIR R4 core specification or from the package's StructureDefinitions — nothing is special-cased per IG:

Rule What it does
fullUrl-uuid (mandatory) Valid values are kept (lowercase urn:uuid:<UUID>, urn:oid:, absolute http(s)://); an uppercase UUID is lowercased; anything else (a label, missing, empty) gets a deterministic UUID v5: fixed namespace, seeded by (resourceType, original fullUrl, resource.id, entry position) plus a Bundle-level salt (SHA-256 of the Bundle with the rewritable fields stripped), so the same input always yields the same output and different Bundles never collide
reference-rewire (mandatory) Every Reference-typed element is located through the resource's StructureDefinition snapshot (the profile when meta.profile resolves, else the core definition) — including extension valueReference (definition resolved by url / slice), Composition.section.entry, Provenance.target, datatype children such as Identifier.assigner, backbone elements and contained resources. The original fullUrl, a case-different UUID, Type/id (matching resource.id) and a bare id all map to the same entry and are rewritten to its final fullUrl. A bare id / type-less form that matches several entries is disambiguated by the element's targetProfile types; still ambiguous or missing → listed in unresolved with same-type entries as candidates, never guessed. #contained, absolute URLs outside the Bundle and identifier-only logical references are left alone; …/_history/v is resolved with the version kept
resource-id (mandatory) A resource.id / Bundle.id outside [A-Za-z0-9\-.]{1,64} takes its last segment (urn:uuid:… → the UUID); if still invalid, resource.id becomes the entry's UUID and Bundle.id a generated value
bdl-7 Duplicate fullUrls are only reported (notes), never merged or dropped; references to a duplicated label become unresolved with both candidates
bundle-type / document-order (profile-driven, or requested by the caller) When the StructureDefinition of bundle_profile (or meta.profile) fixes Bundle.type (fixed / pattern) it is set; a caller-supplied bundle_type (R4 bundle-type code) is applied as rule bundle-type with reason: "requested by caller (bundle_type)", but a profile-fixed type always wins (the conflict is only noted); for type=document the Composition is moved to the first entry. A document with no Composition at all is never retyped on its own: notes names both ways out — bundle_type=<the type the entries' shape supports> or adding a Composition. Elements the profile requires with real values (Bundle.identifier, Bundle.timestamp …) are only reported in notes, never generated

Apart from those fields the output Bundle is identical to the input (deep copy, key order preserved), numbers included: the HTTP layer parses the request with JSON.parse, which turns 10.0 into 10, 1e3 into 1000 and rounds integers beyond 2^53, while a FHIR decimal's precision is part of its value. So fhir_finalize_bundle, the normalized_bundle of fhir_validate_bundle(normalize_ids=true), the entry resources of fhir_build_bundle and the resource of fhir_finalize_resource re-read the bundle / entries / draft argument losslessly from the raw request text; the finalisation still runs on the ordinary tree (UUIDs, changes and validation are unchanged) and only at the end is each number swapped back to the caller's spelling (10.0, 1.50, 0.50, -0.0, 12345678901234567890, inside extensions and nested arrays too; only where the value is the same). All three entry points (/mcp, POST /tools/<name>, the admin console playground) return the same bytes. An already valid Bundle is returned unchanged with changes: [], and finalising a finalised Bundle changes nothing. Without package_id the package is resolved from bundle_profile / meta.profile; when no IG resolves, hl7.fhir.r4.core is used.

fhir_validate_bundle shares the engine: by default (normalize_ids=false) ID-class errors carry suggestions (replace_fullurl / replace_reference / replace_id / move_entry / replace_bundle_type) and data gains the full map id_map (old → new), id_changes (every path that must change together) and id_unresolved; a Bundle without ID-class issues is byte-identical to 1.6.1. With normalize_ids=true the Bundle is finalised first, the normalised Bundle is validated and returned as normalized_bundle with id_changes, and each fixed problem is noted as an information issue. fhir_get_profile_required_elements on a Bundle profile additionally returns bundle_rules (fullUrl format, references = the target's fullUrl, id format, a profile-fixed Bundle.type — or the hint that fhir_finalize_bundle's bundle_type sets a free one —, document / message ordering, required elements and entry slices), likewise generated from the core specification and the StructureDefinition.

Content repair (repair="content")

fhir_finalize_bundle / fhir_finalize_resource take repair, default "content": besides the id normalisation above they correct content errors whose fix is uniquely determined; repair="ids" is exactly 1.1.0 / 1.5.0 (ids only). fhir_validate_bundle accepts repair="content" with normalize_ids=true. fhir_author_resources / fhir_assemble_bundle always apply the same rules (node-server/src/fhirRepair/ — one input, one verdict, one repair in every tool). Every correction is listed in changes[{path, from, to, rule, reason}]; a value that cannot be decided uniquely is left as written and listed in repair_issues[{path, rule, message, candidates}], the validator's error stays (and carries repair).

rule Corrects Left as written and reported
display the code exists and the display is the same concept written differently → the official display; the original wording moves to an empty CodeableConcept.text. Method: (a) equal to one of the valid displays / designations the validator lists (other languages, LOINC long / short names) after normalisation — NFKC, case, punctuation, word order, plural, roman numerals (Type II ↔ Type 2), SNOMED semantic tag, initialism; (b) otherwise the display is searched in the code's own system (fhir_normalize_code): this code ranks first, exactly or sharing at least half its words with a valid display another code ranks first (SNOMED 428921007 with "Pneumonia" → 233604007), the code ranks only lower, no search available — a different meaning: not changed, the candidates are the codes the display denotes; ancestors / descendants count as different
ucum a Quantity (any Quantity-typed element, component[].valueQuantity included) with no code, a code that is not valid UCUM, or a LOINC code (8310-5) as code gets the UCUM code its unit / code text determines, plus system: UCUM names / synonyms / symbols (mmHg → mm[Hg], °C/℃/degC → Cel), the <count word>/<time unit> grammar (次/分 → /min, mg/天 → mg/d), the micro sign / Greek mu for the UCUM prefix u and bracketed units written without brackets (μIU/mL → u[IU]/mL), a factor glued to a unit in a divisor (a decimal one as an annotation: mL/min/1.73m2 → mL/min/{1.73_m2}; an integer one: mg/24h → mg/(24.h)); it must fit the profile's fixed code or required UCUM ValueSet (min where /min is fixed is read as /min; a spelling equal to exactly one allowed code at conversion factor 1 becomes that code: 1/min → /min). A LOINC code as code with no unit, or a value with neither code nor unit: when the profile allows exactly one unit (fixed / pattern, or a required ValueSet of one code) it is set directly (reason in changes: "the profile allows only "; the unit text too when the profile requires one), otherwise — for the LOINC case — error quantity-code-not-ucum ("valueQuantity.code holds a LOINC code, not a UCUM unit; give the unit") the evidence disagrees (code g, unit kg), the derived code is not allowed (K for a body temperature), nothing derivable; a system naming another real CodeSystem is never touched
roc-date ROC (民國) dates in date / dateTime / instant: marked (民國 / ROC) or three-digit years (115-10-03, 115/10/03, 115-10-03T16:05:00, 民國115年10月3日) → +1911, precision kept; a time keeps its own offset, else takes the site timezone (see timezone below) one- or two-digit years (99-10-03) are ambiguous; a time without offset while the site timezone is unknown (error, code: timezone-required); four-digit years are not this rule's business
timezone a Gregorian dateTime / instant with a time but no UTC offset ("If a date has a time, it must have a timezone") → the site timezone is added (seconds completed). The site timezone comes from, in order: the timezone argument (+08:00 or an IANA name such as Asia/Taipei, resolved at that local time, DST aware) → a timezone line of site_config (- 時區:+08:00, timezone: Asia/Taipei; label 時區 / timezone / time zone / tz / UTC offset) → the same line in the 【院所設定】 section of source_text the site timezone is unknown (error timezone-required, saying how to give it); site_config names several timezones; the wall time does not exist or is ambiguous in the IANA zone (a DST change)
system a Coding.system that is not absolute (ICD-10-CM, LOINC, SNOMED CT, loinc.org, an OID), a ValueSet URL (the HL7 core URL without /ValueSet/, http://hl7.org/fhir/condition-code, included), or a URL that resolves to nothing (…/ns/fhir/CodeSystem#icd-10-cm, …/CodeSystem/_ActMedicalServiceCode, …/CodeSystem/v3-ActEncounterCode) → the one CodeSystem holding the code among those it names: installed CodeSystems' name / title / id / URL tail / identifier, the FHIR R4 external code system list, the URL's fragment / last segment, a THO abstract _ code, a THO / core ValueSet named by the URL's last segment (the CodeSystem the code has in it), the element's bound ValueSets; failing all of these, the installed CodeSystems with a similar name (equal after dropping v2- / v3- and punctuation, one a prefix of the other, or ≥ 80% Levenshtein similarity) — renamed / retired names (v3-AdmitSource → admit-source, v3-GTS → v3-GTSAbbreviation) — only when exactly one holds the code and the element's required / extensible binding (when decidable) accepts it several match, or the code is not in the system named
shape lossless reshapes: a Coding given as {coding: [one]} or wrapped in another property (Encounter.class: {type: {...}}), a Coding nested in a Coding's code, a CodeableConcept given as a bare Coding, a required-bound code element given as a CodeableConcept with one coding / a Coding (reaction.severity → "moderate"; the code must be in the ValueSet and no display / text is lost), a one-item array for a single element, a single value for a repeating one, a property misplaced where exactly one neighbour defines it (Dosage.doseQuantity → doseAndRate[0].doseQuantity, entry.meta → entry.resource.meta, Timing.route → Dosage.route, doseAndRate.unit → doseAndRate.doseQuantity.unit, doseAndRate[0].timing → Dosage.timing, a request written in the resource → entry.request), a missing resourceType when meta.profile names one resource type. The Bundle itself: no resourceType but a proper entry array → "Bundle" (before the INVALID_ARGUMENT check; with repair="ids" the error names the location); keys such as "entry[1]", "entry2" → the entry array rebuilt in key-number order; duplicate fullUrls (bdl-7) with deep-equal resources → the later copies dropped several codings, a text that differs from the display, several possible places, duplicate numbers or numbers inside an existing entry array, different resources under one fullUrl; the message shows the correct shape with an example
empty empty elements (ele-1): a value {}, [], "" or null is removed (a parent left empty goes in the next pass) a null carrying _x extensions is kept
fixed-slice a required (min ≥ 1) Coding / CodeableConcept slice whose content the profile fully fixes (fixed / pattern) and that is absent → added (a pulse-oximetry profile requiring two fixed codings in code.coding, vital-signs category:VSCat); the new value must belong to the slice by its discriminators a required child of the slice is not fixed, an item of the slice is already there, the element's max would be exceeded
alt-coding a CodeableConcept Coding whose code is missing from the package's excerpt CodeSystem (validation: Unknown code — e.g. TW Core 1.0.0 medication-nhi-tw holds 35 codes) but defined in an installed full copy, and whose concept links to exactly one coding of another system that the validator accepts and a ValueSet bound to the coding element or a coding slice contains (an NHI drug → its ATC code, TW Core medcation-atc-tw) → replaced by that coding; dropped when the CodeableConcept already carries it; text kept, and set to the original display when absent (the product name is not lost). For a resource that declares no profile (no meta.profile) the binding is read from the IG's profiles of that resource type (the package owning the CodeSystem, else the resolved / default IG) — only when there is one such profile or all of them bind the element the same way, otherwise nothing is changed — and the reason names the profile; validation itself still uses only the profiles the resource declares several accepted linked codings (reported with candidates), none accepted / allowed by the bindings, the code is in the package's CodeSystem
code a required-bound code written as one code's display or in another case (periodUnit: "hour" → h), complete local expansions of ≤ 200 codes only several or none match

Never adds an error: each resource is validated before and after; when the repaired resource has an error it did not have (a display error revealed by a corrected system, profile rules that apply once meta.profile is moved in), the responsible changes are withdrawn and explained in repair_issues (warning, not applied: …). Exception (evaluator decision): when the only new error after a system correction is the display check of that same Coding (Wrong Display Name … for <new system>#<code>), the error existed semantically before — the system correction is kept and listed in changes, and the display error stays reported by the display rule with the candidates the model's display denotes; any other new error still withdraws the change. Since v14.1 the whole Bundle is validated before and after as well: an error only the Bundle context shows (reference targets, entry slices, document rules) also withdraws the change behind it (traced by the error's path and the entries / fullUrls its message names; when it cannot be traced, nothing is applied). The Bundle envelope repairs (resourceType, the rebuilt entry array) make the Bundle readable by the validator at all and are the baseline of that comparison. The same input always gives the same output, a repaired Bundle repaired again yields changes: [], and numbers keep their spelling.

v16 structure repairs (any IG, any depth): the max of every element is read from the declared profile's StructureDefinition (else core) — a repeating element given a single value → a one-item array, a max-1 element given a one-item array → the item, a meta.profile string → an array; an entry without resourceType whose meta.profile (array or string) names profiles of one resource type → that type; empty elements ({}, [], "", null, ele-1) removed; an Unrecognized property moved only when exactly one level up / down defines it. These are lossless reshapes: an error that appears after the repair only because the value can now be read (under the reshaped element, or the resource now validated against the meta.profile it declares, and profile-match errors of references to it) was hidden, not added, so the repair is kept; an inferred resourceType is withdrawn only by an unknown top-level property (a wrong type). On the example Bundles of the TWPAS 1.2.7 and TW Core 1.0.0 packages with these corruptions applied to every entry, the repaired Bundles are back to the examples' own error counts (node-server/scripts/v16-structure-eval/evalRepairs.ts).

Fix suggestions (suggestions / caused_by)

Besides saying what is wrong, validation issues carry, where possible, "what to change it to": suggestions is a list of candidates ordered by confidence (at most 5 per issue; up to 12 for a binding error on a small ValueSet), and reference-chain errors get caused_by. Both are additive — the set of issues, their severity and message, and valid are unchanged; the field is absent when there is no suggestion.

{"severity": "error", "path": "MedicationRequest.dosageInstruction[0].timing.repeat", "message": "Unrecognized property 'periodUnits'",
 "suggestions": [{"value": "periodUnit", "kind": "rename_property", "confidence": 0.82, "reason": "closest element name at this level: 'periodUnit'",
                  "patch": {"op": "move", "from": "MedicationRequest.dosageInstruction[0].timing.repeat.periodUnits", "path": "MedicationRequest.dosageInstruction[0].timing.repeat.periodUnit"}}]}
Error kind Suggestion
Unknown code 'X' in the CodeSystem 'http://unitsofmeasure.org' replace_code The closest valid UCUM: case (UCUM is case-sensitive), square brackets ([U] → U, [UI]/mL → [iU]/mL), prefix + unit from the UCUM table, common clinical spellings (mcg → ug, mmHg → mm[Hg]), and an annotation for dose-form words that are not units (tab → {tablet}); every candidate is checked with the same UCUM parser the validator uses
No definition could be found for URL value 'U', A definition for CodeSystem 'S' could not be found, The Coding references a value set, not a code system replace_system The CodeSystem(s) the misused ValueSet (or the ValueSet of that name) draws from (…/CodeSystem/v3-ActEncounterCode → …/CodeSystem/v3-ActCode), and the closest canonical in the package closure; when the Coding has a code, systems that hold it rank first
Unrecognized property 'p' rename_property / move_property / remove_property In order: the closest element name at the same level (case, spelling; datatype children and typed names of choice elements included, e.g. value → valueQuantity); the level of this resource where an element of that name actually lives (ancestors, descendants up to three levels including datatype children, sibling backbones / datatypes of an ancestor; timing.periodUnit → timing.repeat.periodUnit, dosageInstruction[0].note → MedicationRequest.note, Patient.city → Patient.address[0].city); a sibling that means the same and whose type fits the value (lower confidence; evidence from the FHIR element's alias / short and a small table of FHIR-generic synonyms: every: 8 next to periodUnit → period, upperLimit in a Range → high, zip → postalCode, comment → note); only when none of these exists, removal (patch.op = remove), with a reason that lists the level's element names (capped at 14) and the other resource types that carry the name (reasonCode on an Observation → "an element of Encounter, Procedure …")
The type 'date' is not allowed for Condition.onset[x] rename_property The typed name of an allowed type that fits the value (onsetDate → onsetDateTime)
Value not in a required / extensible ValueSet choose_code Codes of the expanded ValueSet: a ValueSet of ≤ 50 codes is always listed whole (the expansion takes no lookup budget), ranked by string similarity; a small one (≤ 12) whose codes resemble nothing is ranked by meaning instead (embedding endpoint; string order when unavailable); for a large one the closest to the given code / display (incl. fhir_normalize_code)
Value not in the element's ValueSet, but a valid code of another bound element of the same resource (criticality: "severe" → reaction[0].severity) move_value The instance path of the element the value belongs to (patch.op = move): elements with a required / extensible binding and a compatible type (code → code, CodeableConcept → CodeableConcept) are read from the snapshots of the resource's profiles (meta.profile / the validation's profile) and base definition, membership confirmed with the validator's own check; lower confidence when the target already has a value
Unknown code in other CodeSystems replace_code / replace_system Spelling repairs the validator accepts (case, ICD dot, LOINC hyphen), codes whose name matches the display / text (fhir_normalize_code), near codes of an IG CodeSystem, and another terminology whose code shape fits (a LOINC code under the SNOMED system); when the code exists in another CodeSystem included by the element's bound ValueSet (slice bindings included — an IG-local CodeSystem/v2-0203 next to the HL7 CodeSystem of that name in identifier-type-tw) or in the CodeSystem of the same name under another base → replace_system (0.9; 0.6 when the ValueSet enumerates only some codes of that CodeSystem and not this one)
No definition could be found for URL value 'U' where U is a Coding.system and the Coding has a code / display replace_coding / replace_system The element's bound ValueSets are read from the profile / base snapshots (slices, the .coding child, maxValueSet / additional bindings) and the Coding's display / code is normalised within them (fhir_normalize_code) → a whole {system, code, display} (oral in dosageInstruction.route → SNOMED 26643006 Oral route, TW medication-path-tw PO 口服), value = the code with system / display alongside; plus the CodeSystems those ValueSets draw from (0.9 for one that holds the code, 0.35 otherwise)
Wrong Display Name replace_display (+ replace_code) The valid display(s) the validator names (uniform across terminologies); if the written display is the name of another code, also a code change
Unable to find a profile match for urn:uuid:X caused_by The referenced resource's own errors (fullUrl, entry, path, message); when it has none, its errors against the profile the reference requires (with profile); when X is a label-style reference, also a replace_reference
UUIDs must be valid and lowercase, missing fullUrl, URN reference is not locally contained, Entry N matches … by type and id, Relative Reference … missing a fullUrl, Invalid Resource id, bdl-7 / bdl-11 replace_fullurl / replace_reference / replace_id / move_entry / replace_bundle_type The deterministic value fhir_finalize_bundle would apply (always the same for the same input); an unresolvable reference lists the fullUrls of same-type entries as lower-confidence candidates; the whole map is in data.id_map / id_changes (see "Bundle finalisation")
Constraint failed: bdl-11 (a document with no Composition at all; bdl-12 for message / MessageHeader likewise; the consequent bdl-9 / bdl-10 get only the first kind) replace_value / add_resource 1. Bundle.type → a type the entries' shape supports: no request → collection (0.8); every entry carries request → transaction (0.7), plus batch (0.4) when no entry references another; some carry a request → transaction / collection; every entry carries search → searchset. Never suggested when the profile fixes Bundle.type. 2. Add a Composition (or MessageHeader) as the first entry (patch.op = add, Bundle.entry[0]); reason lists the elements the core StructureDefinition requires (status, type, date, author, title …). A Composition that exists but is not first is handled by move_entry in the row above

patch uses the validator's dotted paths (op: replace / move / remove; move also has from; a move into a repeating element that does not exist yet carries [0]). confidence is a heuristic score, not a probability, but the bands per kind are fixed: exact / case rename > move > semantic rename > remove; suggestions are an aid — validate again after applying one. Suggestions are deterministic for the same input; if computing one fails, that issue simply has none and the validation result is unaffected. Evaluation method and figures: node-server/scripts/suggestions-eval/.

In-process validation is based on profile snapshots and terminology bindings, benchmarked against HL7 validator_cli 7.0.1 (method and results in node-server/scripts/validator-audit/). It is not equivalent to conformance certification by the official HL7 FHIR Validator.

Slice / profile mismatch explanations (slice_match / profile_match)

Three errors (messages as HL7 validator_cli words them, unchanged) get an explanation field (v16 R3, fhirSuggestions/slices.ts, any IG):

Error Field Content
This element does not match any known slice defined in the profile P and slicing is CLOSED slice_match closest: the closest slice (scored by discriminator agreement: resourceType, meta.profile relation, fixed / pattern values, required bindings) and its profile; others; failed: the discriminators that failed and why (meta-profile-missing / -misspelled / -other-version / -not-array / -other, resource-type-differs, pattern-differs, value-missing, not-in-valueset, reference-unresolved); blocking: the first 5 errors of the resource against the closest profile (blocking_total = all); hint
Slice 'S': a matching slice is required, but not found (from P) slice_match closest.item: the item most likely meant for the slice (unmatched ones first), why it fails, blocking; with no item of the slice's type closest: null, and hint plus an add_resource suggestion say which profile to author (never invented)
Unable to find a profile match for R among choices: … profile_match target (the referenced resource, its declared meta.profile), the closest profile, failed, blocking

blocking comes from the resource's own reported errors when it declares that profile, otherwise from one stand-alone validation against the closest profile (at most 12 per call, planned in issue order before anything runs — reproducible). When meta.profile gives no signal and several same-type slices score alike, each is validated and the one with the fewest errors (differing fixed / pattern values weigh extra) wins. When the closest profile is unique and the resource does not declare it correctly, a set_profile suggestion is added (patch sets meta.profile; confidence 0.9 = no other error, 0.6 = blocking still to fix). Evaluated on the TWPAS example Bundles with one entry corrupted (a required element dropped, optionally meta.profile removed or misspelled): all 1,752 related errors explained; of the 426 about that entry, 421 (98.8%) name the right closest profile and all 426 have blocking naming the dropped element (node-server/scripts/v16-structure-eval/evalSlices.ts).

IG structure: Bundle blueprint and minimal skeleton

Before writing a Bundle that must conform to a Bundle profile (a prior-authorization request, a document Bundle …), get the IG's structural knowledge from these two tools, then write the resources. Both read only StructureDefinition snapshots, the IG's ValueSets and the IG's own examples, and work for any installed IG (no IG-specific rules).

Tool Returns Parameters
fhir_get_bundle_blueprint The Bundle profile's blueprint (whole response ≤ 4,000 characters): slices (every entry slice: name, min..max, the resource profile it requires; profile names follow profile_base, otherwise a full URL; no min..max means 0..*), entry.slicing (discriminator, whether slicing is closed — then an entry matching no slice is an error — and ordered), type (Bundle.type when fixed), references (the references between slices that the slice profiles require, also inside extensions and sliced backbones, target profiles matched through derived profiles too; e.g. claim: extension:encounter.valueReference -> encounter, coverage: payor -> organizationOrg; no suffix = required, if X used = required when X is present), recipe (how to write it: each entry's resource.meta.profile = its slice's profile, every required slice, fullUrl = urn:uuid, references = the target entry's fullUrl; the first entry of a document / message). omitted counts references and slices dropped for size; view="references" lists every reference bundle_profile, package_id, version, view (summary default / references)
fhir_get_minimal_skeleton A profile's minimal legal instance and where each required value comes from (whole response ≤ 6,000 characters): skeleton (every required element recursively: required slices, extensions expanded from their definitions (url + value), fixed / pattern values filled in, meta.profile; replace each <...>) and table rows [path, value_source, how]; conditional (required when a profile invariant <guard> implies (a.exists() and …) applies; the rule texts in conditions), rules / more_rules (other invariants), examples (example instances / distinct), omitted profile, package_id, version

How value_source is decided (all data-driven):

Value Meaning and rule
ig-constant A fixed / pattern value of the profile (incl. extension urls; already in the skeleton), or a value identical in every one of the IG's own example instances (at least 2; a value that varies never counts). Eligible elements depend on the resource kind: an administrative entity (Organization, Location, HealthcareService, Endpoint, OrganizationAffiliation — not persons) that the IG reuses in every example — any element (e.g. the payer agency's identifier; its optional identifier / name go into the skeleton too); any other resource — only workflow / derived codes (status-like elements with a required / extensible binding, booleans) over at least 2 distinct instances; record facts (dates, values, names, identifiers, gender) and primary clinical codes never
code-lookup A coded element with a required binding (or a required coding): small bindings list their codes with displays, larger ones name the ValueSet to search with fhir_normalize_code / fhir_expand_valueset; codes of a preferred / example binding are illustrations and are not listed
text-allowed A CodeableConcept whose binding is not required and whose coding is optional: {text} alone is valid (add a coding when a code fits)
derivable Computable without a record fact: references (the target entry's fullUrl), sequence / rank numbering, workflow defaults, the curated derivation rules, the system of a bound Quantity, insurance.focal, writing dates (Claim.created, authoredOn, recordedDate, extensions named like a recorded date …: the record's date for it, else the visit / document date)
record Everything else: a clinical or administrative fact the record must state

Only record rows need the record; a missing ig-constant, code-lookup, text-allowed or derivable value is never a reason to refuse. Elements outside the skeleton are optional. Suggested flow: fhir_get_bundle_blueprint → fhir_get_minimal_skeleton(profile, package_id) per slice profile → write the resources (your own JSON, or fhir_author_resources + fhir_assemble_bundle) → fhir_finalize_bundle / fhir_validate_bundle with bundle_profile. Pass the package_id of the IG you write for — example constants come from its examples (from the profile's owning package only when that IG has no example of the profile).

Authoring from field values

The model no longer writes FHIR JSON: it gives field values, the MCP composes the resource for the profile, validates it, keeps it in a per-task context and assembles the Bundle from a list of keys. Every rule comes from the resolved package's StructureDefinitions / ValueSets and FHIR R4 core — no IG-specific special cases (TW Core, TWPAS or any other installed IG alike).

Tool Description Key parameters
fhir_get_authoring_fields The profile's "writing form": fields (required / expected / commonly used in the IG's own examples, reason in why), more (the remaining mustSupport fields and record links) and omitted (dropped for size, never silent). Record links: an optional backbone the IG neither marks mustSupport nor uses in its examples, but whose required Reference child targets a profile of the same IG (same canonical base) of a non-entity type (not a person / organisation master-data type such as Patient, Practitioner, Organization, Location), e.g. Encounter.diagnosis, is listed in fields with all its children (condition, use, rank) and the example {condition: {ref: "condition"}}; a direct reference to such a profile (Encounter.reasonReference, Condition.encounter, Procedure.reasonReference, Observation.hasMember / derivedFrom) is in more with targets. The whole response (envelope, warnings, provenance; non-ASCII counted as \uXXXX) stays within 6,000 characters (150 kept as a margin); its length is data.response_chars. Each field has kind + format (how to write the value), source (record / facility / code / fixed / default / derived), small binding.codes, targets and children; fields with auto: true are filled by the MCP (value shows what), pins shows what the profile fixes inside a field you still fill (identifier system, component code, unit). A conventional default is auto only on a required element; an optional one (Patient.active …) is listed as a plain optional field whose format / example only hint at the conventional value profile, package_id, version, include_more (true by default)
fhir_author_resources 1.10.0 (v16: validation as fhir_validate_resource 1.14.0); 1.9.0 (v15.4: validation as fhir_validate_resource 1.13.0); 1.8.0 (v15.3: validation as fhir_validate_resource 1.12.0); 1.7.0 (v15.2: alt-coding repair, candidates carry accepted_by_validator); Author several resources in one call (8 or more is fine): {key, profile, fields}; the MCP adds meta.profile, fixed / pattern values, slices, workflow defaults of required elements, derivable values (e.g. Condition.category when an encounter is given), displays, UCUM units, ROC → ISO dates and applies the corrections below, then validates and stores each in the context. Returns {context_id, storage, results[{key, status, resource, provenance, missing, refs, issues, notes}], summary, settings} context_id (omit on the first call), resources, timezone, apply_defaults (required / none), site_config
fhir_context Inspect or trim a context: list (status and references per key), get (one key's full result), delete (one key or the whole context) context_id, action, key
fhir_assemble_bundle 1.10.0 (v16 R5: with bundle_profile, every resource is placed into its entry slice before validation and bundle_slices is reported; repairs as fhir_finalize_bundle 1.8.0, validation as fhir_validate_bundle 1.17.0); 1.9.0 (v15.4: resources as fhir_author_resources 1.9.0, validation as fhir_validate_bundle 1.16.0); 1.8.0 (v15.3: resources as fhir_author_resources 1.8.0, validation as fhir_validate_bundle 1.15.0); 1.7.0 (v15.2: as fhir_author_resources 1.7.0 / fhir_finalize_bundle 1.5.0 / fhir_validate_bundle 1.14.0); Build the Bundle from a list of keys: each entry's fullUrl is the key's deterministic urn:uuid; the Bundle is finalised by the fhir_finalize_bundle rules and validated like fhir_validate_bundle (grounding too with source_text); every issue carries the key and the model's field name context_id, keys, bundle_type, bundle_profile, identifier, timestamp, source_text, site_config, allow_invalid

Flow

  1. Plan one key per resource (patient-1, enc-1, cond-1, obs-wbc …).
  2. Call fhir_get_authoring_fields(profile) once per profile.
  3. Call fhir_author_resources without context_id for the first batch and keep data.context_id; cross references are {ref: "<key>"} and may point at a key not authored yet (status refs_pending, resolved at assembly); cycles (Encounter.diagnosis ↔ Condition.encounter) are fine.
  4. Fix a status: invalid resource by resending the same key with merge: true and only the changed fields (null removes a field); issues carry key, field (your field name), path (FHIR path) and suggestions.
  5. fhir_assemble_bundle(context_id, keys, bundle_type); for a document Bundle, author the Composition first (section.entry as {ref}) and pass identifier / timestamp from the record (bdl-9 / bdl-10).

Value grammar

Kind Accepted forms
string / code / boolean / integer / decimal the JSON scalar; numbers keep their spelling (12.50 stays 12.50, 1e2 stays 1e2)
coding / codeableConcept {system, code, display?}; several codes as an array; {text} or a plain string when there is no required binding
quantity {value, unit} (unit text or UCUM; the MCP adds the UCUM system / code) or the string "95 mg/dL"
date / dateTime / instant ISO 2026-06-15, 2026-06-15T06:00, or ROC 115/06/15, 115/06/15 06:00 (precision kept). A time without an offset needs timezone (e.g. "+08:00", remembered on the context), otherwise it is an error — an offset is never guessed
period {start, end?} with the date forms above
identifier {value, system?, type?}; a slice as identifier:medicalRecord
reference {ref: "<key>"} (a resource in the same context) or {reference: "Patient/123"} (external)
choice value[x] give value; its shape picks the type (quantity → valueQuantity, {system, code} → valueCodeableConcept …), or name it (valueQuantity)
component [{code, value}] or component:<slice> {value}
extension slice extension:<slice> with {value: …} or the bare value ({ref}, {system, code} …); the MCP writes {url, value[x]} from the extension's definition; FHIR JSON that already carries url is kept as written
dosage {text?, route?, dose?, frequency?/timing?, period?, asNeeded?}

Rules: model values win (only fixed / pattern values override, a conflict is reported as an error; a display the terminology rejects is replaced as described under "Corrections"); values only the record or the facility knows (dates, numbers, identifiers, names, doses, the time zone, Bundle.identifier / timestamp) are never invented — they are listed in missing; values the MCP supplies are marked fixed / default / derived / lookup in provenance.

Corrections (listed in results[].notes)

  • The display follows the code: a display the model wrote that the terminology rule (the same check fhir_validate_resource runs) rejects for a resolvable code is replaced by the code's display (provenance lookup, note display replaced for <field> (was '<old>')); when that CodeableConcept has no text, the model's wording moves into .text (provenance model) so nothing is lost. A case-only difference is valid; a whitespace-only one takes the valid spelling without a note. This rule never changes the code; when the code does not resolve the model's display is kept (validation reports the unknown code). Example: LOINC 1988-5 with display CRP → display C reactive protein [Mass/volume] in Serum or Plasma, text CRP.
  • System correction (1.2.0): the model's Coding.system is changed only in the two cases below and only when the answer is unique (provenance lookup, note system corrected for <field>: …); everything else is left as written for validation to report (with the v10 suggestions) — never guessed:
  • the system is a ValueSet URL (a ValueSet of any installed package, or the /CodeSystem/ ↔ /ValueSet/ twin of one): when the code belongs to exactly one CodeSystem within that ValueSet (expansion / valueSetContains), that CodeSystem is used — e.g. Composition.type written as {system: "https://twcore.mohw.gov.tw/ig/twcore/ValueSet/loinc-type-doc-code", code: "11488-4"} → system: "http://loinc.org" (note '<ValueSet URL>' is a ValueSet; code '11488-4' belongs to http://loinc.org). (1.3.0) When the ValueSet has a filter-defined include (e.g. laboratory-code-tw = a LOINC CLASSTYPE filter + an enumeration of NHI payment codes), its local expansion is only partial and never disproves membership: the code exists in a CodeSystem the ValueSet includes and the filter is decidable and satisfied → corrected, note … belongs to http://loinc.org (membership confirmed); the filter cannot be decided but the code exists in exactly one of the ValueSet's CodeSystems → corrected, note (membership unverifiable; the code exists only in <CS> among the ValueSet's systems). code: {system, code} and the raw form code: {coding: [{system, code}]} behave the same. The code under several CodeSystems of the ValueSet → an error with candidates; the code not in the ValueSet (values that are not codes, such as laboratory, WBC, hs-Troponin I) → not corrected; validation reports the error The Coding references a value set, not a code system (with candidates, see validator 1.7.0), status is invalid, and fhir_assemble_bundle and fhir_validate_bundle give the same verdict.
  • the system URL resolves to no definition (the validator's URL rules: an HL7-space URL only when the HL7 validator reports "No definition could be found for URL value"; not a CodeSystem, a ValueSet or a known external terminology): when exactly one CodeSystem of the element's bound ValueSets (slices and additional bindings included — the fhirSuggestions/bindings.ts binding walk) holds the code, that one is used — exact code first, then case-insensitive (the code then takes the correct spelling), then an exact and unique display (the code becomes that display's code). E.g. MedicationRequest.dosageInstruction.route written as {system: "http://terminology.hl7.org/CodeSystem/route", code: "PO"} → https://twcore.mohw.gov.tw/ig/twcore/CodeSystem/medication-path-tw (display 口服 per the display rule). Several CodeSystems hold it, or none (e.g. oral) → unchanged; the validation error's suggestions offer replace_coding / replace_system.
  • After a correction the display follows the rule above.
  • A single value is wrapped into an array: a repeating element (max > 1) given one object / scalar gets a one-item array, at any depth, including FHIR shapes the model passes through (code: {coding: {system, code}}, identifier: {…} / telecom: {…} inside a nested object, extension inside an extension written with its url, doseAndRate); note wrapped <path> into an array. One value for a top-level field is the documented grammar and gets no note.

Defaults policy (apply_defaults): "required" (the default) fills a workflow default (Observation.status → final, Encounter.status → finished …) or the profile's defaultValue[x] only on elements the profile requires (min ≥ 1, required slices included) that the model left empty; optional elements (Patient.active, Practitioner.active, Organization.active, Immunization.primarySource, MedicationRequest.dispenseRequest.numberOfRepeatsAllowed, a 0..1 Condition.clinicalStatus …) are never auto-filled — give them when the record states them. "none" fills no default at all; required elements without a value then appear in missing. Remembered on the context (like timezone). Derived values (Condition.category …) are unaffected.

Facility settings (site_config): free text in the format fhir_check_grounding's site_config takes — every http(s) URL in it is an accepted facility identifier system, and its lines ground Organization values and Practitioner identifiers. fhir_author_resources stores it on the context (passing it again only adds lines not stored yet); fhir_assemble_bundle's site_config is merged with the context's; with source_text it goes to the grounding engine, so facility identifier systems and organisation data are grounded as site_config instead of being flagged (G-SYSTEM-NOT-SITE). Never logged and never echoed (settings.site_config_lines reports the line count only). Example:

{"site_config": "MRN system: https://hospital.example.tw/mrn\nOrganization: Example Hospital, facility code 0123456789", "resources": [
  {"key": "patient-1", "profile": "Patient-twcore", "fields": {"identifier:medicalRecord": {"value": "MR-00012345", "system": "https://hospital.example.tw/mrn"}}}
]}

Contexts: a context id is 128 random bits (32 hex), stored in Redis under authoring:ctx:<id>, dropped after 60 minutes idle and refreshed by every call; without Redis an in-process memory store is used (storage: "memory", lost on restart). {ref: key} maps to urn:uuid:<UUID v5(context id, key)> — the same value in single-resource validation and in the final Bundle, so the same context + the same keys always give a byte-identical Bundle. Clinical content is never logged (ids, counts and byte sizes only).

Error codes ({ok: false, data: null, error: {code, message, message_zh, hint, hint_zh, retryable}})

code When Next step
CONTEXT_EXPIRED the context id does not exist or expired (never partial results) start over without context_id and re-author every resource with the same keys and fields
KEY_NOT_FOUND a key passed to assembly / get is not in the context fhir_context action=list, then author or drop it
UNRESOLVED_REFERENCE a resource references a key not in keys or not in the context; error.refused lists key / field / ref_key add ref_key to keys, author it, or change the field with merge
INVALID_RESOURCE a stored resource still has validation errors; error.refused lists key / field / error fix with merge and assemble again (allow_invalid=true only if the user explicitly accepts it)
MISSING_COMPOSITION a document Bundle without an authored Composition; error.missing.fields lists what it needs author the Composition from the record and put its key first
CONTEXT_CONFLICT the same context kept being written from several places at once retry once
INVALID_ARGUMENT bad arguments (duplicate key, missing profile, unknown bundle_type, timestamp not an instant …) fix as the message says

Example

{"timezone": "+08:00", "resources": [
  {"key": "patient-1", "profile": "Patient-twcore", "fields": {"identifier:idCardNumber": {"value": "A123456789"}, "name:official": {"text": "王小明"}, "gender": "male", "birthDate": "80/03/15"}},
  {"key": "enc-1", "profile": "Encounter-twcore", "fields": {"class": {"system": "http://terminology.hl7.org/CodeSystem/v3-ActCode", "code": "IMP"}, "subject": {"ref": "patient-1"}, "period": {"start": "115/06/10 14:30", "end": "2026-06-15T10:00"}, "diagnosis": [{"condition": {"ref": "cond-1"}, "rank": 1}]}},
  {"key": "cond-1", "profile": "Condition-twcore", "fields": {"code": {"system": "http://hl7.org/fhir/sid/icd-10-cm", "code": "J18.9"}, "subject": {"ref": "patient-1"}, "encounter": {"ref": "enc-1"}}},
  {"key": "obs-wbc", "profile": "Observation-laboratoryResult-twcore", "fields": {"code": [{"system": "http://loinc.org", "code": "6690-2"}, {"system": "https://twcore.mohw.gov.tw/ig/twcore/CodeSystem/medical-service-payment-tw", "code": "08011C"}], "subject": {"ref": "patient-1"}, "effectiveDateTime": "2026-06-11T06:00", "value": {"value": 12.50, "unit": "10*3/uL"}}}
]}

Fix: {"context_id": "<id>", "resources": [{"key": "obs-wbc", "merge": true, "fields": {"value": "11.8 10*3/uL"}}]}; assemble: {"context_id": "<id>", "keys": ["patient-1", "enc-1", "cond-1", "obs-wbc"], "bundle_type": "collection", "bundle_profile": "Bundle-twcore"}.

fhir_finalize_resource (1.4.0) remains for a draft already written as FHIR JSON: it pins fixed / pattern values only where the draft has the parent node (slice members identified by the slicing discriminators and _slice tags at any depth included; tags are always removed), fills a single-system coding.system, rewrites {reference: <key>} to the context's urn, generates the narrative and validates — it never adds clinical content; with field values use fhir_author_resources instead.

Entry slices of the Bundle profile (bundle_slices)

With bundle_profile, fhir_assemble_bundle (v16 R5, authoring/bundleSlices.ts, any IG) places every resource into the Bundle profile's entry slices before validation: the resource's declared meta.profile or the profile it was authored for decides the slice, else the only / clearly closest slice of its type; a resource authored for its slice's profile gets that profile in meta.profile when missing; entries follow the slice order when the slicing is ordered. bundle_slices reports entries (each key's slice and how it was decided), missing (slices below their min, with "author a … for profile …" guidance — resources are never invented), excess (over max) and unmatched (resources no slice takes under closed slicing), summarised in warnings. Decomposing the TWPAS / TW Core example Bundles into one key per entry (meta.profile removed, order reversed) and re-assembling puts every entry back into its slice with meta.profile restored and no more validation errors than the example; dropping a required slice's key is listed in missing (authoring/bundleSlices.ig.test.ts).

Checking against the source record (grounding / completeness)

Conformance-valid is not the same as faithful: a model can fill in a diagnosis code, date or dose the record never states, and the validator still passes. These tools compare a Bundle with the clinical record using deterministic rules (no LLM, no embeddings, no randomness). Code names are looked up in the local database only (ICD-10-CM / PCS, LOINC, SNOMED CT, RxNorm, CVX, IG CodeSystems); no external terminology server is called.

Tool Description Key parameters
fhir_check_grounding Check that every clinical fact and code in a Bundle is supported by record text (evidence map or auto-locate), with date / number / unit / ID number / dose / frequency normalisation and section, negation and history rules. The output has the fhir_validate_bundle shape (entries[].issues, issues) plus a grounding summary (units_checked, coverage …) bundle, source_text (required), site_config, evidence_map, mode, verbosity (warnings by default), policy, instruction_markers
fhir_check_completeness Gap report: for caller-supplied structured targets (resourceType, item, aliases, codes, date rule), decide whether the record states each required fact; returns found / missing / stated_absent / unknown, the missing list, a template follow-up question and a ready abstention object (insufficient_information) targets, source_text (required), site_config, extracted_facts (facts the model claims; verified, not trusted), instruction_markers

Modes (fhir_check_grounding.mode; grounding on fhir_validate_*)

Mode Elements with evidence Elements without evidence
strict The quote is verified (must occur verbatim in the record, value matches, section and context are right) error G-UNGROUNDED
auto Same The tool locates supporting record text itself: information G-AUTO-LOCATED when found, an error when not found or found only in the wrong context

Default: strict when an evidence_map is given, otherwise auto. evidence_map is [{path, quote}] or {path: quote}; a path may be Bundle.entry[2].resource.code, entry[2].code, <fullUrl>|code, omit list indices, or use value[x]. Every issue carries rule and source:"grounding", and its message starts with [grounding:<RULE>]. Instruction lines such as 【轉換目標】 (instruction_markers) never count as evidence; site_config can only support identifier.system.

Grounding parameters of fhir_validate_bundle / fhir_validate_resource (backward compatible)

  • No source_text, or grounding="off": the output is byte-identical to the pre-extension version.
  • With source_text: the conformance check runs first, then grounding, and the results are merged: data.conformanceValid (the original valid), data.groundingValid, data.valid = conformanceValid ∧ groundingValid, grounding issues appended per entry to entries[i].issues (Bundle-level ones to issues), entries[i].valid updated, the data.grounding summary, data.source = "builtin+grounding" and provenance.groundingVersion.
  • Callers should check for data.groundingValid in the response; if it is absent the server does not support grounding and the result must not be treated as checked.
  • fhir_validate_resource checks the resource as the only entry of a Bundle and reports issue paths rooted at the resource (e.g. Condition.code); references cannot be followed, so rules that need other resources see no context.

Versions and privacy

  • provenance carries tool, toolVersion, lexiconVersion / lexiconSha256, terminologyVersion and sourceSha256 (first 16 hex chars of the record's SHA-256). health_check reports tools_version, lexicon_version and lexicon_sha256.
  • The record text (source_text) and the site configuration are never written to logs, metric labels or provenance — only sourceSha256; malformed JSON request bodies are not logged either.

The rules and the lexicon (tw-lexicon) follow Taiwanese clinical writing; fhir_check_completeness needs the caller to read the conversion target and pass targets — it does not parse natural-language targets. Results support review; they do not replace a clinician.

Does the record actually provide it? (record checks)

When a record deliberately lacks a piece of information, models still tend to "fill something in": "number not yet assigned" as the MRN, a month-only date as Encounter.period.start, a "to be ruled out" diagnosis as an established Condition. Such values can be well-formed — the string may even appear in the record — so both the validator and grounding pass them. Record checks are a post-validation layer (node-server/src/recordChecks/; like the fix suggestions it runs after the validator and never touches InstanceValidator, so the validator parity audit is unaffected) that reports "the record does not actually provide this information".

Report only: no value is ever rewritten or removed; whether to answer insufficient_information instead is the model's decision.

Rule code Needs source_text Severity What it checks
R1 information-absent no error The whole value of a field that needs a structured value (Identifier.value, Coding.code, dates, Quantity.value, Reference, names, contact values, Observation.valueString, lot numbers, the text of a CodeableConcept without a code) describes that the information is absent / unknown / pending / not yet available / not established — e.g. 尚未取號, 建檔失敗, 尚在申請, 不詳, 待確認, unknown, pending, not yet assigned. Message: "the record does not provide this information"
R1 information-absent (text fields, v15.3) no error The whole value of a text field is an absent / not determined / not out / not established / to be completed / to be ruled out / being re-drawn / pending / not yet … statement: CodeableConcept.text (no coding, or placeholder codings only — no code, v3-NullFlavor / data-absent-reason, a code that is itself unknown / TBD / --), the Dosage.text of medication resources, and the valueString of an Observation whose code needs a quantitative result (its note when it has no value[x]). "Needs a quantitative result", in order: LOINC SCALE_TYP Qn or a quantity PROPERTY (MCnc, SCnc, NCnc, CCnc …, looked up by the tools in loinc.concepts), the LOINC name on the Coding ("[Mass/volume]", "-mCnc"), a vital-sign code, a vital-signs profile / category, a referenceRange with low / high, another Observation with the same code and a valueQuantity; only in such a result slot do specimen problems (溶血, clotted, QNS) and "see report" count as no result, and measurement times (3 h, 07:00) are not content. E.g. Condition.code = {"text": "暫不下診斷"}, valueString = "檢體溶血,重抽中(報告未出)". The message names the main clinical field (diagnosis, result, medicine, dose)
R1 information-absent (empty shell, v15.4) no (error needs it) error / warning The resource lacks the primary value it should carry and explains the absence next to it: an Observation without value[x] and without component values (panels with hasMember excluded; also a lone valueString that is itself an absence statement — a bracketed reference range / detection limit such as "(< 14)" is not a result); a MedicationRequest / MedicationStatement without structured dose or timing (no doseAndRate, timing.repeat, timing.code); a Condition without a real coding, text only. The side fields — dataAbsentReason (code and text), note, narrative text, valueString, dosageInstruction.text / patientInstruction / additionalInstruction, statusReason, code.text — are read clause by clause (dominance rule: a clause dominated by an absence predicate, and no clause of that field states a result — "陰性,檢體輕微溶血" is not absent). Predicate classes: pending-result (not out / not back), rejected-specimen (退件, 不予發報, QNS), machine-error (incl. Err-3), undecided (未定, 待確認, TBD), indistinguishable (尚難區分, 鑑別中, cannot differentiate), conditional-on-future-result (要看 Cr 報告, 依…結果調整, depending on the level), not-done. Severity: with source_text, when the record's statement about this item (found by code text / displays / vital-sign labels / terminology names incl. Chinese / medicine names — the mention's clause and the following clauses of its sentence up to a stated value; a Condition also by the diagnosis slot; or by the side text itself, a record clause it copies or paraphrases) says not out / not decided / rejected → error, message 「這筆資源沒有提供病歷中應有的值;病歷寫的是「…」」 with the hint "report insufficient_information instead of creating an empty resource"; the record says not done (本次未做 X 光, patient declined) → warning; the record does not say so / does not mention the item → warning; without source_text → warning only when the side text itself is an absence statement (a dataAbsentReason code alone or "not done" → nothing, so clean calls stay unchanged); entered-in-error never, cancelled / not-taken warning at most and nothing with a not-done reason. A resource another rule already reported is not reported again. The v15.3 result-slot note rule (an Observation without value[x] whose note says there is no result while its code needs a quantitative one) is likewise a warning without source_text (1.17.1; still an error with it)
R2 identifier-format no error (see below) Format by Identifier.system: ROC national ID (http://www.moi.gov.tw) shape and check digit; resident certificate / UI number (http://www.immigration.gov.tw) new and old formats with check digits; no identifier may be a sentence; site_config can set formats for facility systems (MRN, NHI card …)
R3 insufficient-precision yes error An event time (Encounter.period, Observation.effective[x] / issued, MedicationRequest.authoredOn, DiagnosticReport, Specimen.collection, Composition.date …, type dateTime / instant) has only the year or month
R3 incomplete-value yes error A Quantity that is the element's value (valueQuantity, doseQuantity …) has no value; a medicine given only as free text, with no code and no structured dose
R4 unconfirmed-as-confirmed yes error / warning A Condition not marked provisional / differential / unconfirmed while the record's sentences about it say it is not established (待排除, 尚未確立, 鑑別診斷, r/o, differential, not established) → error; only hedged (疑似, 可能, possible, suspected) → warning. No error when the record also confirms it (確診 / 證實 / confirmed) or mentions it without a qualifier
R4 symptom-for-unconfirmed-diagnosis yes warning A Condition coded with an ICD-10 symptom code (R00–R99) while the record's (main) diagnosis says "to be ruled out …", and no Condition in the input is provisional / differential (when the record's primary diagnosis slot decides it, the slot rule below reports an error instead)
R4 unconfirmed-as-confirmed (primary diagnosis slot) yes error / warning The record's primary diagnosis slot (Dx / Diagnosis / Impression / Assessment / A: / 診斷 / 主診斷 / 臆斷 …) is not established as a whole — differential only (a 鑑別診斷 / DDx / r/o heading), a slot-level cue (未下診斷, 診斷未定, no definitive diagnosis, "no diagnosis until the rapid test", "diagnose after the culture result"), or every item under a cue — and a Condition is not provisional / differential / unconfirmed: it is one of the differential items, its code.text copies the differential list, it is a symptom code (ICD-10 R00–R99, a SNOMED finding by its semantic tag, or a concept the record names only in the complaint / HPI / examination) standing in for the main diagnosis, or it is another diagnosis the record does not establish → error (hedged only: 疑似 / possible → warning). Comorbidities the record establishes elsewhere (past history, plan, a plain item of another primary slot) are not reported
R4 information-absent (no diagnosis) yes error A structured record has no diagnosis slot at all (or an empty 「診斷:」) and a Condition (not provisional / differential, not a problem-list-item) is named only in the complaint / HPI / examination, or nowhere in the record
R4 information-absent (provisional / differential primary, v15.3) yes error No primary diagnosis slot of the record is established and the record says so explicitly (differentials only, a slot-level cue such as 未下診斷 / 暫不下診斷 / diagnosis pending, every item to be ruled out / r/o, or an empty 「診斷:」), while the encounter's primary Condition is provisional / differential / unconfirmed or has a text-only code → "the record has no established primary diagnosis". How the primary Condition is decided: see below. Hedged slots (疑似 / possible) are not reported (provisional is the right encoding of a suspected diagnosis); when the record's primary diagnosis is established, a secondary r/o item written as differential is correct and never reported
R1 information-absent (stated absence, identifier without value) yes error The record writes the field as absent (病歷號:申請中, 採檢日期:無法辨識, 血糖:機器錯誤,未得數值, 處方:待藥師確認), no other line gives it a value, and the Bundle has one → information-absent when the value is not in the record, value-from-other-event when it is another event's (the visit date used as the specimen date). An identifier with a system but no value: information-absent when the record has no such number, incomplete-value (with the number) when it has
R5 ungrounded-value yes error (warning for record-keeping times, derived quantities, reference ranges) Every Identifier.value, date / dateTime / instant and Quantity.value must be found in the record (identifiers also in site_config): a normalised match or an explicit derivation (below). Not found → "this value is not in the record", with evidence listing what was searched, the normalised forms tried and the nearest record candidates. A date in the record whose time is not (the typical record date + a filled-in T00:00:00) is an error too; a date-only value matching a record date passes. 0 / negative / 999 values the record does not state are also called out as placeholders
R5 value-from-other-event yes error / warning The value is in the record but belongs to another time point, event or field: the record says the first reading failed ("機器錯誤、未得數值") and the second reading's value is written at the first reading's time → error; two readings' values and times swapped → warning; a date the record states only as a past event (上次 / 曾 / previous) or as the birth date → error; a number the record gives as another identifier (the admission number written as the MRN) → error. Reported only when determinable

How R1 avoids false positives: the value must consist ENTIRELY of absence predicates (lexicon record-checks-lexicon, Chinese and English, including the generated variants "尚未 / 還沒 / 待 / 無法 × 取號 / 建檔 / 提供 / 帶來 …" and "…中" / "…失敗"), field nouns (病歷號, 身分證, ID, date …), fillers (家屬說, 目前, yet …) and punctuation, with at least one predicate; any other content (another word, a run of three or more digits) means it is not absent. So 「家族史不詳」 only matters in the one field it is written into, and 「不明原因發燒」, "Hypertension, unspecified" or 「王不明」 are never read as absence. Codes of HL7 / FHIR code systems (gender = unknown, v3-NullFlavor UNK …) are real codes and are not checked. Nil words (無 / none / N/A / -) count only in identifier, date, quantity, name and reference fields.

R2 formats and check digits (system URLs from the identifier slices of TW Core IG v1.0.0 Patient-twcore / Practitioner-twcore)

Format System Rule
tw-national-id http://www.moi.gov.tw 1 letter + 1/2 + 8 digits; check digit: the letter maps to a two-digit number (A=10 … in the order ABCDEFGHJKLMNPQRSTUVXYWZIO), tens ×1 and units ×9, then the 8 digits ×8…×1, plus the check digit; the sum is a multiple of 10. A second character 8/9 points to the resident-certificate system
tw-resident-certificate http://www.immigration.gov.tw New format (from 2021-01-02): 1 letter + 8/9 + 8 digits, national-ID check digit; old format (valid until 2030-12-31): 2 letters (second A–D) + 8 digits, the second letter contributes the units digit of its letter value (A→0 … D→3), otherwise as the national ID
tw-nhi-card (TW Core defines no system URL; set it in site_config) 12 digits (spaces / hyphens between groups allowed)
tw-passport (set in site_config) 9 digits

site_config syntax (one rule per line, in the same text as the other facility settings): identifier-format <system> <format>, where <format> is a format name above or a JavaScript regular expression matched against the whole value, e.g. identifier-format https://hospital.example.tw/mrn [0-9]{7,8}, identifier-format https://hospital.example.tw/nhi-card tw-nhi-card. A site rule replaces the built-in rule of the same system; an invalid regular expression is ignored and listed in record_checks.notes. When only the check digit fails and the record (or site_config) states exactly this value, the issue is a warning (the record itself carries that number — verify it at the source).

How R3 searches the record: dates — every record line (conversion-target, site-settings and family-history lines excluded) is searched for day-precision dates within the same year / month (western, ROC 民國, 年月日 and month-day forms); found → "the record has a more complete value ''" with the record lines, none → "the record does not provide a more complete date". Quantities — lines mentioning the item's name (code.text, display, vital-sign aliases) are searched for a number with a compatible unit after the name. Medicines — lines mentioning the medicine name are searched for a dose (number + mg / g / mcg / mL / IU / tab …), and the name text itself is checked for a dose. evidence states the search scope and result. History-type times (Condition.onset, Procedure.performed, Immunization.occurrence, MedicationStatement.effective …) and date-typed elements (birthDate) are not precision-checked.

R4 scope: the Condition's names (code.text, displays, ICD-10 code) are located in the record and qualified per sentence (。;;): prefix cues (待排除, r/o, 鑑別診斷, 疑似 …) govern up to the next comma, postfix cues (尚未確立, not established, ? …) the same clause or a short clause right after it ("A、B、C,尚未確立"); a whole line of a rule out section counts as explicit. A cue inside a bracket qualifies only the concept inside the bracket: in "A:Elevated PSA(未確立惡性腫瘤)" 未確立 modifies the malignancy, so an Elevated PSA Condition is not reported (an established malignancy Condition is); a bracket holding only a cue ("肺炎(待排除)", "(r/o)", "(?)") qualifies the item before it.

How the primary diagnosis slot is read: the text after the heading (lexicon diagnosisSlot, Chinese + English; 次診斷 / comorbidities are never primary) and its continuation lines (numbered items) are split into items at top-level separators (、 ; , / vs 或 …); a leading prefix cue (鑑別 A/B/C, 待排除 A、B, r/o A vs B) governs the items after it on the same line, a trailing cue-only item on a line (…,尚未確立) governs the items before it; a line saying 已排除 / ruled out / 不支持 lists excluded items (neither established nor open). An unqualified item of any primary slot that matches the Condition always counts as established.

R6 corrections for wrong ROC conversions (recordChecks/dateSuggestions.ts): when a date is judged ungrounded (the record checks' ungrounded-value, and the grounding G-UNGROUNDED / G-VALUE-MISMATCH issues returned by fhir_validate_bundle / fhir_validate_resource / fhir_assemble_bundle) and the record states the same month / day with another year — typically a wrong ROC ↔ Gregorian conversion (record 民國92年8月25日 written as 1983-08-25, correct 2003-08-25; 民國47年6月20日 as 1959-06-20, correct 1958-06-20; +1900 / +1910 / +1912, the ROC year written as Gregorian such as 0092-08-25; a year-only or year-month value only for these known mistakes and ±1 / ±11 years), or a Gregorian record date with that month / day — the issue gets suggestions: kind: "replace_value", patch: {op: "replace", path, value}, the record's date as value (the original time part kept) and a reason quoting the record and stating the rule (ROC year N = Gregorian N+1911). One candidate: confidence 0.95, and the record-check message names the correct value; several: all listed, those written under this field's label (出生日期 for birthDate …) first. The issue's severity is unchanged (still an error) and nothing is applied. fhir_check_grounding's own output is unchanged (it is compared byte for byte with the reference implementation).

How R5 finds values (recordChecks/recordValues.ts): only the record body (the 【轉換目標】 conversion target and the 院所設定 section never count); identifiers are also searched in the whole site_config. - Identifiers: NFKC, case-insensitive, without - _ . / spaces; whole tokens, contiguous joins of separator pieces, leading zeros, a label glued in front (MRN00775310). System-assigned identifiers (urn:uuid, Bundle / Composition / Claim identifiers) are not checked. - Dates: Gregorian, ROC (114/06/10, 民國114年6月10日), 年月日, slashes, month-day (with the record's years), English month names; times as HH:MM(:SS), 上午 / 下午 N 點 M 分, am / pm, HHMM right after a date; a value in another offset is also compared in the site timezone (site_config timezone:, default +08:00). Derivations: 入院第 N 天 / hospital day N / HD#N (admission + N−1), 術後第 N 天 / POD#N (operation + N), 第 N 天, N 天前 / 後, 昨天 / 前天 / 今天 / 隔天 / 次日 / yesterday / next day, N weeks / months before / after, a complaint duration (咳嗽 3 天 → 3 or 2 days before), a course on a treatment line (處方 … 7 天 → start + 6 or 7), N 年前 / N 年 (years), an age (54 歲 → birth year), "N minutes / hours later / before". Chinese numerals are read (入院第三天). Anchors are the record's own dates: the admission date for hospital-day expressions, the operation date for post-op ones, else a date on the same line, else the nearest preceding dated line (birth dates excluded); Encounter.period.start and Patient.birthDate must be dates the record states. Record-keeping times (authoredOn, recordedDate, issued …) only warn about an ungrounded date and their time is not checked; timestamps the tools / documents write themselves (Bundle.timestamp, Composition.date, Provenance.recorded …) are not checked. - Quantities: every number outside dates and times (so a 0 or a 14 is never grounded by "08:00" or "2025/03/14"), with its unit, Chinese numerals, 半, fractions; it must have a compatible unit (when both units are known), or convert (mass / volume / length / time, °F ↔ °C, glucose mg/dL ↔ mmol/L, 10^3/uL ↔ /uL), BMI from height and weight, an explicit product (500 mg x 2). Derived quantities (dispenseRequest.quantity, expectedSupplyDuration, Claim item quantities) also accept a product of numbers on one line and only warn when not found; reference ranges warn; denominators are not checked.

How the primary Condition is decided (v15.3 R2, diagnosisSlot.ts primaryCondition): in order — (1) among the Encounter.diagnosis entries that reference a Condition of the instance and whose use is not CM (comorbidity), the lowest rank, else the first listed; only comorbidities → no primary; (2) otherwise the Conditions with category encounter-diagnosis (not problem-list-item): one → it; several → the first whose names match an item of the record's diagnosis slot, else the first listed; (3) otherwise the only Condition that is neither a problem-list item nor a comorbidity the record states outside its diagnosis slot (past history / plan / lab lines); (4) otherwise none and nothing is reported. Refuted / entered-in-error Conditions never count. Only that one Condition is examined.

Grounding messages on codes (v15.3 R3, recordChecks/codeGrounding.ts): with source_text, the G-UNGROUNDED / G-VALUE-MISMATCH issues that grounding reports on a coded element (CodeableConcept / Coding, e.g. Condition.code = 'sct#…') in fhir_validate_bundle / fhir_validate_resource (and behind them fhir_author_resources / fhir_assemble_bundle) are rewritten: the message says that the code's meaning — text, displays, the terminology names the tool looked up (English + Chinese) and the ICD-10 code — was compared with the record lines (conversion target, site settings and family history excluded), since a record never writes the code itself; the issue carries closest_record_phrase = {text, similarity, section} (the closest record clause; similarity = grounding's weighted name containment with abbreviation and zh↔en bridge expansion, and for Chinese vs Chinese also the character-bigram Dice after concept-search normalisation, whichever is larger; null below 0.1), compared and grounding_code_check. It stays an error only with clear counter-evidence (contradicted): it is the code of the encounter's primary Condition, unrelated to every record clause (similarity < 0.25), and the record has a diagnosis slot whose every item is comparable with it (same language, both with ICD-10 codes, or a zh→en bridge translating ≥ 80 % of the Chinese; abbreviation-only items never) and dissimilar — the record states a different diagnosis. Everything else becomes a warning with downgraded_from: "error": a related phrase exists (related, similarity ≥ 0.25 — the code may name it in other words) or nothing can be concluded (uncertain: another language, no diagnosis slot, comorbidities, medicines, lab codes). When a downgrade leaves an entry / the grounding result without errors, entries[i].valid, groundingValid, valid and the grounding error / warning counts are recomputed from the remaining issues; data.record_checks.grounding_codes = {examined, warnings, errors}. fhir_check_grounding's own output is unchanged (it is compared byte for byte with the reference implementation).

Chinese names of SNOMED codes (v15.3 R3 follow-up, grounding/snomedChinese.ts, fhir_validate_bundle 1.15.1 / fhir_validate_resource 1.12.1): SNOMED CT has English names only, so a Chinese diagnosis slot was never comparable and a wrong SNOMED primary diagnosis stayed uncertain. The comparison now also uses Chinese names derived from data (no hand-written translation list), each with a source and a confidence: IG zh designations / Chinese ValueSet displays (0.9 / 0.85); the SNOMED → ICD-10 map (snomed.icd10_map, map group 1, the highest-priority unconditional rule TRUE / OTHERWISE TRUE) → the ICD-10-CM Chinese name (target 0.75; 0.65 after conditional rules or when an ICD-10-CM child names it; 3-character category 0.6), without classification segments such as 未明示 / 無併發症 / 部位未明示; and the mined bilingual word pairs (fhir.term_thesaurus, source bilingual) applied word by word to the English names (only when ≥ 80 % of the content words translate; confidence 0.8 × coverage × the weakest word's Dice). Decision: Chinese names with confidence ≥ 0.5 join the closest-phrase comparison (related); only ≥ 0.6 make a Chinese slot item comparable; a comparable item names "something else" only when it is unrelated (< 0.25) to every English / Chinese name of the code and to every Chinese name of the mapped ICD 3-character category (0.3 — they can only prevent an error), and below 0.15 when only ICD-derived names make it comparable (the map is many-to-one and coarse); a slot ICD code in the mapped target's category is never different. Then it is an error (contradicted, the message names the Chinese name and its source), otherwise still a warning. The names used are listed in compared.chinese_names = [{name, source, confidence}]; neither fhir_check_grounding nor any other tool output contains them. 1.17.1 hardening: ICD Chinese names keep complete noun phrases only (qualifier prefixes such as 其他或未明示之 are removed whole; names starting with a function word / bound morpheme — 或氣喘, 性氣喘 — and tails one character short of another name are rejected); a mined-pair rendering that no ICD Chinese name of the concept corroborates is capped at 0.55 (related evidence only, never makes the slot comparable). Hierarchy guard: a diagnosis-slot item whose SNOMED concept (its ICD-10 code through the reverse snomed.icd10_map, or an identical ICD Chinese name) is the coded concept, an ancestor or a descendant of it (snomed.concept_ancestors) is never "something else" — lower respiratory tract infection vs 社區型肺炎 (J18.9) is a warning at most. Abbreviation-only slot items are judged on the record's own casing. The IG designation table loads in one cheap query (Chinese-language packages only, ~0.6 s on production), single-flight, refreshed in the background and warmed at startup; while the first load is running a request waits at most 2 s and then proceeds without IG names, with every Chinese name capped below the comparability threshold — results in that window may be warnings where later identical calls give errors, never the reverse.

Empty shells (v15.4, recordChecks/emptyShell.ts, fhir_validate_bundle 1.16.0): instead of writing 「報告未出」 as the value, the model creates a resource without a value and writes the reason in note / dataAbsentReason / dosageInstruction.text. Such resources follow the "empty shell" row above. The absence predicates are classes of patterns (ABSENCE_CLASSES in lexicon.ts, matched after NFKC + lower case), not a flat word list, so 報告尚未回傳 / 結果還沒出來 / 待 CT 正式報告 / "result not back yet" share one pattern; negations (無溶血, no hemolysis), clauses that state a result (negative, normal, a number with a unit) and clauses that only say 追蹤中 / 原因不明 are not absences. 溶血 / 凝固 / hemolyzed count only for an Observation whose code needs a quantitative result (a serum-appearance result can be 「溶血」). A named diagnosis with 「待確認」 written as provisional is the correct encoding of a suspected diagnosis and is not reported. A diagnosis slot that says "A 與 B 尚難區分 / cannot differentiate" is unestablished (A written as established gets unconfirmed-as-confirmed). dataAbsentReason is no longer an R1 value slot (it is FHIR's way to say the value is absent); it is side-field evidence.

Output

  • Each issue: severity, code (above), path, message (Chinese / English), message_zh, message_en, rule (R1–R5), source: "record-checks", evidence (the basis of the verdict), and located_quotes for the rules that need the record.
  • fhir_validate_bundle / fhir_validate_resource: issues go into entries[i].issues (Bundle-level ones into issues); an error makes entries[i].valid and data.valid false (conformanceValid / groundingValid keep their meaning); data.record_checks = {ok, errors, warnings, rules, source_text, notes?, grounding_codes?} is added when source_text is given or anything was found. Without source_text only R1 and R2 run (the stated absences and identifiers without value of R1, and R3–R5, need the record); without source_text and without any finding the output is byte-identical to the output before this layer (no record_checks, no extra provenance fields).
  • fhir_author_resources: as fhir_validate_resource, in results[].issues (an error makes the status invalid); the optional source_text (not stored on the context) adds R3 / R4; site_config format rules apply.
  • fhir_assemble_bundle: as fhir_validate_bundle (R3 / R4 with source_text), issues tagged with key / field.
  • fhir_finalize_bundle: optional source_text; results in data.record_issues and data.record_checks (the Bundle and changes are unchanged), plus a warning when any is an error.
  • fhir_finalize_resource: optional source_text; issues go into data.validation.issues (an error makes validation.valid false); the draft is never changed by them.
  • The same rule holds for every tool above: without source_text and without findings the output is unchanged.
  • When the layer reports, provenance gains recordChecksVersion, recordChecksLexicon, recordChecksLexiconSha256. The record text is never logged.

Tool versions

Tools whose behaviour is pinned by fixtures carry their own version (node-server/src/toolVersions.ts), bumped whenever their output for the same input changes. health_check reports the current values under tools_version; every other tool uses the server version (version, currently 1.1.0).

Tool Version
fhir_validate_bundle 1.13.0 (R6: correction suggestions for ungrounded dates with a wrong ROC conversion, also on the returned grounding issues); 1.12.0 (record checks v15.1: with source_text every identifier / date / quantity value must be in the record (ungrounded-value), values of another time point / event / field (value-from-other-event), stated absences and identifiers without value, the primary diagnosis slot, bracket-scoped cues; output without source_text unchanged); 1.11.2 (normalize_ids + repair="content" as fhir_finalize_bundle 1.3.2); 1.11.1 (content repairs with normalize_ids + repair="content" as fhir_finalize_bundle 1.3.1); 1.11.0 (record checks: information-absent / identifier-format always; with source_text also precision, values, free-text medicines and unconfirmed diagnoses; data.record_checks); 1.10.0 (repair="content" with normalize_ids=true: content repairs before validation, content_changes / repair_issues); 1.9.0 (validator v13: a ValueSet canonical as Coding.system is an error, LOINC property filters decide membership); 1.8.0 (normalized_bundle keeps number spellings; bdl-11 / bdl-12 on a document / message without its head resource carry replace_value / add_resource suggestions; 1.7.0: normalize_ids, ID-class suggestions and id_map / id_changes)
fhir_validate_resource 1.10.0 (R6, as fhir_validate_bundle 1.13.0); 1.9.0 (record checks as fhir_validate_bundle 1.12.0); 1.8.0 (record checks, as fhir_validate_bundle 1.11.0); 1.7.0 (validator v13: a ValueSet canonical as Coding.system is an error with candidates, LOINC property filters decide membership); 1.6.2 (validator v8: unknown HL7-space CodeSystems always errors, whitespace in references, RxNorm displays naming another drug, exception issues for failed lookups / crashes; 1.6.0: issue suggestions / caused_by)
fhir_finalize_bundle 1.4.1 (R6: correction suggestions on ungrounded dates in record_issues); 1.4.0 (data.record_issues with source_text as the record checks of fhir_validate_bundle 1.12.0); 1.3.2 (evaluator decisions: a display error exposed by the system correction of the same Coding no longer withdraws it; a profile allowing exactly one unit sets it, recorded in changes); 1.3.1 (v14.1 content repairs: rules timezone / empty / fixed-slice, UCUM equivalents and annotations, quantity-code-not-ucum, ValueSet-named and renamed systems, more shapes, the Bundle envelope and duplicate fullUrls, whole-Bundle verification, site_config argument); 1.3.0 (record checks: optional source_text, data.record_issues / record_checks, report only); 1.2.0 (repair, default content: display / ucum / roc-date / system / shape / code content repairs, verified to add no error, repair_issues, timezone; repair="ids" = 1.1.0); 1.1.0 (number spellings kept, bundle_type argument, hint for a document without a Composition; 1.0.1: Bundle finalisation: fullUrl-uuid / reference-rewire / resource-id / bundle-type / document-order)
fhir_build_bundle 1.2.1 (entry resources keep number spellings; 1.2.0: shares the finalisation engine: deterministic UUIDs, key / urn:uuid:key / Type/key references rewired, invalid fullUrls normalised, changes)
fhir_get_profile_required_elements 1.4.1 (bundle_rules.bundle_type names fhir_finalize_bundle's bundle_type; 1.4.0: bundle_rules for Bundle profiles; 1.3.0: derived rows: allowedCodes + derivation rule; include_mustsupport="defaults"; optional)
fhir_list_igs, fhir_list_artifacts, fhir_search_artifacts, fhir_list_resource_profiles (new compact) 1.3.0
fhir_get_ig, fhir_rank_resource_profiles, fhir_get_profile, fhir_get_profile_elements, fhir_get_valueset, fhir_expand_valueset, fhir_get_resource_skeleton 1.2.0
fhir_validate_code 1.3.0 (= / in property filters on LOINC (CLASSTYPE, CLASS, STATUS …) are decided from the local LOINC tables; results that were unverifiable may now be valid / invalid)
fhir_finalize_resource 1.8.1 (R6: correction suggestions on ungrounded dates in validation); 1.8.0 (record checks in validation as fhir_validate_resource 1.9.0); 1.7.2 (as fhir_finalize_bundle 1.3.2); 1.7.1 (content repairs as fhir_finalize_bundle 1.3.1, site_config argument, timezone takes IANA names); 1.7.0 (record checks merged into validation, optional source_text); 1.6.0 (repair / timezone: the content repairs of fhir_finalize_bundle 1.2.0, changes / repair_issues); 1.5.0 (validation rules as fhir_validate_resource 1.7.0); 1.4.0 (slice members pinned by their discriminators, _slice tags at any depth and always removed; 1.3.2: resource keeps the draft's number spellings; 1.3.1: validation rules as fhir_validate_resource 1.6.1; 1.3.0: validation issue suggestions / caused_by)
fhir_lookup_code 1.3.2 (RxNorm codes)
fhir_normalize_code 1.8.0 (excerpt CodeSystems → the installed full copy, candidate linked, aliases resolved outside the default IG)
fhir_check_grounding 1.1.0
fhir_check_completeness 1.1.1
fhir_author_resources 1.6.1 (R6: correction suggestions on ungrounded dates); 1.6.0 (validation as fhir_validate_resource 1.9.0: the v15.1 record checks with source_text); 1.5.2 (as fhir_finalize_bundle 1.3.2); 1.5.1 (content repairs as fhir_finalize_bundle 1.3.1; a timezone line of the context's site_config is the site timezone); 1.5.0 (record checks issues in results[].issues, optional source_text); 1.4.0 (system correction and display check moved to the shared repair layer: a display is replaced only for the same concept, a different meaning is kept with a warning and candidates; UCUM / ROC / system-name / shape repairs as in the finalize tools; repairs never add a validation error); 1.3.0 (a filter-defined ValueSet written as the system is corrected too, the note says whether membership was confirmed or unverifiable; an uncorrected ValueSet system is an error, validation as 1.7.0); 1.2.0 (a ValueSet URL / an unresolvable URL in Coding.system is corrected to the right CodeSystem when unique, with a note); 1.1.0 (the display follows the code, single values wrapped into arrays, defaults only on required elements and apply_defaults, site_config, notes / settings in the result; 1.0.0: authoring from field values, see "Authoring from field values")
fhir_assemble_bundle 1.6.0 (R6, validation as fhir_validate_bundle 1.13.0); 1.5.0 (resources as fhir_author_resources 1.6.0, validation as fhir_validate_bundle 1.12.0); 1.4.2 (as fhir_finalize_bundle 1.3.2); 1.4.1 (resources as fhir_author_resources 1.5.1, finalize as fhir_finalize_bundle 1.3.1); 1.4.0 (validation as fhir_validate_bundle 1.11.0, incl. record checks); 1.3.0 (finalize always with repair="content", repair_issues, resources as fhir_author_resources 1.4.0); 1.2.0 (resources as fhir_author_resources 1.3.0, validation as fhir_validate_bundle 1.9.0); 1.1.0 (site_config merged with the context's and passed to grounding; 1.0.0: Bundle from a list of keys)
fhir_get_authoring_fields 1.2.0 (record links: a backbone linking another record resource of the same IG (Encounter.diagnosis) is a field with its children and a {condition: {ref}} example, a direct reference to one is in more with targets; the size budget covers the whole response, new response_chars); 1.1.0 (defaults are auto only on required elements; optional ones show the conventional value as a hint; 1.0.0: the profile's writing form)
fhir_get_bundle_blueprint, fhir_get_minimal_skeleton 1.0.0 (v16: Bundle blueprint and minimal skeleton with value sources, see "IG structure")
fhir_context 1.0.1 (get results carry notes)
query_fhir_condition, query_fhir_medication 1.2.0 (no element is stamped with the current time any more — recordedDate / note.time / meta.lastUpdated appear only when the caller supplies them)