Skip to content

feat(validator): FHIR resource validation — schema engine, $validate, write-path enforcement#232

Draft
dougc95 wants to merge 9 commits into
mainfrom
feat/fhir-validator
Draft

feat(validator): FHIR resource validation — schema engine, $validate, write-path enforcement#232
dougc95 wants to merge 9 commits into
mainfrom
feat/fhir-validator

Conversation

@dougc95

@dougc95 dougc95 commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds FHIR resource validation to HFS: a new helios-fhir-validator crate (a runtime FHIR Schema engine), the $validate operation, and optional write-path enforcement driven by HFS_VALIDATION_* config — across R4 / R4B / R5 / R6.

70 files changed, ~8.9k insertions.

Relationship to the Validation strategy discussion

This work is a response to the architecture strategy document in discussions/215 — "Validation". It delivers most of the functional scope that discussion commits to, but takes a deliberately different implementation path, so it's offered for review/discussion rather than as a literal implementation of that doc.

Where this PR agrees with #215:

  • FHIR Schema as the internal validation IR.
  • An SD → FHIRSchema converter validated against the external FHIR Schema conformance suite (the doc's highest-risk Stage 1, proven in isolation).
  • Invariants delegated to helios-fhirpath (not re-implemented) — exactly as the doc argues for.
  • OperationOutcome as the public contract; per-version R4/R4B/R5/R6 dispatch.
  • The $validate operation + write-path validation config surface.

Where this PR diverges from #215 (worth a maintainer decision):

#215 proposes This PR does
Compile SD→FHIRSchema→**&'static Rust tables** via extending helios-fhir-gen + a new #[derive(FhirValidate)] macro in helios-fhir-macro A standalone helios-fhir-validator crate with a runtime engine that loads embedded compressed schema packs and interprets them (mirrors the fhir-schema reference impl)
Crate named helios-validation Crate named helios-fhir-validator
Local-first bundled terminology tables + in-process helios-hts $validate-code fallback Strength-aware binding checks + an optional remote $validate-code TerminologyProvider (no bundled local tables, no HTS wiring yet)
Checked-in reviewable .fhirschema.json artifacts per version Schema packs shipped as compressed *.json.gz blobs (regenerated by the generate-schema-packs bin)

Aspect coverage (against #215's aspects table)

Aspect Status
Structure / Cardinality / Value domains (primitives) ✅ implemented
Invariants (FHIRPath) ✅ via helios-fhirpath
Profiles — base-chain resolution, meta.profile + stored/tenant profiles, slicing, fixed/pattern, choice types ✅ implemented
Coding/CodeableConcept bindings 🟡 strength-aware; terminology resolution is optional/remote-only (not local-first+HTS)
References (Reference.type target profiles) 🟡 IR carries refers; target-profile checking is a follow-up
Questionnaires / Business rules ⛔ deferred by design (as #215 also defers them)

Operational surface

  • $validate — accepts a raw resource or a Parameters wrapper (resource/mode/profile), honors ?profile= and ?mode=, supports create/update/delete/profile modes, and always returns 200 OK + OperationOutcome (non-200 only for malformed calls, e.g. mode=profile with no profile). mode=delete does no content validation.
  • Write-path enforcementHFS_VALIDATION_MODE = off | log | enforce; enforce rejects non-conformant create/update with 422 Unprocessable Entity + OperationOutcome. Off by default.
  • ConfigHFS_VALIDATION_MODE, HFS_VALIDATION_UNKNOWN_PROFILE (error|warn|ignore), HFS_VALIDATION_TERMINOLOGY / _TERMINOLOGY_FAIL / _TERMINOLOGY_TIMEOUT_MS, HFS_VALIDATION_CONSTRAINTS / _SUPPRESS_CONSTRAINTS, HFS_VALIDATION_META_PROFILES, HFS_VALIDATION_STORED_PROFILES.
  • validator-cli (cli feature) for standalone resource validation.

Testing

  • Conformance (tests/conformance.rs) against vendored upstream FHIR Schema fixtures (tests/fixtures/upstream/).
  • Converter, extended-aspect, FHIRPath-constraint, and deferred-effects tests.
  • All-version pack smoke, fuzz-lite robustness sweep, and a latency smoke test.

Not in this PR (follow-ups from #215)

  • #[derive(FhirValidate)] / helios-fhir-gen codegen path (this PR uses the runtime-engine alternative instead).
  • Local-first terminology tables + in-process helios-hts fallback.
  • Inferno US Core $validate nightly + conformance badge.
  • Runtime SD-supplied profiles; type/profile slice discriminators (per Validation #215, staged).

🤖 Generated with Claude Code

dougc95 and others added 7 commits July 9, 2026 07:47
…dation engine

Phase 1 of the FHIR Schema based validator (see fhir-schema sibling repo):
- FhirSchema serde model (differential, order-preserving, Arc-shared elements)
- SchemaResolver trait + SchemaRegistry/CompositeResolver (SearchParameter
  registry pattern)
- Cooperative schema-set walk: base layering, complex-type resolution,
  unknown-element, array/scalar shape, required/excluded, choices/choiceOf,
  fixed/pattern, numeric min/max, dynamic Resource resolution
  (Bundle.entry.resource, contained)
- Deferred-effects collection for constraints/bindings (executed in a later
  phase)
- Vendored upstream FHIR Schema conformance suite (MIT, provenance in
  UPSTREAM.md) with exact ordered error matching; fixtures 1,2,3,4,7 green,
  5/6 gated until slicing lands

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Phase 2 — the full upstream conformance suite is now green (all 7 fixtures):
- engine/slicing.rs: mark/sweep slicing with verbatim cardinality messages;
  closed/openAtEnd/ordered rules, @default absorption, and prohibited
  max:0 slices (all beyond the reference validator, pinned by extended
  fixtures)
- extensions maps compile into synthetic {url}-matched slicing on the
  extension element at validation time
- _field primitive-extension sidecars: base-element pairing, shape
  mirroring, Element-part validation with null gaps
- elementReference resolution for recursive structures (Questionnaire.item)
- extended fixture suite pinning Helios-specific semantics (excluded,
  numeric min/max, fixed/pattern wording, slicing rules, sidecars,
  elementReference, required-satisfied-by-choice-branch)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…idator-cli

Phase 3:
- converter/: StructureDefinition (raw JSON, snapshot OR differential) to
  FhirSchema — ED-id tree reconstruction, choice [x] expansion,
  discriminator-to-pattern-match translation, extension-slicing to
  extensions sugar, contentReference to elementReference, targetProfile to
  refers, min/max to required/excluded/cardinality, ele-1/ext-1 dedup,
  primitive value-regex extraction. Golden tests pin every mapping.
- generate-schema-packs bin (gen-packs feature): converts
  crates/fhir-gen/resources/{V}/profiles-{types,resources,others}.json.
  All four versions convert with ZERO failures: R4 257, R4B 247, R5 296,
  R6 228 schemas; packs total ~437 KB gzipped, committed under packs/.
- packs.rs: include_bytes + per-version OnceLock core registries
  (profiles-first insertion so core names win aliases).
- pack_smoke (#[ignore]): real-world Patient with choice types, sidecars,
  contained resources and extensions validates clean against the converted
  R4 core; structural breakage and Bundle dynamic resolution verified.
- validator-cli bin (cli feature): file/stdin, --fhir-version, --profile,
  --profile-file (converts SDs on the fly), text/json output, exit 0/1/2.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…aints, terminology bindings

Phase 4:
- engine/primitives.rs: JSON type-class checks from the primitive's declared
  type + anchored spec-regex checks (process-wide compiled-regex cache);
  kind 'primitive-value'. Upstream fixtures declare bare primitives, so the
  exact-match conformance contract is untouched (pinned by
  extended/primitives.json).
- effects.rs: execute() pipeline — constraints deduped by (path,id) across
  cooperative layers, severity mapping (error/warning; guideline skipped),
  suppress list, not-evaluable surfaced as warnings; bindings filtered to
  required strength, coded shape from declared type with value-shape
  fallback, fail-open/fail-closed terminology outages. Async
  TerminologyProvider (async-trait), sync ConstraintEvaluator with
  batch evaluate_all.
- Validator::validate(): sync walk + effects execution; validate_sync
  remains pure.
- fhirpath_effects.rs (feature 'fhirpath'): typed-parse-once per resource,
  focus via 'path.all(expr)' wrapping, FHIR invariant semantics
  (empty → pass; the JS reference fails empty — known bug there),
  Arc-cached expression ASTs.
- 6 stub-driven effects tests + real-invariant test (pat-1 from the R4
  pack; binary carries an MSVC 14.44 LNK1318 caveat, scenarios also covered
  via the rest integration suite).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Phase 5:
- routes: POST /{type}/$validate and GET|POST /{type}/{id}/$validate
  (before the catch-all; GET validates the stored resource) — the
  CapabilityStatement's long-advertised validate operation is now real.
- handlers/validate.rs: raw-resource or Parameters bodies (resource/mode/
  profile parts, query fallbacks), mode=create|update|delete|profile
  semantics, always-200-OperationOutcome contract (4xx only for malformed
  requests).
- validation.rs: ValidationService (embedded core packs per version,
  FHIRPath constraint evaluator with dom-6 suppressed by default,
  meta.profile honored, pluggable TerminologyProvider) + ErrorKind →
  IssueType mapping with bracket-indexed FHIRPath expressions.
- OperationOutcome: IssueType gains Invariant and CodeInvalid.
- AppState carries the service (always present; write-path gating comes
  with HFS_VALIDATION_* in the next phase).
- 12 integration tests incl. real spec invariant (pat-1) evaluation
  through the full HTTP stack.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…g, tenant profiles

Phase 6:
- ValidationConfig (HFS_VALIDATION_MODE off|log|enforce, META_PROFILES,
  UNKNOWN_PROFILE, CONSTRAINTS, SUPPRESS_CONSTRAINTS, TERMINOLOGY off|remote,
  TERMINOLOGY_TIMEOUT_MS, TERMINOLOGY_FAIL open|closed, STORED_PROFILES)
  following the BulkExportConfig pattern; remote terminology requires
  HFS_TERMINOLOGY_SERVER (validated).
- ValidationService::from_config + check_write: enforce mode rejects invalid
  writes with 422 carrying the full multi-issue OperationOutcome (new
  RestError::ValidationFailed); log mode traces and proceeds.
- Hooks on every write path: create, update, conditional create/update,
  batch (per-entry 422 results), and transaction (atomic pre-execution
  rejection).
- Per-tenant profile registries: stored StructureDefinitions convert and
  register on write (all write paths), overlay the core packs via
  CompositeResolver for $validate, meta.profile, and enforcement.
  In-memory since process start (startup warm-load is future work).
- RemoteTerminologyProvider: ValueSet/$validate-code against
  HFS_TERMINOLOGY_SERVER with a 300s verdict cache.
- Docs: config env table, run-hfs-server skill section, CLAUDE.md workspace
  table (validator + web crates).
- 6 enforcement integration tests (enforce/log/off matrix, batch per-entry,
  stored-profile $validate round-trip, meta.profile enforcement); rest and
  batch conformance suites still green (39 + 57).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…latency smoke

Phase 7 (quick wins; remaining hardening documented in lib.rs):
- fuzz_lite: 2500 deterministic mutations of a rich Patient plus pure-junk
  roots — the walk never panics on arbitrary JSON.
- pack_smoke: every enabled version's embedded pack (R4/R4B/R5/R6) loads,
  resolves core schemas, validates a Patient, and catches unknown elements.
- Latency smoke: ~300µs per typical Patient structural validation in DEBUG
  builds — the 5ms release-plan target is beaten by an order of magnitude.
- lib.rs 'Current limitations' section pins the hardening backlog:
  type/profile/binding/resolve-ref slice matchers, refers enforcement,
  extensible-binding warnings, schema-set memoization, hts in-process
  terminology adapter, core extension-definitions vendoring.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@dougc95 dougc95 marked this pull request as draft July 9, 2026 12:17
Resolve conflicts:
- crates/rest/Cargo.toml: keep both helios-fhir-validator and helios-observability deps
- handlers/mod.rs: keep both user_settings and validate modules + re-exports
- handlers/batch.rs: combine our StructureDefinition profile-upsert with main's entry_error handling
- error.rs: ValidationFailed surfaces its full OperationOutcome via into_response; add a fallback triple in client_response
- Cargo.lock: regenerated

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
# Conflicts:
#	crates/rest/src/routing/fhir_routes.rs
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants