Skip to content

Commit 5e5f668

Browse files
authored
Merge pull request #2 from medvertical/claude/validation-engine-extensions
Extend structural validation: schema, references, slices, orchestrator
2 parents 09afe05 + 30b6591 commit 5e5f668

19 files changed

Lines changed: 931 additions & 166 deletions

.claude-plugin/marketplace.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
"source": "./plugins/records",
1313
"description": "FHIR validation and data-quality workflows with Records.",
1414
"category": "development",
15-
"version": "0.4.0",
15+
"version": "0.5.0",
1616
"author": {
1717
"name": "MedVertical"
1818
},

README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
# Records for Claude Code
1010

1111
[![Plugin CI](https://github.com/medvertical/claude-records/actions/workflows/plugin-ci.yml/badge.svg)](https://github.com/medvertical/claude-records/actions/workflows/plugin-ci.yml)
12-
![Version](https://img.shields.io/badge/version-0.4.0-blue)
12+
![Version](https://img.shields.io/badge/version-0.5.0-blue)
1313
[![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](./LICENSE)
1414

1515
FHIR validation and data-quality workflow skills for Claude Code.
@@ -63,6 +63,7 @@ Example prompts:
6363
The plugin includes focused commands that route into the `fhir-validation` workflow:
6464

6565
- `/records:doctor` - diagnose FHIR/IG project structure, runtimes, and privacy boundaries.
66+
- `/records:validate` - run end-to-end local structural validation (detect, validate, explain, map).
6667
- `/records:init-ci` - generate or update Records FHIR validation CI.
6768
- `/records:explain-outcome` - explain FHIR `OperationOutcome` issues and safe fixability.
6869
- `/records:derive-quality-rules` - derive reviewable project quality rules from local evidence.
@@ -92,7 +93,7 @@ The FHIR validation skill guides Claude through five validation paths:
9293

9394
The local CLI fallback uses Records' packaged FHIR R4 structural schema for resource types, required fields, unknown fields, cardinality, primitive types, choice fields, and simple backbone children. It does not replace profile, terminology, invariant, reference, metadata, advisor-rule, anomaly, or evidence-report validation.
9495

95-
Executable helper scripts support deterministic project detection (including FHIR package-cache and dependency resolution), local structural fallback validation, StructureDefinition snapshot/slicing analysis, generated FSH source mapping, OperationOutcome explanation, PHI-minimizing summaries, quality-rule derivation, CI YAML generation, and FHIR expression to JSON Pointer mapping.
96+
Executable helper scripts support deterministic project detection (including FHIR package-cache and dependency resolution), local structural fallback validation (multi-resource schema, primitive datatype formats, required choices, and contained/intra-Bundle reference integrity), an end-to-end validation orchestrator, StructureDefinition snapshot/slicing analysis and instance-based slice matching, generated FSH source mapping, OperationOutcome explanation, PHI-minimizing summaries, quality-rule derivation, CI YAML generation, and FHIR expression to JSON Pointer mapping.
9697

9798
## Repository Scope
9899

@@ -137,7 +138,7 @@ Canonical installation is via `medvertical/claude-records`. The ClaudeRegistry s
137138

138139
## Release Notes
139140

140-
See [plugins/records/eval-results/v0.4.0.md](./plugins/records/eval-results/v0.4.0.md) for the current release checks and scope.
141+
See [plugins/records/eval-results/v0.5.0.md](./plugins/records/eval-results/v0.5.0.md) for the current release checks and scope.
141142

142143
## Development
143144

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "claude-records",
33
"private": true,
4-
"version": "0.4.0",
4+
"version": "0.5.0",
55
"type": "module",
66
"scripts": {
77
"test": "node plugins/records/scripts/smoke-test.mjs && node plugins/records/scripts/eval-fixtures.mjs",

plugins/records/.claude-plugin/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "records",
33
"description": "FHIR validation and data-quality workflow skills for Claude Code.",
4-
"version": "0.4.0",
4+
"version": "0.5.0",
55
"author": {
66
"name": "MedVertical"
77
},
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
---
2+
description: Run end-to-end local structural FHIR validation (detect, validate, explain, map) with the Records fhir-validation skill.
3+
argument-hint: "[file-or-directory]"
4+
---
5+
6+
# Records FHIR Validate
7+
8+
Use `/records:fhir-validation` behavior for this validation task. Prefer a
9+
profile-aware runtime (Records CLI/MCP/API, IG Publisher, HAPI, Firely) when
10+
one is configured; this command runs the local **structural fallback** and must
11+
be labeled as such.
12+
13+
Target: `$ARGUMENTS` or the current working directory.
14+
15+
Run the orchestrator, which detects project context for directories, validates
16+
each resource, and enriches every issue with fixability guidance and a JSON
17+
Pointer:
18+
19+
```bash
20+
node "${CLAUDE_PLUGIN_ROOT}/skills/fhir-validation/scripts/validate.mjs" "$ARGUMENTS"
21+
```
22+
23+
If no argument was provided, use `.`. Summarize:
24+
25+
1. Mode and privacy boundary (structural fallback; not profile/terminology/
26+
invariant/cross-document-reference aware).
27+
2. Totals: resources scanned, errors, warnings, information.
28+
3. Errors first, grouped by file and path, with the JSON Pointer and whether
29+
the fix is mechanical, domain input, or setup/package repair.
30+
4. For directories, surface the detector's recommended order and any missing
31+
package dependencies.
32+
33+
Do not edit files unless the user explicitly asks for fixes, and do not claim
34+
profile conformance from this structural run.
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Records Claude Plugin Eval Results v0.5.0
2+
3+
## Scope
4+
5+
Validation-engine extensions release, deepening the local structural fallback
6+
toward what the OSS validators (HL7 reference validator, HAPI, Firely) do:
7+
8+
- Expand the embedded R4 structural schema to Condition, Encounter, Procedure,
9+
MedicationRequest, and DiagnosticReport, plus primitive datatype format checks
10+
(`date`, `dateTime`, `instant`, `time`, `boolean`, `integer`, `uri`) and
11+
required `choice[x]` elements (`r4-primitives.mjs`).
12+
- Add reference integrity checks: contained (`#id`) references must resolve, and
13+
inside a Bundle relative (`Type/id`) and `urn:` references are resolved against
14+
entry `fullUrl`s and resource ids.
15+
- Add instance-based slice matching (`match-slices.mjs`) that evaluates value and
16+
pattern discriminators against an instance to attribute array entries to named
17+
slices.
18+
- Add an end-to-end orchestrator (`validate.mjs`) and a `/records:validate`
19+
command that detect project context, validate each resource, and enrich every
20+
issue with fixability guidance and a JSON Pointer. The FHIRPath-to-pointer
21+
logic is now a shared library (`lib/fhirpath-pointer.mjs`).
22+
- Broaden smoke and fixture eval coverage across all of the above.
23+
24+
## Automated Checks
25+
26+
- `npm test`
27+
- `npm run release:check`
28+
- `npx --yes @anthropic-ai/claude-code plugin validate .`
29+
- `npx --yes @anthropic-ai/claude-code plugin validate plugins/records`
30+
- `node plugins/records/skills/fhir-validation/scripts/validate.mjs plugins/records/fixtures/mini-ig`
31+
- `node plugins/records/skills/fhir-validation/scripts/validate-structural.mjs plugins/records/fixtures/condition-missing-subject.json`
32+
- `node plugins/records/skills/fhir-validation/scripts/match-slices.mjs plugins/records/fixtures/structuredefinition-sliced.json plugins/records/fixtures/observation-sliced-instance.json`
33+
34+
## Release Decision
35+
36+
Ship v0.5.0 as the validation-engine extensions release for
37+
`records@medvertical`.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"resourceType": "Condition",
3+
"id": "condition-missing-subject",
4+
"clinicalStatus": {
5+
"coding": [
6+
{
7+
"system": "http://terminology.hl7.org/CodeSystem/condition-clinical",
8+
"code": "active"
9+
}
10+
]
11+
},
12+
"recordedDate": "2026-13-02"
13+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
{
2+
"resourceType": "Observation",
3+
"id": "observation-sliced-instance",
4+
"status": "final",
5+
"code": {
6+
"coding": [
7+
{
8+
"system": "http://loinc.org",
9+
"code": "85354-9"
10+
}
11+
]
12+
},
13+
"category": [
14+
{
15+
"coding": [
16+
{
17+
"system": "http://terminology.hl7.org/CodeSystem/observation-category",
18+
"code": "vital-signs"
19+
}
20+
]
21+
},
22+
{
23+
"coding": [
24+
{
25+
"system": "http://terminology.hl7.org/CodeSystem/observation-category",
26+
"code": "laboratory"
27+
}
28+
]
29+
},
30+
{
31+
"coding": [
32+
{
33+
"system": "http://terminology.hl7.org/CodeSystem/observation-category",
34+
"code": "social-history"
35+
}
36+
]
37+
}
38+
]
39+
}

plugins/records/fixtures/structuredefinition-sliced.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,22 @@
3131
"min": 1,
3232
"max": "1"
3333
},
34+
{
35+
"id": "Observation.category:vital.coding.code",
36+
"path": "Observation.category.coding.code",
37+
"patternCode": "vital-signs"
38+
},
3439
{
3540
"id": "Observation.category:lab",
3641
"path": "Observation.category",
3742
"sliceName": "lab",
3843
"min": 0,
3944
"max": "1"
45+
},
46+
{
47+
"id": "Observation.category:lab.coding.code",
48+
"path": "Observation.category.coding.code",
49+
"patternCode": "laboratory"
4050
}
4151
]
4252
}

plugins/records/scripts/smoke-test.mjs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,11 @@ const required = [
9292
"plugins/records/skills/fhir-validation/references/structural-validation.md",
9393
"plugins/records/skills/fhir-validation/scripts/lib/operationoutcome-issues.mjs",
9494
"plugins/records/skills/fhir-validation/scripts/lib/r4-structural-schema.mjs",
95+
"plugins/records/skills/fhir-validation/scripts/lib/r4-primitives.mjs",
96+
"plugins/records/skills/fhir-validation/scripts/lib/fhirpath-pointer.mjs",
9597
"plugins/records/skills/fhir-validation/scripts/validate-structural.mjs",
98+
"plugins/records/skills/fhir-validation/scripts/validate.mjs",
99+
"plugins/records/skills/fhir-validation/scripts/match-slices.mjs",
96100
"plugins/records/skills/fhir-validation/scripts/generate-issue-map-doc.mjs",
97101
"plugins/records/skills/fhir-validation/scripts/analyze-structuredefinition.mjs",
98102
"plugins/records/skills/fhir-validation/scripts/detect-fhir-project.mjs",
@@ -106,6 +110,7 @@ const required = [
106110
"plugins/records/commands/init-ci.md",
107111
"plugins/records/commands/explain-outcome.md",
108112
"plugins/records/commands/derive-quality-rules.md",
113+
"plugins/records/commands/validate.md",
109114
"plugins/records/agents/fhir-validation-reviewer.md",
110115
"plugins/records/fixtures/invalid-observation.json",
111116
"plugins/records/fixtures/operationoutcome-required.json",
@@ -168,10 +173,14 @@ for (const script of [
168173
"plugins/records/skills/fhir-validation/scripts/generate-ci.mjs",
169174
"plugins/records/skills/fhir-validation/scripts/map-fhir-expression.mjs",
170175
"plugins/records/skills/fhir-validation/scripts/validate-structural.mjs",
176+
"plugins/records/skills/fhir-validation/scripts/validate.mjs",
177+
"plugins/records/skills/fhir-validation/scripts/match-slices.mjs",
171178
"plugins/records/skills/fhir-validation/scripts/generate-issue-map-doc.mjs",
172179
"plugins/records/skills/fhir-validation/scripts/analyze-structuredefinition.mjs",
173180
"plugins/records/skills/fhir-validation/scripts/lib/operationoutcome-issues.mjs",
174181
"plugins/records/skills/fhir-validation/scripts/lib/r4-structural-schema.mjs",
182+
"plugins/records/skills/fhir-validation/scripts/lib/r4-primitives.mjs",
183+
"plugins/records/skills/fhir-validation/scripts/lib/fhirpath-pointer.mjs",
175184
]) {
176185
const scriptStat = await stat(path.join(repo, script));
177186
if (!scriptStat.isFile()) errors.push(`Script is not a file: ${script}`);
@@ -281,6 +290,18 @@ function runJsonInput(script, input) {
281290
}
282291
}
283292

293+
// Run an arbitrary JSON-emitting script with file args and return status+parsed.
294+
function runValidatorScript(script, args) {
295+
const result = spawnSync(process.execPath, [script, ...args], { cwd: repo, encoding: "utf8" });
296+
let parsed = null;
297+
try {
298+
parsed = JSON.parse(result.stdout);
299+
} catch {
300+
errors.push(`${rel(script)} did not output JSON: ${result.stderr || result.stdout}`);
301+
}
302+
return { status: result.status, parsed };
303+
}
304+
284305
const invalidObs = runValidator([path.join(plugin, "fixtures/invalid-observation.json")]);
285306
if (invalidObs.parsed) {
286307
if (invalidObs.status !== 1) errors.push("Structural validator should exit 1 for the invalid Observation.");
@@ -403,6 +424,46 @@ if (!sushiCi.includes("sushi .")) errors.push("CI generator --sushi should inclu
403424
const uploadCi = spawnSync(process.execPath, [ciGen, "--upload-artifact"], { cwd: repo, encoding: "utf8" }).stdout;
404425
if (!uploadCi.includes("upload-artifact")) errors.push("CI generator --upload-artifact should add an artifact upload step.");
405426

427+
// --- v0.5.0: expanded schema, primitives, required choice, references ---
428+
const condition = validatorCodes([path.join(plugin, "fixtures/condition-missing-subject.json")]);
429+
if (!condition.codes?.includes("required:Condition.subject")) errors.push("Validator should require Condition.subject.");
430+
if (!condition.codes?.some((entry) => entry.startsWith("value:Condition.recordedDate"))) errors.push("Validator should flag the malformed Condition.recordedDate primitive.");
431+
432+
const badPrimitive = validatorCodes([], '{"resourceType":"Patient","id":"x","birthDate":"2020-13-01","active":"yes"}');
433+
if (!badPrimitive.codes?.some((entry) => entry.startsWith("value:Patient.birthDate"))) errors.push("Validator should flag an invalid date primitive.");
434+
if (!badPrimitive.codes?.some((entry) => entry.startsWith("value:Patient.active"))) errors.push("Validator should flag a non-boolean primitive.");
435+
436+
const reqChoiceMissing = validatorCodes([], '{"resourceType":"MedicationRequest","id":"m","status":"active","intent":"order","subject":{"reference":"Patient/p"}}');
437+
if (!reqChoiceMissing.codes?.some((entry) => entry.includes("required:MedicationRequest.medication[x]"))) errors.push("Validator should require a medication[x] choice.");
438+
const reqChoicePresent = validatorCodes([], '{"resourceType":"MedicationRequest","id":"m","status":"active","intent":"order","subject":{"reference":"Patient/p"},"medicationCodeableConcept":{}}');
439+
if (reqChoicePresent.codes?.some((entry) => entry.includes("MedicationRequest.medication[x]"))) errors.push("Validator should accept a satisfied medication[x] choice.");
440+
441+
const containedMissing = runValidator([], '{"resourceType":"Observation","status":"final","code":{},"subject":{"reference":"#p1"}}');
442+
if (containedMissing.parsed?.summary.error !== 1 || !containedMissing.parsed?.operationOutcome.issue.some((entry) => entry.severity === "error" && entry.code === "not-found")) {
443+
errors.push("Validator should flag an unresolved contained reference as an error.");
444+
}
445+
const bundleUnresolved = runValidator([], '{"resourceType":"Bundle","type":"collection","entry":[{"resource":{"resourceType":"Observation","status":"final","code":{},"subject":{"reference":"Patient/missing"}}}]}');
446+
if (bundleUnresolved.parsed?.summary.warning !== 1 || bundleUnresolved.status !== 0) errors.push("Validator should warn (not error) on an unresolved intra-Bundle reference.");
447+
const bundleResolved = runValidator([], '{"resourceType":"Bundle","type":"collection","entry":[{"resource":{"resourceType":"Patient","id":"p"}},{"resource":{"resourceType":"Observation","status":"final","code":{},"subject":{"reference":"Patient/p"}}}]}');
448+
if (bundleResolved.parsed?.summary.warning !== 0 || bundleResolved.parsed?.summary.error !== 0) errors.push("Validator should resolve an intra-Bundle reference cleanly.");
449+
450+
// --- v0.5.0: instance-based slice matching ---
451+
const matcher = path.join(plugin, "skills/fhir-validation/scripts/match-slices.mjs");
452+
const slice = runJson(matcher, [path.join(plugin, "fixtures/structuredefinition-sliced.json"), path.join(plugin, "fixtures/observation-sliced-instance.json")]);
453+
const sliceElement = slice?.slicedElements?.[0];
454+
if (!sliceElement || JSON.stringify(sliceElement.slices) !== JSON.stringify({ vital: [0], lab: [1] })) errors.push("Slice matcher should attribute entries 0 and 1 to the vital and lab slices.");
455+
if (!sliceElement?.unmatched?.includes(2)) errors.push("Slice matcher should report the unmatched social-history entry.");
456+
457+
// --- v0.5.0: orchestrator ---
458+
const orchestrator = path.join(plugin, "skills/fhir-validation/scripts/validate.mjs");
459+
const orchFile = runValidatorScript(orchestrator, [path.join(plugin, "fixtures/invalid-observation.json")]);
460+
if (orchFile.status !== 1 || orchFile.parsed?.totals.error !== 2) errors.push("Orchestrator should report 2 errors and exit 1 for the invalid Observation.");
461+
const firstIssue = orchFile.parsed?.results?.[0]?.issues?.[0];
462+
if (!firstIssue?.jsonPointer || !firstIssue?.safeFixability) errors.push("Orchestrator should enrich issues with a JSON Pointer and fixability guidance.");
463+
const orchDir = runValidatorScript(orchestrator, [miniIg]);
464+
if (orchDir.parsed?.detector?.projectType !== "fsh-ig") errors.push("Orchestrator should include detector context for a directory target.");
465+
if (!(orchDir.parsed?.totals.resources >= 2)) errors.push("Orchestrator should validate every resource in a directory.");
466+
406467
if (errors.length) {
407468
console.error(errors.map((error) => `- ${error}`).join("\n"));
408469
process.exit(1);

0 commit comments

Comments
 (0)