@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 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 1 GB free for the tools, the EU packages and the FHIR package cache (`~/.fhir/packages`) |
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
- | `--debug` | Print the full Java validator command, keep the raw output and show stack traces |
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
- | `--debug` | Print the full Java validator command, keep the raw output and show stack traces |
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
- | `--debug` | Passed to validate: debug output and stack traces |
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.2.0", "validatorVersion": "7.0.5" },
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.2.0
502
+ key: complies-with-eu-${{ runner.os }}-1.3.1
496
503
 
497
- - run: npm install -g @gefyra/complies-with-eu@1.2.0
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`, with `--debug` or when the validator fails), `examples/`, package-mode folders | `complies-with-eu clean` (keeps reports and `mapping.json`), or delete the folder |
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). Open the raw log named in the message, rerun with `--debug`, or reinstall the tools with `init --force`. |
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
 
@@ -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('--debug', 'Print the full Java validator command, keep the raw output and show stack traces')
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('--debug', 'Print the full Java validator command, keep the raw output and show stack traces')
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('--debug', 'Passed to validate: debug output and stack traces')
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.2.0",
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": "./bin/complies-with-eu.js"
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": "^12.1.0",
59
- "fs-extra": "^11.2.0",
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": "^9.39.5",
65
- "eslint": "^9.39.5",
66
- "globals": "^15.15.0"
64
+ "@eslint/js": "^10.0.1",
65
+ "eslint": "^10.14.0",
66
+ "globals": "^17.14.0"
67
67
  }
68
68
  }
@@ -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
- /** Progress output; stderr with --json so stdout carries only the JSON result. */
33
- function makeLog(options) {
34
- return options.json ? (...a) => process.stderr.write(a.join(' ') + '\n') : (...a) => console.log(...a);
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. */
@@ -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
- * Progress logger. In --json mode everything goes to stderr so stdout carries
43
- * only the final JSON document.
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 makeLogger(options) {
46
- return options.json
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 = makeLogger(options);
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(chalk.bold('\nRunning FHIR Java Validator...\n'));
293
+ log('');
283
294
  let parsed, mode;
284
295
  try {
285
- ({ parsed, mode } = await runValidator(modifiedProfilePath, igIds, !!options.debug));
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
- console.log(chalk.bold.green('\nDone!\n'));
368
+ out(chalk.bold.green('\nDone!\n'));
357
369
  for (const c of compliance) {
358
- console.log(` ${c.pass ? chalk.green('PASS') : chalk.red('FAIL')} ${c.euCanonical}`);
370
+ out(` ${c.pass ? chalk.green('PASS') : chalk.red('FAIL')} ${c.euCanonical}`);
359
371
  }
360
372
  }
361
373
 
362
- log(`\n Report: ${reportPath}\n`);
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 = makeLogger(options);
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
- const spinner = ora(' Validating...').start();
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, !!options.debug));
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
- log(chalk.bold('\n══════════════════════════════════════════'));
573
- log(chalk.bold(' Batch validation complete'));
574
- log(chalk.bold('══════════════════════════════════════════\n'));
575
- log(` Total: ${tasks.length}`);
576
- log(chalk.green(` Processed: ${results.success} (${compliant} compliant, ${nonCompliant} non-compliant)`));
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
- log(chalk.red(` Errors: ${results.failed}`));
590
+ out(chalk.red(` Errors: ${results.failed}`));
579
591
  }
580
- log('');
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
- log(` ${icon} ${label} → ${r.reportPath}`);
596
+ out(` ${icon} ${label} → ${r.reportPath}`);
585
597
  }
586
- log(`\n Reports: ${getWorkdir()}\n`);
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({ igIds, port, debug: !!options.debug });
773
+ return startValidatorServer({ ...serverOpts, port });
761
774
  }
762
775
  log(chalk.cyan(' Starting local validator server on a free port...'));
763
- return startValidatorServer({ igIds, debug: !!options.debug });
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 = makeLogger(options);
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
- log(chalk.bold('\n══════════════════════════════════════════'));
864
- log(chalk.bold(' Package Validation Complete'));
865
- log(chalk.bold('══════════════════════════════════════════\n'));
866
- log(` Total: ${summary.total}`);
867
- log(chalk.green(` Compliant: ${compliant}`));
868
- if (nonCompliant > 0) log(chalk.yellow(` Non-compliant: ${nonCompliant}`));
869
- if (errored > 0) log(chalk.red(` Errors: ${errored}`));
870
- log(`\n Package report: ${packageReportPath}`);
871
- log(` Artifacts: ${pkgWorkdir}\n`);
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({ options, igIds, verifyCanonical: prepared[0].canonical, log });
960
- log(chalk.green(` Validator server ready on port ${server.port}\n`));
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
- ({ results: batchResults } = await validateBatchViaServer(server.port, modifiedDir, files));
963
- log(chalk.green(' Validator batch completed\n'));
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
  };