diff --git a/CHANGELOG.md b/CHANGELOG.md index 04b67ce4..78d7efe3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,32 +8,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.18.1] - 2026-10-25 -### Security - -- - ### Added -- ICF (`http://hl7.org/fhir/sid/icf`): qualified codes. A category may be followed by `.` and one qualifier digit per position, the positions depending on the component: extent for body functions (`b`); extent, nature and localization for body structures (`s`); performance, capacity, and optionally capacity with assistance and performance without assistance for activities and participation (`d`); one for environmental factors (`e`), a barrier after `.` or a facilitator after `+` in place of the `.`. Each digit is checked against its scale in the code system (`q1`-`q6`, added to the ICF code systems in fhir.tx.support with a `kind` property). A qualified code gets a generated display (8, not specified, is left out of it; `d` qualifiers are labelled `performance`, `capacity`, ...), its qualifiers as properties in `$lookup` (`extent`, `nature`, `localization`, `performance`, `capacity`, `capacity-with-assistance`, `performance-without-assistance`, `barrier`, `facilitator`), and sits under its category for subsumption and `is-a` filters. Value sets can control qualifiers with an `exists` `true`/`false` filter on any of those properties (8 counts as absent) and `qualified` `=` `false`. ICF is now marked as not closed. The behaviour is in `tx/cs/cs-icf.js`, a subclass of the CodeSystem provider that `FhirCodeSystemProvider.create()` picks by url - the way to give any code system loaded from a package behaviour of its own +- ICF (`http://hl7.org/fhir/sid/icf`): qualified codes now cover environmental factors (`e`) - a barrier after `.`, or a facilitator after `+` in place of the `.` (no `+9`), on the new `q5` (barrier) and `q6` (facilitator) scales. As a whole: A category may be followed by `.` and one qualifier digit per position, the positions depending on the component: extent for body functions (`b`); extent, nature and localization for body structures (`s`); performance, capacity, and optionally capacity with assistance and performance without assistance for activities and participation (`d`); one for environmental factors (`e`), a barrier after `.` or a facilitator after `+` in place of the `.`. Each digit is checked against its scale in the code system (`q1`-`q6`, added to the ICF code systems in fhir.tx.support with a `kind` property). A qualified code gets a generated display (8, not specified, is left out of it; `d` qualifiers are labelled `performance`, `capacity`, ...), its qualifiers as properties in `$lookup` (`extent`, `nature`, `localization`, `performance`, `capacity`, `capacity-with-assistance`, `performance-without-assistance`, `barrier`, `facilitator`), and sits under its category for subsumption and `is-a` filters. Value sets can control qualifiers with an `exists` `true`/`false` filter on any of those properties (8 counts as absent) and `qualified` `=` `false`. ICF is now marked as not closed. The behaviour is in `tx/cs/cs-icf.js`, a subclass of the CodeSystem provider that `FhirCodeSystemProvider.create()` picks by url - the way to give any code system loaded from a package behaviour of its own - SNOMED CT alternate identifiers (`sct2_Identifier`, e.g. the LOINC Extension's LOINC codes): the importer reads them into the cache (cache format 18, written only when there are identifiers, so other caches stay 17). `$lookup` returns them as Coding properties named for the scheme (`loinc` = `http://loinc.org#...`), by default and when asked for. `$expand`: the filter `mapped:` `exists` `true`/`false` (e.g. `mapped:30051010000102`) for concepts that do or don't have one. `$translate`: each module with identifiers defines an implicit map `/version/?fhir_cm=` (e.g. `?fhir_cm=11010000107`), `equivalent` in both directions, nominated by url or found from the systems (`targetSystem` `http://loinc.org` from a SNOMED CT coding, or `http://snomed.info/sct|` from a LOINC one) -- `$lookup` on SNOMED CT: when the request names properties, only those are returned - effectiveTime, module, parent, child and the defining relationships are no longer added regardless -- Implicit ConceptMaps nominated by url are looked for in every version of a code system, not just the first loaded (a SNOMED CT edition's maps are named by its edition URI) - SNOMED CT test edition: a subset of the LOINC Extension (LOINC ontology, module `11010000107`) - twelve LOINC-coded concepts with their LOINC identifiers (`sct2_Identifier`), plus the module and identifier scheme concepts - generated from the LOINC Extension release by `utilities/make-sct-loinc-subset.js`. The test cache is now `sct_test_20250909d.cache` (2280 concepts) - LOINC importer: mappings from LOINC codes, parts and answers to other code systems, in two new tables - `ExternalCodes` (system, version, code, display; one row per target) and `ExternalMappings` (LOINC code to target, with an equivalence). Read from `PartFile/PartRelatedCodeMapping.csv` (the file's own equivalence; the RadLex rows are left out for now), `LoincIeeeMedicalDeviceCodeMappingTable` (MDC, `urn:iso:std:iso:11073:10101`, the CF_CODE10 as the code and the REFID as display; IEEE's `broader`/`narrower` are from the LOINC side, so they become `narrower`/`wider`) and the `ExtCode*` columns of `AnswerList.csv` (`equivalent`; LOINC's `CMS/HCPCS` and `https://www.cdc.gov/phin` systems become `http://www.cms.gov/Medicare/Coding/HCPCSReleaseCodeSets` and `urn:oid:2.16.840.1.113883.6.238`). Both mapping files are optional. LOINC caches need rebuilding to get them; nothing uses them yet -- LOINC importer (and subsetter): CSV files are read by record, not by line, so a quoted field with a line break in it is one record. LOINC 2.83's `ComponentHierarchyBySystem.csv` has one (the text of `LP409416-7`), and the import failed with `UNIQUE constraint failed: Config.ConfigKey` - the second half of the record looked like a second root - LOINC: the mappings to other code systems are used. `$lookup` returns them as Coding properties named for the map (`sct`, `chebi`, `mdc`, `race`...), with the equivalence as a subproperty, by default and when asked for. `$translate`: each target system has an implicit map `http://loinc.org/cm/` - `loinc`, `ncbi` (PubChem), `sct`, `hcpcs`, `gene-names`, `rxnorm`, `chebi`, `ncbi-clinvar`, `ncbi-gene`, `ncbi-taxo`, `mdc`, `race` - that works in both directions (LOINC to the system, and the system to LOINC), found from the target system or nominated by url. Found from the systems, a map is only offered for a code it has a mapping for, so translations that a SNOMED CT edition answers (the LOINC Extension) are unchanged. Targets in other systems (`CMS LCDS`, `Unicode`) are not used yet. Code system providers' `findImplicitConceptMaps` now also gets the code being translated +- `$closure`: `dispose` - a request with `name` and `dispose` = `true` says the client has finished with the closure table, and the server deletes it (not in the specification; disposing of a table that doesn't exist is fine - it may already have been pruned). It can't be combined with concepts, a version, `reset` or configuration (`CLOSURE_DISPOSE_NOT_ALONE`, 400). Documented in the OpenAPI ### Changed -- +- Global language packs: the `codesystem-supplement-type` code is now `global-lang-pack` (`lang-pack` is still accepted) +- `$lookup` on SNOMED CT: when the request names properties, only those are returned - effectiveTime, module, parent, child and the defining relationships are no longer added regardless +- Daily report: the title is now "{server} daily report for {date} covering the last {24 hours | n days}", and the summary is one line per item - `Versions:` (FHIRsmith and Node), `Uptime:` (this run, and the percentage over all recorded runs), `Space:` (the log files and the disk) and `Audit:` +- Tx test generator (`tx/tests/testcases-generator.js`): disabled suites and tests are left out, and `version` gates (including negated ones, `!4.0`) and `modes` lists are read as the Java runner reads them, so tests gated `!4.0` now run at R5. The rules are in `tx/tests/test-case-gates.js` ### Fixed -- +- Implicit ConceptMaps nominated by url are looked for in every version of a code system, not just the first loaded (a SNOMED CT edition's maps are named by its edition URI) +- LOINC importer (and subsetter): CSV files are read by record, not by line, so a quoted field with a line break in it is one record. LOINC 2.83's `ComponentHierarchyBySystem.csv` has one (the text of `LP409416-7`), and the import failed with `UNIQUE constraint failed: Config.ConfigKey` - the second half of the record looked like a second root +- `$expand`: a supplement that only supplied the chosen display or a designation - a global language pack, typically - was never reported as a `used-supplement`. Designations from a supplement carry it as `.supplement`, but the reporting looked for `.source`, which is never set +- `CodeSystem/$validate-code` with `useSupplement`: the supplement was always reported as missing (`VALUESET_SUPPLEMENT_MISSING`, 422), even when it was passed in `tx-resource`, because the supplements were only checked off while preparing a value set, and validating against a code system doesn't prepare one +- `$lookup` on a CodeSystem resource: a property asked for by its own code (`property=prop`) was not returned - only `property=property`, `*` or no `property` parameter returned the code system's properties. A property the server doesn't know is still ignored ### Tx Conformance Statement -(paste) +FHIRsmith v0.18.1 passed all 4337 HL7 terminology service tests (modes tx.fhir.org+omop+general+snomed+mimetypes+icd-11+icf+closure, tests v1.10.0, runner v7.0.3) ## [0.18.0] - 2026-10-23 diff --git a/package-lock.json b/package-lock.json index 5bf076af..cbf73b64 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "fhirsmith", - "version": "0.18.1-snapshot", + "version": "0.18.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "fhirsmith", - "version": "0.18.1-snapshot", + "version": "0.18.1", "license": "BSD-3", "dependencies": { "axios": "^1.13.4", diff --git a/package.json b/package.json index 225ea54a..b4590633 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "fhirsmith", - "version": "0.18.1-snapshot", - "txVersion": "1.9.6", + "version": "0.18.1", + "txVersion": "1.10.0", "description": "A Node.js server that provides a collection of tools to serve the FHIR ecosystem", "main": "server.js", "engines": { diff --git a/tests/tx/test-case-gates.test.js b/tests/tx/test-case-gates.test.js new file mode 100644 index 00000000..2a5d6e8e --- /dev/null +++ b/tests/tx/test-case-gates.test.js @@ -0,0 +1,38 @@ +const { versionGateMatches, passesModes, testVersions } = require('../../tx/tests/test-case-gates'); + +describe('tx test case gates', () => { + test('version gates are major.minor prefixes, optionally negated', () => { + expect(versionGateMatches(undefined, '5.0')).toBe(true); + expect(versionGateMatches('4.0', '4.0')).toBe(true); + expect(versionGateMatches('4.0', '4.0.1')).toBe(true); + expect(versionGateMatches('4.0', '5.0')).toBe(false); + expect(versionGateMatches('4.0', '4.01')).toBe(false); + expect(versionGateMatches('!4.0', '5.0')).toBe(true); + expect(versionGateMatches('!4.0', '4.0')).toBe(false); + }); + + test('a test gated "!4.0" runs at R5 only', () => { + expect(testVersions({}, { version: '!4.0' })).toEqual({ hasR5: true, hasR4: false }); + expect(testVersions({}, { version: '4.0' })).toEqual({ hasR5: false, hasR4: true }); + expect(testVersions({}, {})).toEqual({ hasR5: true, hasR4: true }); + }); + + test('a suite version gate applies to its tests', () => { + expect(testVersions({ version: '5.0' }, {})).toEqual({ hasR5: true, hasR4: false }); + expect(testVersions({ version: '5.0' }, { version: '4.0' })).toEqual({ hasR5: false, hasR4: false }); + }); + + test('disabled tests and suites are not generated', () => { + expect(testVersions({}, { disabled: true })).toEqual({ hasR5: false, hasR4: false }); + expect(testVersions({ disabled: true }, {})).toEqual({ hasR5: false, hasR4: false }); + }); + + test('modes', () => { + const modes = new Set(['general', 'snomed']); + expect(passesModes({}, modes)).toBe(true); + expect(passesModes({ mode: 'snomed' }, modes)).toBe(true); + expect(passesModes({ mode: 'omop' }, modes)).toBe(false); + expect(passesModes({ modes: ['omop', 'snomed'] }, modes)).toBe(true); + expect(passesModes({ modes: ['omop'] }, modes)).toBe(false); + }); +}); diff --git a/tests/tx/test-cases.test.js b/tests/tx/test-cases.test.js index 193c4536..fd345345 100644 --- a/tests/tx/test-cases.test.js +++ b/tests/tx/test-cases.test.js @@ -217,6 +217,30 @@ describe('simple-cases', () => { await runTest({"suite":"simple-cases","test":"simple-lookup-zxx"}, "4.0"); }); + it("simple-lookup-unknown-property" + 'R5', async () => { + await runTest({"suite":"simple-cases","test":"simple-lookup-unknown-property"}, "5.0"); + }); + + it("simple-lookup-unknown-property" + 'R4', async () => { + await runTest({"suite":"simple-cases","test":"simple-lookup-unknown-property"}, "4.0"); + }); + + it("simple-lookup-only-unknown-property" + 'R5', async () => { + await runTest({"suite":"simple-cases","test":"simple-lookup-only-unknown-property"}, "5.0"); + }); + + it("simple-lookup-only-unknown-property" + 'R4', async () => { + await runTest({"suite":"simple-cases","test":"simple-lookup-only-unknown-property"}, "4.0"); + }); + + it("simple-expand-unknown-property" + 'R5', async () => { + await runTest({"suite":"simple-cases","test":"simple-expand-unknown-property"}, "5.0"); + }); + + it("simple-expand-unknown-property" + 'R4', async () => { + await runTest({"suite":"simple-cases","test":"simple-expand-unknown-property"}, "4.0"); + }); + it("simple-expand-all-count" + 'R5', async () => { await runTest({"suite":"simple-cases","test":"simple-expand-all-count"}, "5.0"); }); @@ -1098,6 +1122,91 @@ describe('language2', () => { }); +describe('global-lang-pack', () => { + // Global language packs: a CodeSystem supplement marked with the codesystem-supplement-type extension, value global-lang-pack, is used for the code system it supplements whether or not it is referenced; any other supplement is only used when it is referenced + + it("glp-cs-validate-de" + 'R5', async () => { + await runTest({"suite":"global-lang-pack","test":"glp-cs-validate-de"}, "5.0"); + }); + + it("glp-cs-validate-de" + 'R4', async () => { + await runTest({"suite":"global-lang-pack","test":"glp-cs-validate-de"}, "4.0"); + }); + + it("glp-validate-de" + 'R5', async () => { + await runTest({"suite":"global-lang-pack","test":"glp-validate-de"}, "5.0"); + }); + + it("glp-validate-de" + 'R4', async () => { + await runTest({"suite":"global-lang-pack","test":"glp-validate-de"}, "4.0"); + }); + + it("glp-validate-nl-unreferenced" + 'R5', async () => { + await runTest({"suite":"global-lang-pack","test":"glp-validate-nl-unreferenced"}, "5.0"); + }); + + it("glp-validate-nl-unreferenced" + 'R4', async () => { + await runTest({"suite":"global-lang-pack","test":"glp-validate-nl-unreferenced"}, "4.0"); + }); + + it("glp-validate-nl-referenced" + 'R5', async () => { + await runTest({"suite":"global-lang-pack","test":"glp-validate-nl-referenced"}, "5.0"); + }); + + it("glp-validate-nl-referenced" + 'R4', async () => { + await runTest({"suite":"global-lang-pack","test":"glp-validate-nl-referenced"}, "4.0"); + }); + + it("glp-cs-validate-nl-unreferenced" + 'R5', async () => { + await runTest({"suite":"global-lang-pack","test":"glp-cs-validate-nl-unreferenced"}, "5.0"); + }); + + it("glp-cs-validate-nl-unreferenced" + 'R4', async () => { + await runTest({"suite":"global-lang-pack","test":"glp-cs-validate-nl-unreferenced"}, "4.0"); + }); + + it("glp-cs-validate-nl-referenced" + 'R5', async () => { + await runTest({"suite":"global-lang-pack","test":"glp-cs-validate-nl-referenced"}, "5.0"); + }); + + it("glp-cs-validate-nl-referenced" + 'R4', async () => { + await runTest({"suite":"global-lang-pack","test":"glp-cs-validate-nl-referenced"}, "4.0"); + }); + + it("glp-lookup-de" + 'R5', async () => { + await runTest({"suite":"global-lang-pack","test":"glp-lookup-de"}, "5.0"); + }); + + it("glp-lookup-de" + 'R4', async () => { + await runTest({"suite":"global-lang-pack","test":"glp-lookup-de"}, "4.0"); + }); + + it("glp-lookup-other" + 'R5', async () => { + await runTest({"suite":"global-lang-pack","test":"glp-lookup-other"}, "5.0"); + }); + + it("glp-lookup-other" + 'R4', async () => { + await runTest({"suite":"global-lang-pack","test":"glp-lookup-other"}, "4.0"); + }); + + it("glp-expand-de" + 'R5', async () => { + await runTest({"suite":"global-lang-pack","test":"glp-expand-de"}, "5.0"); + }); + + it("glp-expand-de" + 'R4', async () => { + await runTest({"suite":"global-lang-pack","test":"glp-expand-de"}, "4.0"); + }); + + it("glp-expand-en" + 'R5', async () => { + await runTest({"suite":"global-lang-pack","test":"glp-expand-en"}, "5.0"); + }); + + it("glp-expand-en" + 'R4', async () => { + await runTest({"suite":"global-lang-pack","test":"glp-expand-en"}, "4.0"); + }); + +}); + describe('extensions', () => { // Testing proper handling of extensions, which depends on the extension @@ -6556,6 +6665,110 @@ describe('snomed', () => { await runTest({"suite":"snomed","test":"snomed-translate-edition-inactive-replaced"}, "4.0"); }); + it("snomed-lookup-loinc" + 'R5', async () => { + await runTest({"suite":"snomed","test":"snomed-lookup-loinc"}, "5.0"); + }); + + it("snomed-lookup-loinc" + 'R4', async () => { + await runTest({"suite":"snomed","test":"snomed-lookup-loinc"}, "4.0"); + }); + + it("snomed-lookup-loinc-inactive" + 'R5', async () => { + await runTest({"suite":"snomed","test":"snomed-lookup-loinc-inactive"}, "5.0"); + }); + + it("snomed-lookup-loinc-inactive" + 'R4', async () => { + await runTest({"suite":"snomed","test":"snomed-lookup-loinc-inactive"}, "4.0"); + }); + + it("snomed-lookup-loinc-none" + 'R5', async () => { + await runTest({"suite":"snomed","test":"snomed-lookup-loinc-none"}, "5.0"); + }); + + it("snomed-lookup-loinc-none" + 'R4', async () => { + await runTest({"suite":"snomed","test":"snomed-lookup-loinc-none"}, "4.0"); + }); + + it("snomed-lookup-loinc-default" + 'R5', async () => { + await runTest({"suite":"snomed","test":"snomed-lookup-loinc-default"}, "5.0"); + }); + + it("snomed-lookup-loinc-default" + 'R4', async () => { + await runTest({"suite":"snomed","test":"snomed-lookup-loinc-default"}, "4.0"); + }); + + it("snomed-expand-loinc" + 'R5', async () => { + await runTest({"suite":"snomed","test":"snomed-expand-loinc"}, "5.0"); + }); + + it("snomed-expand-loinc" + 'R4', async () => { + await runTest({"suite":"snomed","test":"snomed-expand-loinc"}, "4.0"); + }); + + it("snomed-expand-loinc-isa" + 'R5', async () => { + await runTest({"suite":"snomed","test":"snomed-expand-loinc-isa"}, "5.0"); + }); + + it("snomed-expand-loinc-isa" + 'R4', async () => { + await runTest({"suite":"snomed","test":"snomed-expand-loinc-isa"}, "4.0"); + }); + + it("snomed-expand-loinc-isa-none" + 'R5', async () => { + await runTest({"suite":"snomed","test":"snomed-expand-loinc-isa-none"}, "5.0"); + }); + + it("snomed-expand-loinc-isa-none" + 'R4', async () => { + await runTest({"suite":"snomed","test":"snomed-expand-loinc-isa-none"}, "4.0"); + }); + + it("snomed-translate-loinc" + 'R5', async () => { + await runTest({"suite":"snomed","test":"snomed-translate-loinc"}, "5.0"); + }); + + it("snomed-translate-loinc" + 'R4', async () => { + await runTest({"suite":"snomed","test":"snomed-translate-loinc"}, "4.0"); + }); + + it("snomed-translate-loinc-none" + 'R5', async () => { + await runTest({"suite":"snomed","test":"snomed-translate-loinc-none"}, "5.0"); + }); + + it("snomed-translate-loinc-none" + 'R4', async () => { + await runTest({"suite":"snomed","test":"snomed-translate-loinc-none"}, "4.0"); + }); + + it("snomed-translate-loinc-reverse" + 'R5', async () => { + await runTest({"suite":"snomed","test":"snomed-translate-loinc-reverse"}, "5.0"); + }); + + it("snomed-translate-loinc-reverse" + 'R4', async () => { + await runTest({"suite":"snomed","test":"snomed-translate-loinc-reverse"}, "4.0"); + }); + + it("snomed-translate-loinc-reverse-none" + 'R5', async () => { + await runTest({"suite":"snomed","test":"snomed-translate-loinc-reverse-none"}, "5.0"); + }); + + it("snomed-translate-loinc-reverse-none" + 'R4', async () => { + await runTest({"suite":"snomed","test":"snomed-translate-loinc-reverse-none"}, "4.0"); + }); + + it("snomed-translate-loinc-system" + 'R5', async () => { + await runTest({"suite":"snomed","test":"snomed-translate-loinc-system"}, "5.0"); + }); + + it("snomed-translate-loinc-system" + 'R4', async () => { + await runTest({"suite":"snomed","test":"snomed-translate-loinc-system"}, "4.0"); + }); + + it("snomed-translate-loinc-system-reverse" + 'R5', async () => { + await runTest({"suite":"snomed","test":"snomed-translate-loinc-system-reverse"}, "5.0"); + }); + + it("snomed-translate-loinc-system-reverse" + 'R4', async () => { + await runTest({"suite":"snomed","test":"snomed-translate-loinc-system-reverse"}, "4.0"); + }); + }); describe('sct-ecl', () => { @@ -10941,6 +11154,10 @@ describe('closure', () => { await runTest({"suite":"closure","test":"closure-resync-0"}, "5.0"); }); + it("closure-dispose" + 'R5', async () => { + await runTest({"suite":"closure","test":"closure-dispose"}, "5.0"); + }); + it("closure-init" + 'R4', async () => { await runTest({"suite":"closure","test":"closure-init"}, "4.0"); }); @@ -10981,6 +11198,10 @@ describe('closure', () => { await runTest({"suite":"closure","test":"closure-resync-0"}, "4.0"); }); + it("closure-dispose" + 'R4', async () => { + await runTest({"suite":"closure","test":"closure-dispose"}, "4.0"); + }); + }); describe('icf', () => { @@ -11226,6 +11447,54 @@ describe('icf', () => { await runTest({"suite":"icf","test":"icf-cs-facilitator"}, "4.0"); }); + it("icf-cs-e-no-facilitator" + 'R5', async () => { + await runTest({"suite":"icf","test":"icf-cs-e-no-facilitator"}, "5.0"); + }); + + it("icf-cs-e-no-facilitator" + 'R4', async () => { + await runTest({"suite":"icf","test":"icf-cs-e-no-facilitator"}, "4.0"); + }); + + it("icf-cs-e-barrier-nine" + 'R5', async () => { + await runTest({"suite":"icf","test":"icf-cs-e-barrier-nine"}, "5.0"); + }); + + it("icf-cs-e-barrier-nine" + 'R4', async () => { + await runTest({"suite":"icf","test":"icf-cs-e-barrier-nine"}, "4.0"); + }); + + it("icf-cs-e-facilitator-eight" + 'R5', async () => { + await runTest({"suite":"icf","test":"icf-cs-e-facilitator-eight"}, "5.0"); + }); + + it("icf-cs-e-facilitator-eight" + 'R4', async () => { + await runTest({"suite":"icf","test":"icf-cs-e-facilitator-eight"}, "4.0"); + }); + + it("icf-cs-e-facilitator-nine" + 'R5', async () => { + await runTest({"suite":"icf","test":"icf-cs-e-facilitator-nine"}, "5.0"); + }); + + it("icf-cs-e-facilitator-nine" + 'R4', async () => { + await runTest({"suite":"icf","test":"icf-cs-e-facilitator-nine"}, "4.0"); + }); + + it("icf-cs-e-two" + 'R5', async () => { + await runTest({"suite":"icf","test":"icf-cs-e-two"}, "5.0"); + }); + + it("icf-cs-e-two" + 'R4', async () => { + await runTest({"suite":"icf","test":"icf-cs-e-two"}, "4.0"); + }); + + it("icf-cs-b-facilitator" + 'R5', async () => { + await runTest({"suite":"icf","test":"icf-cs-b-facilitator"}, "5.0"); + }); + + it("icf-cs-b-facilitator" + 'R4', async () => { + await runTest({"suite":"icf","test":"icf-cs-b-facilitator"}, "4.0"); + }); + it("icf-vs-enum-plain" + 'R5', async () => { await runTest({"suite":"icf","test":"icf-vs-enum-plain"}, "5.0"); }); @@ -11434,6 +11703,38 @@ describe('icf', () => { await runTest({"suite":"icf","test":"icf-vs-capacity-plain"}, "4.0"); }); + it("icf-vs-facilitator-good" + 'R5', async () => { + await runTest({"suite":"icf","test":"icf-vs-facilitator-good"}, "5.0"); + }); + + it("icf-vs-facilitator-good" + 'R4', async () => { + await runTest({"suite":"icf","test":"icf-vs-facilitator-good"}, "4.0"); + }); + + it("icf-vs-facilitator-barrier" + 'R5', async () => { + await runTest({"suite":"icf","test":"icf-vs-facilitator-barrier"}, "5.0"); + }); + + it("icf-vs-facilitator-barrier" + 'R4', async () => { + await runTest({"suite":"icf","test":"icf-vs-facilitator-barrier"}, "4.0"); + }); + + it("icf-vs-facilitator-eight" + 'R5', async () => { + await runTest({"suite":"icf","test":"icf-vs-facilitator-eight"}, "5.0"); + }); + + it("icf-vs-facilitator-eight" + 'R4', async () => { + await runTest({"suite":"icf","test":"icf-vs-facilitator-eight"}, "4.0"); + }); + + it("icf-vs-facilitator-plain" + 'R5', async () => { + await runTest({"suite":"icf","test":"icf-vs-facilitator-plain"}, "5.0"); + }); + + it("icf-vs-facilitator-plain" + 'R4', async () => { + await runTest({"suite":"icf","test":"icf-vs-facilitator-plain"}, "4.0"); + }); + it("icf-vs-noqual-plain" + 'R5', async () => { await runTest({"suite":"icf","test":"icf-vs-noqual-plain"}, "5.0"); }); @@ -11581,6 +11882,18 @@ describe('simple-cases', () => { await runTest({"suite":"simple-cases","test":"simple-lookup-zxx"}, "5.0"); }); + it("simple-lookup-unknown-property" + 'R5-cached', async () => { + await runTest({"suite":"simple-cases","test":"simple-lookup-unknown-property"}, "5.0"); + }); + + it("simple-lookup-only-unknown-property" + 'R5-cached', async () => { + await runTest({"suite":"simple-cases","test":"simple-lookup-only-unknown-property"}, "5.0"); + }); + + it("simple-expand-unknown-property" + 'R5-cached', async () => { + await runTest({"suite":"simple-cases","test":"simple-expand-unknown-property"}, "5.0"); + }); + it("simple-expand-all-count" + 'R5-cached', async () => { await runTest({"suite":"simple-cases","test":"simple-expand-all-count"}, "5.0"); }); @@ -12030,6 +12343,51 @@ describe('language2', () => { }); +describe('global-lang-pack', () => { + // Global language packs: a CodeSystem supplement marked with the codesystem-supplement-type extension, value global-lang-pack, is used for the code system it supplements whether or not it is referenced; any other supplement is only used when it is referenced + + it("glp-cs-validate-de" + 'R5-cached', async () => { + await runTest({"suite":"global-lang-pack","test":"glp-cs-validate-de"}, "5.0"); + }); + + it("glp-validate-de" + 'R5-cached', async () => { + await runTest({"suite":"global-lang-pack","test":"glp-validate-de"}, "5.0"); + }); + + it("glp-validate-nl-unreferenced" + 'R5-cached', async () => { + await runTest({"suite":"global-lang-pack","test":"glp-validate-nl-unreferenced"}, "5.0"); + }); + + it("glp-validate-nl-referenced" + 'R5-cached', async () => { + await runTest({"suite":"global-lang-pack","test":"glp-validate-nl-referenced"}, "5.0"); + }); + + it("glp-cs-validate-nl-unreferenced" + 'R5-cached', async () => { + await runTest({"suite":"global-lang-pack","test":"glp-cs-validate-nl-unreferenced"}, "5.0"); + }); + + it("glp-cs-validate-nl-referenced" + 'R5-cached', async () => { + await runTest({"suite":"global-lang-pack","test":"glp-cs-validate-nl-referenced"}, "5.0"); + }); + + it("glp-lookup-de" + 'R5-cached', async () => { + await runTest({"suite":"global-lang-pack","test":"glp-lookup-de"}, "5.0"); + }); + + it("glp-lookup-other" + 'R5-cached', async () => { + await runTest({"suite":"global-lang-pack","test":"glp-lookup-other"}, "5.0"); + }); + + it("glp-expand-de" + 'R5-cached', async () => { + await runTest({"suite":"global-lang-pack","test":"glp-expand-de"}, "5.0"); + }); + + it("glp-expand-en" + 'R5-cached', async () => { + await runTest({"suite":"global-lang-pack","test":"glp-expand-en"}, "5.0"); + }); + +}); + describe('extensions', () => { // Testing proper handling of extensions, which depends on the extension @@ -14820,6 +15178,58 @@ describe('snomed', () => { await runTest({"suite":"snomed","test":"snomed-translate-edition-inactive-replaced"}, "5.0"); }); + it("snomed-lookup-loinc" + 'R5-cached', async () => { + await runTest({"suite":"snomed","test":"snomed-lookup-loinc"}, "5.0"); + }); + + it("snomed-lookup-loinc-inactive" + 'R5-cached', async () => { + await runTest({"suite":"snomed","test":"snomed-lookup-loinc-inactive"}, "5.0"); + }); + + it("snomed-lookup-loinc-none" + 'R5-cached', async () => { + await runTest({"suite":"snomed","test":"snomed-lookup-loinc-none"}, "5.0"); + }); + + it("snomed-lookup-loinc-default" + 'R5-cached', async () => { + await runTest({"suite":"snomed","test":"snomed-lookup-loinc-default"}, "5.0"); + }); + + it("snomed-expand-loinc" + 'R5-cached', async () => { + await runTest({"suite":"snomed","test":"snomed-expand-loinc"}, "5.0"); + }); + + it("snomed-expand-loinc-isa" + 'R5-cached', async () => { + await runTest({"suite":"snomed","test":"snomed-expand-loinc-isa"}, "5.0"); + }); + + it("snomed-expand-loinc-isa-none" + 'R5-cached', async () => { + await runTest({"suite":"snomed","test":"snomed-expand-loinc-isa-none"}, "5.0"); + }); + + it("snomed-translate-loinc" + 'R5-cached', async () => { + await runTest({"suite":"snomed","test":"snomed-translate-loinc"}, "5.0"); + }); + + it("snomed-translate-loinc-none" + 'R5-cached', async () => { + await runTest({"suite":"snomed","test":"snomed-translate-loinc-none"}, "5.0"); + }); + + it("snomed-translate-loinc-reverse" + 'R5-cached', async () => { + await runTest({"suite":"snomed","test":"snomed-translate-loinc-reverse"}, "5.0"); + }); + + it("snomed-translate-loinc-reverse-none" + 'R5-cached', async () => { + await runTest({"suite":"snomed","test":"snomed-translate-loinc-reverse-none"}, "5.0"); + }); + + it("snomed-translate-loinc-system" + 'R5-cached', async () => { + await runTest({"suite":"snomed","test":"snomed-translate-loinc-system"}, "5.0"); + }); + + it("snomed-translate-loinc-system-reverse" + 'R5-cached', async () => { + await runTest({"suite":"snomed","test":"snomed-translate-loinc-system-reverse"}, "5.0"); + }); + }); describe('sct-ecl', () => { @@ -17073,6 +17483,10 @@ describe('closure', () => { await runTest({"suite":"closure","test":"closure-resync-0"}, "5.0"); }); + it("closure-dispose" + 'R5-cached', async () => { + await runTest({"suite":"closure","test":"closure-dispose"}, "5.0"); + }); + }); describe('icf', () => { @@ -17198,6 +17612,30 @@ describe('icf', () => { await runTest({"suite":"icf","test":"icf-cs-facilitator"}, "5.0"); }); + it("icf-cs-e-no-facilitator" + 'R5-cached', async () => { + await runTest({"suite":"icf","test":"icf-cs-e-no-facilitator"}, "5.0"); + }); + + it("icf-cs-e-barrier-nine" + 'R5-cached', async () => { + await runTest({"suite":"icf","test":"icf-cs-e-barrier-nine"}, "5.0"); + }); + + it("icf-cs-e-facilitator-eight" + 'R5-cached', async () => { + await runTest({"suite":"icf","test":"icf-cs-e-facilitator-eight"}, "5.0"); + }); + + it("icf-cs-e-facilitator-nine" + 'R5-cached', async () => { + await runTest({"suite":"icf","test":"icf-cs-e-facilitator-nine"}, "5.0"); + }); + + it("icf-cs-e-two" + 'R5-cached', async () => { + await runTest({"suite":"icf","test":"icf-cs-e-two"}, "5.0"); + }); + + it("icf-cs-b-facilitator" + 'R5-cached', async () => { + await runTest({"suite":"icf","test":"icf-cs-b-facilitator"}, "5.0"); + }); + it("icf-vs-enum-plain" + 'R5-cached', async () => { await runTest({"suite":"icf","test":"icf-vs-enum-plain"}, "5.0"); }); @@ -17302,6 +17740,22 @@ describe('icf', () => { await runTest({"suite":"icf","test":"icf-vs-capacity-plain"}, "5.0"); }); + it("icf-vs-facilitator-good" + 'R5-cached', async () => { + await runTest({"suite":"icf","test":"icf-vs-facilitator-good"}, "5.0"); + }); + + it("icf-vs-facilitator-barrier" + 'R5-cached', async () => { + await runTest({"suite":"icf","test":"icf-vs-facilitator-barrier"}, "5.0"); + }); + + it("icf-vs-facilitator-eight" + 'R5-cached', async () => { + await runTest({"suite":"icf","test":"icf-vs-facilitator-eight"}, "5.0"); + }); + + it("icf-vs-facilitator-plain" + 'R5-cached', async () => { + await runTest({"suite":"icf","test":"icf-vs-facilitator-plain"}, "5.0"); + }); + it("icf-vs-noqual-plain" + 'R5-cached', async () => { await runTest({"suite":"icf","test":"icf-vs-noqual-plain"}, "5.0"); }); diff --git a/tests/tx/worker.test.js b/tests/tx/worker.test.js index 75a8294b..5731146a 100644 --- a/tests/tx/worker.test.js +++ b/tests/tx/worker.test.js @@ -247,7 +247,7 @@ describe('TerminologyWorker', () => { name: 'SupplementCS', status: 'active', supplements: 'http://main.com', - extension : [{url: 'http://hl7.org/fhir/StructureDefinition/codesystem-supplement-type', valueCode: "lang-pack"}] + extension : [{url: 'http://hl7.org/fhir/StructureDefinition/codesystem-supplement-type', valueCode: "global-lang-pack"}] }); worker.additionalResources = [mainCS, supplementCS]; diff --git a/tx/cs/cs-cs.js b/tx/cs/cs-cs.js index 1f88b176..801f3425 100644 --- a/tx/cs/cs-cs.js +++ b/tx/cs/cs-cs.js @@ -1027,11 +1027,17 @@ class FhirCodeSystemProvider extends BaseCSServices { return; } - // Add properties if requested (or by default) - if (this._hasProp(props, 'property', true)) { + // Add the concept's properties: each one if it was asked for by its code (or '*', or nothing + // was asked for). 'property' asks for all of them - it used to be the only thing that did, so + // asking for a property by its own code returned nothing. A code the server doesn't know is + // simply not matched + { const properties = await this.properties(ctxt); if (properties) { for (const property of properties) { + if (!this._hasProp(props, property.code, true) && !this._hasProp(props, 'property', true)) { + continue; + } let parts = []; parts.push({ name: 'code', valueCode: property.code }); diff --git a/tx/library/codesystem.js b/tx/library/codesystem.js index d8bc4d8b..80aca617 100644 --- a/tx/library/codesystem.js +++ b/tx/library/codesystem.js @@ -649,7 +649,10 @@ class CodeSystem extends CanonicalResource { if (this.jsonObj.url === 'https://terminology.dhp.uz/fhir/CodeSystem/loinc-supplement-uz') { return true; } - return (this.jsonObj.extension || []).find(x => x.url == 'http://hl7.org/fhir/StructureDefinition/codesystem-supplement-type' && getValuePrimitive(x) == 'lang-pack'); + // a global language pack is used whether or not it is referenced. The code was 'lang-pack' + // until Oct 2026; it is still accepted, so that supplements marked the old way keep working + return (this.jsonObj.extension || []).find(x => x.url == 'http://hl7.org/fhir/StructureDefinition/codesystem-supplement-type' + && ['global-lang-pack', 'lang-pack'].includes(getValuePrimitive(x))); } isPropUri(code, uri) { diff --git a/tx/library/designations.js b/tx/library/designations.js index 184bf62c..3ca98a41 100644 --- a/tx/library/designations.js +++ b/tx/library/designations.js @@ -564,6 +564,19 @@ class Designations { return this._langsMatch(langList, cd.language, LangMatchType.LANG, defLang); } + /** + * Note, in the set of supplements to report as used, the supplement a designation came from (if + * it came from one). Designations from a supplement carry it as .supplement (see + * CodeSystemProvider._listSupplementDesignations); this used to look for .source, which is never + * set on a designation, so a supplement that only supplied the chosen display - a global language + * pack, typically - was never reported as used. + */ + _reportSupplement(supplements, cd) { + if (supplements && cd && cd.supplement && cd.supplement.vurl) { + supplements.add(cd.supplement.vurl); + } + } + /** * Find the preferred designation for given language preferences */ @@ -576,39 +589,29 @@ class Designations { // No language list, prefer base designations for (const cd of this.designations) { if (this._isPreferred(cd) && cd.isActive()) { - if (supplements && cd.source) { - supplements.add(cd.source); - } + this._reportSupplement(supplements, cd); return cd; } } for (const cd of this.designations) { if (this.isDisplay(cd) && cd.isActive()) { - if (supplements && cd.source) { - supplements.add(cd.source); - } + this._reportSupplement(supplements, cd); return cd; } } for (const cd of this.designations) { if (this.isDisplay(cd)) { - if (supplements && cd.source) { - supplements.add(cd.source); - } + this._reportSupplement(supplements, cd); return cd; } } for (const cd of this.designations) { if (this._isPreferred(cd)) { - if (supplements && cd.source) { - supplements.add(cd.source); - } + this._reportSupplement(supplements, cd); return cd; } } - if (supplements && this.designations[0].source) { - supplements.add(this.designations[0].source); - } + this._reportSupplement(supplements, this.designations[0]); return this.designations[0]; } @@ -628,25 +631,19 @@ class Designations { // because the FSN was emitted first. for (const cd of this.designations) { if (this._langMatches(lang, cd.language, matchType) && this._isPreferred(cd) && (!activeOnly || cd.isActive())) { - if (supplements && cd.source) { - supplements.add(cd.source); - } + this._reportSupplement(supplements, cd); return cd; } } for (const cd of this.designations) { if (this._langMatches(lang, cd.language, matchType) && this.isDisplay(cd) && (!activeOnly || cd.isActive())) { - if (supplements && cd.source) { - supplements.add(cd.source); - } + this._reportSupplement(supplements, cd); return cd; } } for (const cd of this.designations) { if (this._langMatches(lang, cd.language, matchType) && (!activeOnly || cd.isActive())) { - if (supplements && cd.source) { - supplements.add(cd.source); - } + this._reportSupplement(supplements, cd); return cd; } } @@ -655,14 +652,10 @@ class Designations { } for (const cd of this.designations) { if (!cd.language && this.isDisplay(cd) && cd.isActive()) { - if (supplements && cd.source) { - supplements.add(cd.source); - } + this._reportSupplement(supplements, cd); return cd; } if (!cd.language && this.isDisplay(cd)) { - if (supplements && cd.source) { - supplements.add(cd.source); - } + this._reportSupplement(supplements, cd); return cd; } } diff --git a/tx/tests/test-case-gates.js b/tx/tests/test-case-gates.js new file mode 100644 index 00000000..cc11200e --- /dev/null +++ b/tx/tests/test-case-gates.js @@ -0,0 +1,40 @@ +// Which of the tx-ecosystem test cases the generator emits, and at which FHIR versions. Kept +// apart from the generator so that it can be tested without loading the test runner. + +/** + * Does a suite's or test's "version" gate let it run against this FHIR version? The same rule as + * the Java runner (TxTester.versionGateMatches): the gate is a major.minor prefix - "4.0" runs + * only on R4 - optionally negated with a leading "!", so "!4.0" runs on everything except R4. + */ +function versionGateMatches(gate, ver) { + if (!gate) { + return true; + } + if (gate.startsWith('!')) { + return !versionGateMatches(gate.substring(1), ver); + } + return ver === gate || ver.startsWith(gate + '.'); +} + +/** Does a suite or test run in these modes? A "modes" list needs any one of them, a "mode" that one. */ +function passesModes(item, modes) { + if (Array.isArray(item.modes)) { + return item.modes.some(m => modes.has(m)); + } + return !item.mode || modes.has(item.mode); +} + +/** + * The FHIR versions a test runs at: none if it, or its suite, is disabled (the runner reports a + * disabled test as one that did not run, which counts as a failure here), and otherwise those its + * suite's and its own version gates let through. + */ +function testVersions(suite, test) { + if (suite.disabled || test.disabled) { + return { hasR5: false, hasR4: false }; + } + const runsAt = (v) => versionGateMatches(suite.version, v) && versionGateMatches(test.version, v); + return { hasR5: runsAt('5.0'), hasR4: runsAt('4.0') }; +} + +module.exports = { versionGateMatches, passesModes, testVersions }; diff --git a/tx/tests/test-cases-version.js b/tx/tests/test-cases-version.js index 938cddb6..fd4b979c 100644 --- a/tx/tests/test-cases-version.js +++ b/tx/tests/test-cases-version.js @@ -3,6 +3,6 @@ // Regenerate with: node generate-tests.js function txTestVersion() { - return '1.9.6'; + return '1.10.0'; } module.exports = { txTestVersion }; diff --git a/tx/tests/testcases-generator.js b/tx/tests/testcases-generator.js index bf958155..d02c4f6e 100644 --- a/tx/tests/testcases-generator.js +++ b/tx/tests/testcases-generator.js @@ -3,6 +3,7 @@ const path = require('path'); const { PackageManager} = require("../../library/package-manager"); const {txTestModeSet} = require("./test-runner"); const folders = require('../../library/folder-setup'); +const { passesModes, testVersions } = require('./test-case-gates'); const OUTPUT_FILE1 = path.join(__dirname, '../..', 'tests/tx/test-cases.test.js'); const OUTPUT_FILE2 = path.join(__dirname, 'test-cases-version.js'); @@ -47,7 +48,7 @@ module.exports = { txTestVersion }; const emitSuites = (suffix, oneVersion = false) => { let s = ''; for (const suite of testCases.suites) { - if (!suite.mode || modes.has(suite.mode)) { + if (passesModes(suite, modes) && !suite.disabled) { s += `describe('${suite.name}', () => {\n`; if (suite.description) { @@ -61,15 +62,14 @@ module.exports = { txTestVersion }; const passes = (suite.sequential && !oneVersion) ? ['R5', 'R4'] : [null]; for (const pass of passes) for (const test of suite.tests) { - if ((!test.mode || modes.has(test.mode)) && (!test["full-set"])) { + if (passesModes(test, modes) && (!test["full-set"])) { let testDetails = { suite: suite.name, test: test.name } const escapedNameLiteral = JSON.stringify(test.name); - const hasR5 = (!test.version || test.version.startsWith("5.0")); - const hasR4 = (!test.version || test.version.startsWith("4.0")); + const { hasR5, hasR4 } = testVersions(suite, test); const emitR5 = () => { s += ` it(${escapedNameLiteral} + 'R5${suffix}', async () => {\n`; s += ` await runTest(${JSON.stringify(testDetails)}, "5.0");\n`; diff --git a/tx/workers/expand.js b/tx/workers/expand.js index bff9c9df..606de1c9 100644 --- a/tx/workers/expand.js +++ b/tx/workers/expand.js @@ -439,8 +439,8 @@ class ValueSetExpander { if (!n.designation) { n.designation = []; } - if (t.source) { - this.reportedSupplements.add(t.source); + if (t.supplement && t.supplement.vurl) { + this.reportedSupplements.add(t.supplement.vurl); } n.designation.push(t.asObject()); } diff --git a/tx/workers/validate.js b/tx/workers/validate.js index 020ab25d..55636955 100644 --- a/tx/workers/validate.js +++ b/tx/workers/validate.js @@ -2467,6 +2467,16 @@ class ValidateWorker extends TerminologyWorker { let vs = this.makeVsForCS(codeSystem); + // The checker isn't prepared (there is no real value set to analyse), and preparing is + // where the supplements asked for with useSupplement are found. So note the ones the code + // system has here, or every useSupplement on CodeSystem/$validate-code is reported as + // missing, even when the supplement was passed in tx-resource + for (const s of this.requiredSupplements) { + if (codeSystem.hasSupplement(s)) { + this.usedSupplements.add(s); + } + } + // Create and prepare checker const checker = new ValueSetChecker(this, vs, params);