@gefyra/complies-with-eu 1.2.0 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -0
- package/README.md +19 -10
- package/bin/complies-with-eu.js +11 -3
- package/package.json +7 -7
- package/src/commands/search.js +8 -3
- package/src/commands/validate.js +82 -45
- package/src/lib/constraintCheck.js +19 -0
- package/src/lib/javaValidator.js +131 -50
- package/src/lib/javaValidatorServer.js +166 -23
- package/src/lib/output.js +44 -0
- package/src/lib/preflight.js +1 -1
- package/src/lib/reportGen.js +39 -5
- package/src/lib/toolDownloader.js +3 -3
- package/src/lib/validateCore.js +13 -6
- package/src/lib/validatorOutput.js +276 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.3.1] - 2026-11-15
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- Profiles that validate without any error were reported as an operational error ("produced no result summary", exit code 2) instead of PASS. HL7 FHIR Validator 7.x prints the success summary as `Success: …` without asterisks, which the output parser did not recognise. Package mode (`validate-batch <package>`) was not affected.
|
|
15
|
+
|
|
16
|
+
## [1.3.0] - 2026-11-15
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- `--verbose` for `validate`, `validate-batch` and `search` (passed through to `validate`): streams the full HL7 FHIR Validator output to stderr, as before. `--debug` implies `--verbose`.
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- The HL7 FHIR Validator output (often several hundred "Information @ …" lines per run) is no longer streamed to stderr by default. A progress indicator shows the current phase (loading packages, validating) and ends with the validator summary, for example `*FAILURE*: 34 errors, 1 warnings, 101 notes`. Without a terminal (CI, piped stderr) each phase is printed once as a plain line. The validator server in package mode is quiet in the same way.
|
|
25
|
+
- The raw validator output is now always written to a log file in the workdir (`*-raw.log`, `examples/<slug>/_batch-raw.log`, `<package>/validator-server.log`), not only with `--debug` or on failure. Its path is printed after each run. `clean` removes these logs.
|
|
26
|
+
- When the validator crashes, exits abnormally or returns no result summary, the error message now includes the exit code, the last 20 lines of validator output and the path of the log file. In todo-file mode the error is also printed on the console, not only written to the error report.
|
|
27
|
+
- Updated dependencies: commander 12 → 14, axios 1.21.1, fs-extra 11.4.1. With commander 14, unexpected extra arguments are rejected with a usage error (exit code 2) instead of being ignored, for example `complies-with-eu doctor extra`.
|
|
28
|
+
- README: the disk space requirement is now about 2 GB instead of 1 GB, based on a measured clean installation.
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
|
|
32
|
+
- Package mode: when the validator server crashes or stops answering during the batch (for example out of memory), the progress no longer reports "Validator batch completed". It reports the failure, prints the server exit code, the last lines of its output and the path of `validator-server.log`, and each affected profile error names the log file instead of only `ECONNREFUSED` or `socket hang up`.
|
|
33
|
+
- Package mode starts the HL7 FHIR Validator with the `server` command instead of the deprecated `-server` option, which removes the warning "The option '-server' is no longer in use".
|
|
34
|
+
- Compliance report: identical rows in the "Non-Compliant Profiles — Details" table (for example the same missing constraint reported for several elements) are collapsed into one row with "(reported N×)". For a missing constraint, the element path is now shown when exactly one element of the EU profile declares that constraint. The `report` field of `--json` contains the same Markdown; the JSON shape is unchanged.
|
|
35
|
+
- Progress output no longer appears out of order when stdout and stderr are redirected or captured separately (for example in Docker or CI): all progress lines of `validate`, `validate-batch` and `search` now go to stderr, the same stream as the spinners. The final result (the "Done!" PASS/FAIL list, the report path and the batch summaries) stays on stdout; with `--json` it goes to stderr as before. When stderr is not a terminal, color codes are removed from these progress lines (unless `FORCE_COLOR` is set); spinner symbols and messages from the validator and package tools may still contain color codes.
|
|
36
|
+
|
|
10
37
|
## [1.2.0]
|
|
11
38
|
|
|
12
39
|
First public release as `@gefyra/complies-with-eu` under the Apache License 2.0.
|
package/README.md
CHANGED
|
@@ -73,7 +73,7 @@ You get a per-EU-profile verdict (fulfilled / not fulfilled) and a report that e
|
|
|
73
73
|
|-------------|---------|
|
|
74
74
|
| Node.js | 20 or newer |
|
|
75
75
|
| Java | **21 or newer** on `PATH` or via `JAVA_HOME` (for example [Eclipse Temurin](https://adoptium.net)). The HL7 validator needs Java 17, but the pinned `fhir-pkg-tool` is built for Java 21. |
|
|
76
|
-
| Disk | About
|
|
76
|
+
| Disk | About 2 GB free: roughly 330 MB for the tools in `~/.complies-with-eu` and 1.5 GB or more for the FHIR package cache (`~/.fhir/packages`), which keeps growing with every package you validate |
|
|
77
77
|
| Memory | 2 to 4 GB RAM for the validator JVM |
|
|
78
78
|
| Network | Outbound HTTPS to GitHub (tool downloads during `init`), the FHIR package registry (packages and their dependencies) and, for `search <canonical>`, simplifier.net |
|
|
79
79
|
|
|
@@ -137,6 +137,8 @@ Done!
|
|
|
137
137
|
Report: ~/.complies-with-eu/workdir/hl7_org_fhir_uv_ips_StructureDefinition_Patient-uv-ips-report.md
|
|
138
138
|
```
|
|
139
139
|
|
|
140
|
+
This final result is written to stdout. All progress output (status lines, spinners, the list of EU profiles found) goes to stderr, so it keeps its order when the output is redirected or collected by CI or Docker. With `--json`, stdout carries only the JSON document and the final result goes to stderr as well.
|
|
141
|
+
|
|
140
142
|
Open the Markdown report to see why a profile is not fulfilled (see [Reading the results](#reading-the-results)).
|
|
141
143
|
|
|
142
144
|
A second example with a German package (gematik ISiK):
|
|
@@ -210,7 +212,10 @@ What happens:
|
|
|
210
212
|
| `--output <file>` | Write JSON output to file instead of stdout (requires `--json`) |
|
|
211
213
|
| `--fail-on-noncompliant` | Exit with code 1 if a selected EU profile is not fulfilled |
|
|
212
214
|
| `--workdir <dir>` | Directory for reports and intermediate files (default: `<home>/workdir`) |
|
|
213
|
-
| `--
|
|
215
|
+
| `--verbose` | Stream the full HL7 FHIR Validator output to stderr (default: concise progress only; the full output is always written to a log file in the workdir) |
|
|
216
|
+
| `--debug` | Print the full Java validator command and show stack traces (implies `--verbose`) |
|
|
217
|
+
|
|
218
|
+
**Output.** By default the HL7 FHIR Validator output (often several hundred lines) is not shown. A progress indicator on stderr shows the current phase (loading packages, validating) and ends with the validator summary, for example `*FAILURE*: 34 errors, 1 warnings, 101 notes`. Without a terminal (CI, piped stderr) each phase is printed once as a plain line. The full output is always written to `<workdir>/<profile>-modified-raw.log`, and its path is printed. Use `--verbose` to stream it to stderr as well.
|
|
214
219
|
|
|
215
220
|
**`--examples`.** After the profile check, the example instances of the same resource type in the package (`package/example/` first, then `package/`) get a `meta.profile` claim for each selected EU profile and are validated in one validator run. The detailed report goes to `<workdir>/examples/<slug>/<slug>-examples-report.md`, and a summary table is appended to the profile report. With `--json`, the result includes an `examples` field.
|
|
216
221
|
|
|
@@ -234,7 +239,8 @@ If the argument is an existing `.json` file, it runs in **todo-file mode**. Othe
|
|
|
234
239
|
| `--reselect` | Package mode: force re-prompting even if `mapping.json` exists |
|
|
235
240
|
| `--type <types>` | Package mode: comma-separated resource types to filter (e.g. `Patient,Observation`) |
|
|
236
241
|
| `--server-port <port>` | Package mode: port for the local Java validator server (default: a free port; also `$VALIDATOR_SERVER_PORT`). A server already running there is reused only if it has the package loaded |
|
|
237
|
-
| `--
|
|
242
|
+
| `--verbose` | Stream the full HL7 FHIR Validator output to stderr (default: concise progress only; the full output is always written to a log file in the workdir) |
|
|
243
|
+
| `--debug` | Print the full Java validator command and show stack traces (implies `--verbose`) |
|
|
238
244
|
|
|
239
245
|
#### Todo-file mode
|
|
240
246
|
|
|
@@ -336,7 +342,8 @@ complies-with-eu search http://hl7.org/fhir/uv/ips/StructureDefinition/Patient-u
|
|
|
336
342
|
| `--output <file>` | Passed to validate: write JSON output to file (requires `--json`) |
|
|
337
343
|
| `--fail-on-noncompliant` | Passed to validate: exit with code 1 if a selected EU profile is not fulfilled |
|
|
338
344
|
| `--workdir <dir>` | Directory for reports and intermediate files (default: `<home>/workdir`) |
|
|
339
|
-
| `--
|
|
345
|
+
| `--verbose` | Passed to validate: stream the full HL7 FHIR Validator output |
|
|
346
|
+
| `--debug` | Passed to validate: debug output and stack traces (implies `--verbose`) |
|
|
340
347
|
|
|
341
348
|
### build-index
|
|
342
349
|
|
|
@@ -371,7 +378,7 @@ Deletes everything in the workdir except Markdown reports (`*-report.md`) and `m
|
|
|
371
378
|
| Section | Content |
|
|
372
379
|
|---------|---------|
|
|
373
380
|
| **compliesWithProfile — Overview** | How many of the checked EU profiles are fulfilled, with a ✅ / ❌ row per EU profile. An EU profile counts as not fulfilled if the validator rejects the claim **or** the obligation check finds a violation. |
|
|
374
|
-
| **Non-Compliant Profiles — Details** | Per failed EU profile, a table of element paths and the issue the validator reported (missing constraints, cardinality conflicts, slicing problems). |
|
|
381
|
+
| **Non-Compliant Profiles — Details** | Per failed EU profile, a table of element paths and the issue the validator reported (missing constraints, cardinality conflicts, slicing problems). Identical rows are shown once with "(reported N×)". The validator names no element for a missing constraint; the path is taken from the EU profile when exactly one of its elements declares the constraint, otherwise it is shown as "—". |
|
|
375
382
|
| **Obligation Check (SHALL:able-to-populate)** | The HL7 validator does not evaluate obligations or MustSupport. For every element that an EU profile's differential marks `SHALL:able-to-populate`, this check looks at your profile's snapshot and reports the element if it is not found, has `max: 0`, or is not MustSupport. |
|
|
376
383
|
| **Automatically resolved constraints** | EU `.exists() or …` constraints that your profile does not declare, but that are guaranteed by its minimum cardinalities (unsliced elements with `min ≥ 1`). They are excluded from the verdict. |
|
|
377
384
|
| **Other Validation Results** | Collapsible list of all other validator messages (fatal, errors, warnings, information) that are not about `compliesWithProfile`. |
|
|
@@ -397,7 +404,7 @@ With `--json`, stdout contains only the JSON document (progress goes to stderr);
|
|
|
397
404
|
"package": "hl7.fhir.uv.ips@2.0.0",
|
|
398
405
|
"canonical": "http://hl7.org/fhir/uv/ips/StructureDefinition/Patient-uv-ips",
|
|
399
406
|
"reportPath": "/home/me/.complies-with-eu/workdir/hl7_org_fhir_uv_ips_StructureDefinition_Patient-uv-ips-report.md",
|
|
400
|
-
"tool": { "name": "@gefyra/complies-with-eu", "version": "1.
|
|
407
|
+
"tool": { "name": "@gefyra/complies-with-eu", "version": "1.3.1", "validatorVersion": "7.0.5" },
|
|
401
408
|
"validator": { "mode": "local", "summary": "…" },
|
|
402
409
|
"euPackages": [ { "packageName": "hl7.fhir.eu.base", "version": "2.0.1" } ]
|
|
403
410
|
}
|
|
@@ -492,9 +499,9 @@ jobs:
|
|
|
492
499
|
path: |
|
|
493
500
|
~/.complies-with-eu/tools
|
|
494
501
|
~/.fhir/packages
|
|
495
|
-
key: complies-with-eu-${{ runner.os }}-1.
|
|
502
|
+
key: complies-with-eu-${{ runner.os }}-1.3.1
|
|
496
503
|
|
|
497
|
-
- run: npm install -g @gefyra/complies-with-eu@1.
|
|
504
|
+
- run: npm install -g @gefyra/complies-with-eu@1.3.1
|
|
498
505
|
- run: complies-with-eu init
|
|
499
506
|
|
|
500
507
|
- name: Validate
|
|
@@ -582,7 +589,7 @@ These parameters apply to `validate`, `validate-batch` and the local validator s
|
|
|
582
589
|
| Path | Content | How to reset |
|
|
583
590
|
|------|---------|--------------|
|
|
584
591
|
| `~/.complies-with-eu/tools/` | `validator_cli.jar`, `fhir-pkg-tool.jar`, `versions.json` | `complies-with-eu init --force` |
|
|
585
|
-
| `~/.complies-with-eu/workdir/` | Reports (`*-report.md`), modified profiles (`*-modified.json`), raw validator logs (`*-raw.log`,
|
|
592
|
+
| `~/.complies-with-eu/workdir/` | Reports (`*-report.md`), modified profiles (`*-modified.json`), raw validator logs (`*-raw.log`, `examples/<slug>/_batch-raw.log`, `<package>/validator-server.log`, written on every run), `examples/`, package-mode folders | `complies-with-eu clean` (keeps reports and `mapping.json`), or delete the folder |
|
|
586
593
|
| `~/.complies-with-eu/cache.json` | Your last interactive EU profile selection per canonical | Delete the file |
|
|
587
594
|
| `~/.fhir/packages/` | Standard FHIR package cache, shared with other FHIR tools. Holds the EU packages, your packages and their dependencies. | Delete individual `name#version` folders; they are downloaded again when needed |
|
|
588
595
|
|
|
@@ -603,7 +610,9 @@ Versions up to 1.1.x kept their state inside the installation directory (`.tools
|
|
|
603
610
|
| `--output requires --json` | Add `--json`. |
|
|
604
611
|
| `Package '…' could not be downloaded` | Check the package name and version (for example on [packages.fhir.org](https://packages.fhir.org)), or try `complies-with-eu search <canonical>`. |
|
|
605
612
|
| Profile not found | The canonical is not in that package version. List the profiles with `complies-with-eu search <package>`. |
|
|
606
|
-
| `… produced no result summary` | The validator crashed (often out of memory or a broken jar).
|
|
613
|
+
| `… produced no result summary` | The validator crashed (often out of memory or a broken jar). The message shows the last lines of the validator output; open the raw log named in the message, rerun with `--verbose` (or `--debug`), or reinstall the tools with `init --force`. |
|
|
614
|
+
| `FHIR Java Validator server did not finish the batch validation` | Package mode: the validator server stopped while validating (often out of memory). The message shows its exit code and the last lines of its output; the affected profiles get an error report that names `<workdir>/<package>/validator-server.log`. Give Java more memory or validate fewer profiles at once (`--type`). |
|
|
615
|
+
| Need the full validator output | It is not shown by default. Every run writes it to a log file in the workdir: `<workdir>/<profile>-modified-raw.log` (`validate`, todo-file mode), `<workdir>/examples/<slug>/_batch-raw.log` (`--examples`), `<workdir>/<package>/validator-server.log` (package mode). The path is printed after each run and when the validator fails. `--verbose` streams it to stderr as well. |
|
|
607
616
|
| First run takes several minutes | The validator downloads dependencies and terminology packages into `~/.fhir/packages` once. |
|
|
608
617
|
| `--server-port` fails | Something else is running on that port, or a validator server without your package. Omit the option to use a free port. |
|
|
609
618
|
|
package/bin/complies-with-eu.js
CHANGED
|
@@ -24,6 +24,11 @@ Reports:
|
|
|
24
24
|
Written to <workdir>/<profile>-report.md (package mode: <workdir>/<package>/).
|
|
25
25
|
Default workdir: ~/.complies-with-eu/workdir (override: --workdir or COMPLIES_WITH_EU_WORKDIR).
|
|
26
26
|
|
|
27
|
+
Validator output:
|
|
28
|
+
By default only concise progress is shown. The full HL7 FHIR validator output is always
|
|
29
|
+
written to a log file in the workdir (*-raw.log, _batch-raw.log, validator-server.log);
|
|
30
|
+
its path is printed when the validator fails. Use --verbose to stream it to stderr.
|
|
31
|
+
|
|
27
32
|
Exit codes:
|
|
28
33
|
0 success (also when profiles are non-compliant, unless --fail-on-noncompliant is set)
|
|
29
34
|
1 at least one selected EU profile is not fulfilled (only with --fail-on-noncompliant)
|
|
@@ -74,7 +79,8 @@ function buildProgram() {
|
|
|
74
79
|
.option('--output <file>', 'Write JSON output to file instead of stdout (requires --json)')
|
|
75
80
|
.option('--fail-on-noncompliant', 'Exit with code 1 if a selected EU profile is not fulfilled')
|
|
76
81
|
.option('--workdir <dir>', 'Directory for reports and intermediate files (default: <home>/workdir)')
|
|
77
|
-
.option('--
|
|
82
|
+
.option('--verbose', 'Stream the full HL7 FHIR validator output to stderr (default: concise progress only)')
|
|
83
|
+
.option('--debug', 'Print the full Java validator command and stack traces (implies --verbose)')
|
|
78
84
|
.addHelpText('after', `
|
|
79
85
|
Examples:
|
|
80
86
|
$ complies-with-eu validate hl7.fhir.uv.ips@2.0.0 http://hl7.org/fhir/uv/ips/StructureDefinition/Patient-uv-ips --all-counterparts
|
|
@@ -103,7 +109,8 @@ without a terminal (CI, --json) one of the two flags is required.`)
|
|
|
103
109
|
.option('--reselect', 'Package mode: force re-prompting even if mapping.json exists')
|
|
104
110
|
.option('--type <types>', 'Package mode: comma-separated resource types to filter (e.g. Patient,Observation)')
|
|
105
111
|
.option('--server-port <port>', 'Package mode: port for the local Java validator server (default: a free port; also $VALIDATOR_SERVER_PORT). A server already running there is reused only if it has the package loaded')
|
|
106
|
-
.option('--
|
|
112
|
+
.option('--verbose', 'Stream the full HL7 FHIR validator output to stderr (default: concise progress only)')
|
|
113
|
+
.option('--debug', 'Print the full Java validator command and stack traces (implies --verbose)')
|
|
107
114
|
.action((arg, options) => {
|
|
108
115
|
const { runValidateBatchFile, runValidatePackage } = require('../src/commands/validate');
|
|
109
116
|
if (arg.endsWith('.json') && fs.existsSync(arg)) {
|
|
@@ -128,7 +135,8 @@ without a terminal (CI, --json) one of the two flags is required.`)
|
|
|
128
135
|
.option('--output <file>', 'Passed to validate: write JSON output to file (requires --json)')
|
|
129
136
|
.option('--fail-on-noncompliant', 'Passed to validate: exit with code 1 if a selected EU profile is not fulfilled')
|
|
130
137
|
.option('--workdir <dir>', 'Directory for reports and intermediate files (default: <home>/workdir)')
|
|
131
|
-
.option('--
|
|
138
|
+
.option('--verbose', 'Passed to validate: stream the full HL7 FHIR validator output')
|
|
139
|
+
.option('--debug', 'Passed to validate: debug output and stack traces (implies --verbose)')
|
|
132
140
|
.action((pkg, options) => require('../src/commands/search').runSearch(pkg, options));
|
|
133
141
|
|
|
134
142
|
program
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gefyra/complies-with-eu",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.1",
|
|
4
4
|
"description": "CLI that checks whether FHIR profiles comply with the HL7 Europe (EU) profiles, using the compliesWithProfile extension and the HL7 FHIR validator",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"fhir",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"author": "Gefyra GmbH <js@gefyra.de>",
|
|
28
28
|
"type": "commonjs",
|
|
29
29
|
"bin": {
|
|
30
|
-
"complies-with-eu": "
|
|
30
|
+
"complies-with-eu": "bin/complies-with-eu.js"
|
|
31
31
|
},
|
|
32
32
|
"files": [
|
|
33
33
|
"bin/",
|
|
@@ -55,14 +55,14 @@
|
|
|
55
55
|
"dependencies": {
|
|
56
56
|
"axios": "^1.21.0",
|
|
57
57
|
"chalk": "^4.1.2",
|
|
58
|
-
"commander": "^
|
|
59
|
-
"fs-extra": "^11.
|
|
58
|
+
"commander": "^14.0.3",
|
|
59
|
+
"fs-extra": "^11.4.1",
|
|
60
60
|
"inquirer": "^8.2.6",
|
|
61
61
|
"ora": "^5.4.1"
|
|
62
62
|
},
|
|
63
63
|
"devDependencies": {
|
|
64
|
-
"@eslint/js": "^
|
|
65
|
-
"eslint": "^
|
|
66
|
-
"globals": "^
|
|
64
|
+
"@eslint/js": "^10.0.1",
|
|
65
|
+
"eslint": "^10.14.0",
|
|
66
|
+
"globals": "^17.14.0"
|
|
67
67
|
}
|
|
68
68
|
}
|
package/src/commands/search.js
CHANGED
|
@@ -8,6 +8,7 @@ const { loadAllSelectionCaches, setWorkdir } = require('../lib/config');
|
|
|
8
8
|
const { downloadFhirPackage, resolvePackageDir } = require('../lib/fhirPkgTool');
|
|
9
9
|
const { ensureReady, resolveEuPackageDirs } = require('../lib/preflight');
|
|
10
10
|
const { CliError } = require('../lib/errors');
|
|
11
|
+
const { makeProgressLogger } = require('../lib/output');
|
|
11
12
|
const { findAllProfilesInPackage, findEuProfilesByType } = require('../lib/profileUtils');
|
|
12
13
|
const { runValidate } = require('./validate');
|
|
13
14
|
const { resolveCanonicalOnSimplifier } = require('../lib/simplifierResolve');
|
|
@@ -17,6 +18,7 @@ const { parsePackageSpec } = require('../lib/validateCore');
|
|
|
17
18
|
function validateOptions(options, extra = {}) {
|
|
18
19
|
return {
|
|
19
20
|
debug: options.debug,
|
|
21
|
+
verbose: options.verbose,
|
|
20
22
|
examples: options.examples,
|
|
21
23
|
json: options.json,
|
|
22
24
|
output: options.output,
|
|
@@ -29,9 +31,12 @@ function validateOptions(options, extra = {}) {
|
|
|
29
31
|
};
|
|
30
32
|
}
|
|
31
33
|
|
|
32
|
-
/**
|
|
33
|
-
|
|
34
|
-
|
|
34
|
+
/**
|
|
35
|
+
* Progress output: always stderr, the same stream as the ora spinners, so lines
|
|
36
|
+
* keep their order (stdout carries only the JSON result with --json).
|
|
37
|
+
*/
|
|
38
|
+
function makeLog() {
|
|
39
|
+
return makeProgressLogger();
|
|
35
40
|
}
|
|
36
41
|
|
|
37
42
|
/** Prompts render on stderr with --json so stdout carries only the JSON result. */
|
package/src/commands/validate.js
CHANGED
|
@@ -15,11 +15,13 @@ const {
|
|
|
15
15
|
canonicalToSlug,
|
|
16
16
|
} = require('../lib/profileUtils');
|
|
17
17
|
const { runValidator } = require('../lib/javaValidator');
|
|
18
|
+
const { createProgress } = require('../lib/validatorOutput');
|
|
18
19
|
const {
|
|
19
20
|
isServerReady,
|
|
20
21
|
serverHasCanonical,
|
|
21
22
|
startValidatorServer,
|
|
22
23
|
validateBatchViaServer,
|
|
24
|
+
describeBatchFailure,
|
|
23
25
|
} = require('../lib/javaValidatorServer');
|
|
24
26
|
const { generateExampleReport, appendExampleSummary, mdCell, mdText } = require('../lib/reportGen');
|
|
25
27
|
const {
|
|
@@ -35,17 +37,19 @@ const {
|
|
|
35
37
|
const { loadPackageMapping, savePackageMapping, loadExternalMapping } = require('../lib/mappingFile');
|
|
36
38
|
const { ensureReady, resolveEuPackageDirs } = require('../lib/preflight');
|
|
37
39
|
const { CliError, EXIT, setExitCode, outcomeExitCode } = require('../lib/errors');
|
|
40
|
+
const { makeProgressLogger, makeResultLogger } = require('../lib/output');
|
|
38
41
|
|
|
39
42
|
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
40
43
|
|
|
41
44
|
/**
|
|
42
|
-
*
|
|
43
|
-
*
|
|
45
|
+
* Loggers of a command run:
|
|
46
|
+
* - `log`: progress, always stderr (the same stream as the ora spinners, so lines
|
|
47
|
+
* keep their order when stdout and stderr are captured separately).
|
|
48
|
+
* - `out`: the final human-readable result, stdout (stderr with --json, where
|
|
49
|
+
* stdout carries only the JSON document).
|
|
44
50
|
*/
|
|
45
|
-
function
|
|
46
|
-
return options
|
|
47
|
-
? (...args) => process.stderr.write(args.join(' ') + '\n')
|
|
48
|
-
: (...args) => console.log(...args);
|
|
51
|
+
function makeLoggers(options) {
|
|
52
|
+
return { log: makeProgressLogger(), out: makeResultLogger(options) };
|
|
49
53
|
}
|
|
50
54
|
|
|
51
55
|
/** True when the user can answer prompts (TTY on stdin and stdout, not --json). */
|
|
@@ -69,6 +73,13 @@ function applyCommonOptions(options) {
|
|
|
69
73
|
if (options.output && !options.json) {
|
|
70
74
|
throw new CliError('--output requires --json.');
|
|
71
75
|
}
|
|
76
|
+
// --debug implies --verbose (full validator output)
|
|
77
|
+
if (options.debug) options.verbose = true;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Options for runValidator / runValidatorBatch derived from the CLI options. */
|
|
81
|
+
function validatorRunOptions(options) {
|
|
82
|
+
return { debug: !!options.debug, verbose: !!options.verbose || !!options.debug };
|
|
72
83
|
}
|
|
73
84
|
|
|
74
85
|
/**
|
|
@@ -163,8 +174,8 @@ async function selectEuProfiles({ canonical, discovered, options, log }) {
|
|
|
163
174
|
* Runs example validation for one profile and writes/links the example report.
|
|
164
175
|
* @returns {Promise<object|null>} JSON summary (see summarizeExamples) or null if no examples
|
|
165
176
|
*/
|
|
166
|
-
async function validateExamples({ pkgDir, fhirType, slug, examplesDir, euCanonicals, igIds, canonical, packageSpec, mainReportPath, debug, log }) {
|
|
167
|
-
const exResult = await runExamplesForProfile({ pkgDir, fhirType, slug, examplesDir, euCanonicals, igIds, debug, log });
|
|
177
|
+
async function validateExamples({ pkgDir, fhirType, slug, examplesDir, euCanonicals, igIds, canonical, packageSpec, mainReportPath, debug, verbose, log }) {
|
|
178
|
+
const exResult = await runExamplesForProfile({ pkgDir, fhirType, slug, examplesDir, euCanonicals, igIds, debug, verbose, log });
|
|
168
179
|
if (!exResult) return summarizeExamples(null);
|
|
169
180
|
try {
|
|
170
181
|
await generateExampleReport({
|
|
@@ -191,7 +202,7 @@ async function validateExamples({ pkgDir, fhirType, slug, examplesDir, euCanonic
|
|
|
191
202
|
|
|
192
203
|
async function runValidate(pkg, canonical, options = {}) {
|
|
193
204
|
applyCommonOptions(options);
|
|
194
|
-
const log =
|
|
205
|
+
const { log, out } = makeLoggers(options);
|
|
195
206
|
|
|
196
207
|
log(chalk.bold('\ncomplies-with-eu validate\n'));
|
|
197
208
|
if (!options._preflightDone) await ensureReady();
|
|
@@ -279,10 +290,10 @@ async function runValidate(pkg, canonical, options = {}) {
|
|
|
279
290
|
}
|
|
280
291
|
|
|
281
292
|
// 7. Run FHIR Java Validator
|
|
282
|
-
log(
|
|
293
|
+
log('');
|
|
283
294
|
let parsed, mode;
|
|
284
295
|
try {
|
|
285
|
-
({ parsed, mode } = await runValidator(modifiedProfilePath, igIds,
|
|
296
|
+
({ parsed, mode } = await runValidator(modifiedProfilePath, igIds, validatorRunOptions(options)));
|
|
286
297
|
} catch (err) {
|
|
287
298
|
throw new CliError(`Validator failed: ${err.message}`);
|
|
288
299
|
}
|
|
@@ -325,6 +336,7 @@ async function runValidate(pkg, canonical, options = {}) {
|
|
|
325
336
|
packageSpec,
|
|
326
337
|
mainReportPath: reportPath,
|
|
327
338
|
debug: !!options.debug,
|
|
339
|
+
verbose: !!options.verbose,
|
|
328
340
|
log,
|
|
329
341
|
});
|
|
330
342
|
if (examples.reportPath) {
|
|
@@ -353,13 +365,13 @@ async function runValidate(pkg, canonical, options = {}) {
|
|
|
353
365
|
};
|
|
354
366
|
await emitJson(options, jsonResult, log);
|
|
355
367
|
} else {
|
|
356
|
-
|
|
368
|
+
out(chalk.bold.green('\nDone!\n'));
|
|
357
369
|
for (const c of compliance) {
|
|
358
|
-
|
|
370
|
+
out(` ${c.pass ? chalk.green('PASS') : chalk.red('FAIL')} ${c.euCanonical}`);
|
|
359
371
|
}
|
|
360
372
|
}
|
|
361
373
|
|
|
362
|
-
|
|
374
|
+
out(`\n Report: ${reportPath}\n`);
|
|
363
375
|
setExitCode(outcomeExitCode({ failOnNoncompliant: options.failOnNoncompliant, nonCompliant }));
|
|
364
376
|
return { compliance, pass, reportPath };
|
|
365
377
|
}
|
|
@@ -376,7 +388,7 @@ async function runValidate(pkg, canonical, options = {}) {
|
|
|
376
388
|
*/
|
|
377
389
|
async function runValidateBatchFile(jsonFile, options = {}) {
|
|
378
390
|
applyCommonOptions(options);
|
|
379
|
-
const log =
|
|
391
|
+
const { log, out } = makeLoggers(options);
|
|
380
392
|
log(chalk.bold('\ncomplies-with-eu validate-batch (todo file)\n'));
|
|
381
393
|
await ensureReady();
|
|
382
394
|
|
|
@@ -422,6 +434,7 @@ async function runValidateBatchFile(jsonFile, options = {}) {
|
|
|
422
434
|
const fail = async (task, message) => {
|
|
423
435
|
const { canonical, packageSpec, slug } = task;
|
|
424
436
|
const reportPath = await writeErrorReport(slug, canonical, packageSpec, message);
|
|
437
|
+
log(chalk.red(` ${message}`));
|
|
425
438
|
log(chalk.red(` Error report: ${reportPath}`));
|
|
426
439
|
results.failed++;
|
|
427
440
|
results.reports.push({ canonical, packageSpec, reportPath, error: true, errorMessage: message, pass: false, compliance: [] });
|
|
@@ -509,13 +522,11 @@ async function runValidateBatchFile(jsonFile, options = {}) {
|
|
|
509
522
|
}
|
|
510
523
|
|
|
511
524
|
// ── Step 5: Run FHIR Java Validator ───────────────────────────────────
|
|
512
|
-
|
|
525
|
+
// runValidator shows its own progress (or the full output with --verbose)
|
|
513
526
|
let parsed;
|
|
514
527
|
try {
|
|
515
|
-
({ parsed } = await runValidator(modifiedProfilePath, igIds,
|
|
516
|
-
spinner.succeed('Validation complete');
|
|
528
|
+
({ parsed } = await runValidator(modifiedProfilePath, igIds, validatorRunOptions(options)));
|
|
517
529
|
} catch (err) {
|
|
518
|
-
spinner.fail('Validator failed');
|
|
519
530
|
await fail(task, `FHIR Java Validator failed: ${err.message}`);
|
|
520
531
|
continue;
|
|
521
532
|
}
|
|
@@ -559,6 +570,7 @@ async function runValidateBatchFile(jsonFile, options = {}) {
|
|
|
559
570
|
packageSpec,
|
|
560
571
|
mainReportPath: reportPath,
|
|
561
572
|
debug: !!options.debug,
|
|
573
|
+
verbose: !!options.verbose,
|
|
562
574
|
log,
|
|
563
575
|
});
|
|
564
576
|
if (entry.examples.reportPath) log(chalk.green(` Examples report: ${entry.examples.reportPath}`));
|
|
@@ -569,21 +581,21 @@ async function runValidateBatchFile(jsonFile, options = {}) {
|
|
|
569
581
|
const compliant = results.reports.filter(r => !r.error && r.pass).length;
|
|
570
582
|
|
|
571
583
|
// ── Final summary ─────────────────────────────────────────────────────────
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
584
|
+
out(chalk.bold('\n══════════════════════════════════════════'));
|
|
585
|
+
out(chalk.bold(' Batch validation complete'));
|
|
586
|
+
out(chalk.bold('══════════════════════════════════════════\n'));
|
|
587
|
+
out(` Total: ${tasks.length}`);
|
|
588
|
+
out(chalk.green(` Processed: ${results.success} (${compliant} compliant, ${nonCompliant} non-compliant)`));
|
|
577
589
|
if (results.failed > 0) {
|
|
578
|
-
|
|
590
|
+
out(chalk.red(` Errors: ${results.failed}`));
|
|
579
591
|
}
|
|
580
|
-
|
|
592
|
+
out('');
|
|
581
593
|
for (const r of results.reports) {
|
|
582
594
|
const icon = r.error ? chalk.red('ERROR') : (r.pass ? chalk.green('PASS ') : chalk.yellow('FAIL '));
|
|
583
595
|
const label = r.canonical.split('/').pop().padEnd(60);
|
|
584
|
-
|
|
596
|
+
out(` ${icon} ${label} → ${r.reportPath}`);
|
|
585
597
|
}
|
|
586
|
-
|
|
598
|
+
out(`\n Reports: ${getWorkdir()}\n`);
|
|
587
599
|
|
|
588
600
|
if (options.json) {
|
|
589
601
|
await emitJson(options, {
|
|
@@ -739,7 +751,8 @@ async function discoverAndResolveMapping(pkgDir, euPackageDirs, pkgWorkdir, opti
|
|
|
739
751
|
* validator server. Never reuses an unknown responder: a validator loaded with
|
|
740
752
|
* other IGs would silently produce wrong verdicts.
|
|
741
753
|
*/
|
|
742
|
-
async function acquireLocalServer({ options, igIds, verifyCanonical, log }) {
|
|
754
|
+
async function acquireLocalServer({ options, igIds, verifyCanonical, log, logFile = null }) {
|
|
755
|
+
const serverOpts = { igIds, debug: !!options.debug, verbose: !!options.verbose, logFile };
|
|
743
756
|
const explicit = options.serverPort || process.env.VALIDATOR_SERVER_PORT;
|
|
744
757
|
if (explicit) {
|
|
745
758
|
const port = parseInt(explicit, 10);
|
|
@@ -757,10 +770,10 @@ async function acquireLocalServer({ options, igIds, verifyCanonical, log }) {
|
|
|
757
770
|
);
|
|
758
771
|
}
|
|
759
772
|
log(chalk.cyan(` Starting local validator server on port ${port}...`));
|
|
760
|
-
return startValidatorServer({
|
|
773
|
+
return startValidatorServer({ ...serverOpts, port });
|
|
761
774
|
}
|
|
762
775
|
log(chalk.cyan(' Starting local validator server on a free port...'));
|
|
763
|
-
return startValidatorServer(
|
|
776
|
+
return startValidatorServer(serverOpts);
|
|
764
777
|
}
|
|
765
778
|
|
|
766
779
|
/**
|
|
@@ -772,7 +785,7 @@ async function acquireLocalServer({ options, igIds, verifyCanonical, log }) {
|
|
|
772
785
|
*/
|
|
773
786
|
async function runValidatePackage(pkgInput, options = {}) {
|
|
774
787
|
applyCommonOptions(options);
|
|
775
|
-
const log =
|
|
788
|
+
const { log, out } = makeLoggers(options);
|
|
776
789
|
|
|
777
790
|
log(chalk.bold('\ncomplies-with-eu validate-batch (package mode)\n'));
|
|
778
791
|
await ensureReady();
|
|
@@ -860,15 +873,15 @@ async function runValidatePackage(pkgInput, options = {}) {
|
|
|
860
873
|
}, log);
|
|
861
874
|
}
|
|
862
875
|
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
if (nonCompliant > 0)
|
|
869
|
-
if (errored > 0)
|
|
870
|
-
|
|
871
|
-
|
|
876
|
+
out(chalk.bold('\n══════════════════════════════════════════'));
|
|
877
|
+
out(chalk.bold(' Package Validation Complete'));
|
|
878
|
+
out(chalk.bold('══════════════════════════════════════════\n'));
|
|
879
|
+
out(` Total: ${summary.total}`);
|
|
880
|
+
out(chalk.green(` Compliant: ${compliant}`));
|
|
881
|
+
if (nonCompliant > 0) out(chalk.yellow(` Non-compliant: ${nonCompliant}`));
|
|
882
|
+
if (errored > 0) out(chalk.red(` Errors: ${errored}`));
|
|
883
|
+
out(`\n Package report: ${packageReportPath}`);
|
|
884
|
+
out(` Artifacts: ${pkgWorkdir}\n`);
|
|
872
885
|
|
|
873
886
|
setExitCode(outcomeExitCode({ failOnNoncompliant: options.failOnNoncompliant, nonCompliant, errored }));
|
|
874
887
|
};
|
|
@@ -956,11 +969,34 @@ async function runValidatePackage(pkgInput, options = {}) {
|
|
|
956
969
|
let batchResults;
|
|
957
970
|
let server = null;
|
|
958
971
|
try {
|
|
959
|
-
server = await acquireLocalServer({
|
|
960
|
-
|
|
972
|
+
server = await acquireLocalServer({
|
|
973
|
+
options,
|
|
974
|
+
igIds,
|
|
975
|
+
verifyCanonical: prepared[0].canonical,
|
|
976
|
+
log,
|
|
977
|
+
logFile: path.join(pkgWorkdir, 'validator-server.log'),
|
|
978
|
+
});
|
|
961
979
|
const files = prepared.map(p => path.basename(p.modifiedProfilePath));
|
|
962
|
-
({
|
|
963
|
-
|
|
980
|
+
const progress = createProgress({ text: `Validating ${files.length} profile(s) via the validator server`, verbose: !!options.verbose });
|
|
981
|
+
try {
|
|
982
|
+
({ results: batchResults } = await validateBatchViaServer(server.port, modifiedDir, files));
|
|
983
|
+
} catch (err) {
|
|
984
|
+
progress.fail('Validator batch failed');
|
|
985
|
+
throw err;
|
|
986
|
+
}
|
|
987
|
+
// A server that crashed or hung mid-batch does not throw: every request just
|
|
988
|
+
// fails (ECONNREFUSED, socket hang up). Explain it with exit code, output tail and log path.
|
|
989
|
+
const failure = await describeBatchFailure(server, batchResults);
|
|
990
|
+
if (failure) {
|
|
991
|
+
progress.fail(failure.dead
|
|
992
|
+
? `Validator server exited during the batch (${failure.failed} of ${failure.total} profile(s) without result)`
|
|
993
|
+
: `Validator batch completed with errors (${failure.failed} of ${failure.total} profile(s) without result)`);
|
|
994
|
+
log(chalk.red(failure.message));
|
|
995
|
+
batchResults = batchResults.map(r => (r.error ? { ...r, error: `${r.error}; ${failure.short}` } : r));
|
|
996
|
+
} else {
|
|
997
|
+
progress.succeed(`Validator batch completed (${files.length} profile(s))`);
|
|
998
|
+
}
|
|
999
|
+
log('');
|
|
964
1000
|
} catch (err) {
|
|
965
1001
|
if (err instanceof CliError) throw err;
|
|
966
1002
|
throw new CliError(`Validator server failed: ${err.message}`);
|
|
@@ -1027,6 +1063,7 @@ async function runValidatePackage(pkgInput, options = {}) {
|
|
|
1027
1063
|
packageSpec,
|
|
1028
1064
|
mainReportPath: entry && !entry.error ? entry.reportPath : null,
|
|
1029
1065
|
debug: !!options.debug,
|
|
1066
|
+
verbose: !!options.verbose,
|
|
1030
1067
|
log,
|
|
1031
1068
|
});
|
|
1032
1069
|
if (entry) entry.examples = examples;
|
|
@@ -54,6 +54,24 @@ function findConstraintBasePath(euSd, expr) {
|
|
|
54
54
|
return null;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Returns the distinct element paths of the EU SD that declare a constraint with
|
|
59
|
+
* the given expression (snapshot; the differential only when there is no snapshot).
|
|
60
|
+
*
|
|
61
|
+
* @param {object} euSd
|
|
62
|
+
* @param {string} expr
|
|
63
|
+
* @returns {string[]}
|
|
64
|
+
*/
|
|
65
|
+
function findConstraintPaths(euSd, expr) {
|
|
66
|
+
const snapshotEls = euSd?.snapshot?.element ?? [];
|
|
67
|
+
const elements = snapshotEls.length > 0 ? snapshotEls : (euSd?.differential?.element ?? []);
|
|
68
|
+
const paths = new Set();
|
|
69
|
+
for (const el of elements) {
|
|
70
|
+
if ((el.constraint ?? []).some(c => c.expression === expr) && el.path) paths.add(el.path);
|
|
71
|
+
}
|
|
72
|
+
return [...paths];
|
|
73
|
+
}
|
|
74
|
+
|
|
57
75
|
/**
|
|
58
76
|
* Checks whether at least one term of `{ paths, extensionUrls }` is structurally guaranteed
|
|
59
77
|
* by `min >= 1` in the input SD's elements under `basePath`.
|
|
@@ -229,6 +247,7 @@ module.exports = {
|
|
|
229
247
|
loadEuSds,
|
|
230
248
|
parseExistsOrExpression,
|
|
231
249
|
findConstraintBasePath,
|
|
250
|
+
findConstraintPaths,
|
|
232
251
|
isSatisfiedInInputSnapshot,
|
|
233
252
|
isIgnoredSlicingDetail,
|
|
234
253
|
};
|