fhirsmith 0.18.1 → 1.0.0
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/ACCESSIBILITY.md +119 -0
- package/CHANGELOG.md +190 -9
- package/CONFIGURATION.md +305 -0
- package/LICENSE +29 -0
- package/README.md +29 -122
- package/cluster.js +1220 -0
- package/cluster.md +707 -0
- package/config-template.json +77 -8
- package/extension-tracker/extension-tracker-template.html +5 -8
- package/extension-tracker/extension-tracker.js +0 -2
- package/folder/folder.js +7 -6
- package/ips/analysis.js +560 -0
- package/ips/ips-template.html +5 -8
- package/ips/ips.js +331 -33
- package/ips/jurisdictions.js +65 -0
- package/ips/nationality.js +120 -0
- package/ips/pipeline.js +131 -17
- package/ips/presenter.js +1 -1
- package/ips/profile-editor.js +283 -4
- package/ips/readme.md +170 -20
- package/ips/render.js +769 -109
- package/ips/scripts.js +46 -0
- package/ips/store.js +123 -11
- package/ips/structure.js +247 -0
- package/ips/terminology.js +1 -1
- package/library/body-limit.js +22 -0
- package/library/cache-generation.js +15 -0
- package/library/cgroup-memory.js +133 -1
- package/library/cluster-counts.js +121 -0
- package/library/cluster-events.js +153 -0
- package/library/cluster-metrics.js +193 -0
- package/library/cluster-status.js +468 -0
- package/library/config-reader.js +96 -0
- package/library/daily-report.js +100 -2
- package/library/html-server.js +27 -1
- package/library/i18nsupport.js +23 -1
- package/library/keep-alive.js +18 -0
- package/library/languages.js +17 -3
- package/library/logger.js +31 -0
- package/library/memory-guard.js +428 -0
- package/library/native-memory.js +52 -0
- package/library/npm-audit.js +51 -25
- package/library/openapi-doc.js +7 -5
- package/library/package-manager.js +8 -0
- package/library/role.js +197 -0
- package/library/server-credentials.js +169 -0
- package/library/sqlite-utilities.js +59 -0
- package/library/ssrf-guard.js +100 -0
- package/library/static-response-stats.js +16 -0
- package/library/version-utilities.js +50 -0
- package/library/worker-count.js +192 -0
- package/nginx.md +30 -11
- package/npmprojector/indexer.js +18 -10
- package/package.json +11 -8
- package/packages/feed-check-page.js +117 -0
- package/packages/feed-check.js +279 -0
- package/packages/npm-check.js +57 -3
- package/packages/npm-signing.js +219 -0
- package/packages/openapi.yaml +207 -1
- package/packages/package-crawler.js +322 -41
- package/packages/packages-template.html +6 -8
- package/packages/packages.js +604 -57
- package/packages/readme.md +113 -1
- package/publisher/publisher-template.html +2 -5
- package/publisher/publisher.js +121 -110
- package/publisher/readme.md +75 -0
- package/registry/api.js +179 -1
- package/registry/crawler.js +86 -108
- package/registry/documentation/api.md +68 -2
- package/registry/documentation/configuration.md +34 -0
- package/registry/documentation/internals.md +4 -1
- package/registry/documentation/readme.md +3 -2
- package/registry/model.js +8 -0
- package/registry/openapi.yaml +84 -0
- package/registry/registry-template.html +5 -8
- package/registry/registry.js +58 -6
- package/root-bare-template.html +7 -4
- package/root-template.html +5 -8
- package/server.js +398 -198
- package/shl/shl.js +38 -37
- package/static/a11y/a11y-check.js +832 -0
- package/static/a11y/axe.min.js +12 -0
- package/static/assets/css/bootstrap-fhir.css +15 -15
- package/static/assets/images/flags/au.svg +18 -0
- package/static/assets/images/flags/unknown.svg +11 -0
- package/static/assets/images/flags/us.svg +63 -0
- package/static/assets/images/icon_audit.png +0 -0
- package/static/assets/images/icon_clinical.png +0 -0
- package/static/assets/images/icon_document.png +0 -0
- package/static/assets/images/icon_external.png +0 -0
- package/static/assets/images/icon_medication.png +0 -0
- package/static/assets/images/icon_organization.png +0 -0
- package/static/assets/images/icon_other.png +0 -0
- package/static/assets/images/icon_person.png +0 -0
- package/static/assets/images/icon_section.png +0 -0
- package/stats-db.js +45 -0
- package/stats.js +197 -10
- package/testing/testing-template.html +5 -8
- package/token/nginx_passport_setup.md +1 -1
- package/token/security_guide.md +1 -1
- package/token/token-template.html +2 -5
- package/token/token.js +40 -36
- package/translations/Messages.properties +19 -0
- package/tx/README.md +158 -4
- package/tx/closure/closure-store.js +126 -3
- package/tx/cm/cm-database.js +69 -57
- package/tx/cm/cm-package.js +50 -16
- package/tx/cs/cs-api.js +284 -19
- package/tx/cs/cs-api.md +41 -4
- package/tx/cs/cs-areacode.js +1 -1
- package/tx/cs/cs-country.js +17 -5
- package/tx/cs/cs-cpt.js +33 -16
- package/tx/cs/cs-cs.js +199 -24
- package/tx/cs/cs-currency.js +1 -1
- package/tx/cs/cs-icd11.js +21 -13
- package/tx/cs/cs-icf.js +22 -3
- package/tx/cs/cs-lang.js +2 -2
- package/tx/cs/cs-loinc.js +193 -67
- package/tx/cs/cs-mimetypes.js +12 -2
- package/tx/cs/cs-ndc.js +40 -25
- package/tx/cs/cs-omop.js +54 -29
- package/tx/cs/cs-rxnorm.js +300 -196
- package/tx/cs/cs-snomed.js +861 -95
- package/tx/cs/cs-ucum.js +14 -4
- package/tx/cs/cs-unii.js +18 -14
- package/tx/cs/cs-virtual.js +210 -0
- package/tx/ecosystem-resolver.js +413 -0
- package/tx/html/cluster-dash-metrics.liquid +26 -0
- package/tx/html/cluster-home-metrics.liquid +31 -0
- package/tx/html/cluster-metrics-script.liquid +251 -0
- package/tx/html/codesystem-operations.liquid +3 -3
- package/tx/html/ecl-panel.liquid +1 -1
- package/tx/html/external-metrics.liquid +17 -0
- package/tx/html/operations-form.liquid +17 -17
- package/tx/html/search-form.liquid +11 -11
- package/tx/html/status-line.liquid +58 -0
- package/tx/html/status-panel.liquid +140 -0
- package/tx/html/status-tasks.liquid +25 -0
- package/tx/html/tx-template.html +2 -5
- package/tx/importers/atc-to-fhir.js +17 -16
- package/tx/importers/export-icd11-codesystem.js +3 -2
- package/tx/importers/import-icd11.js +4 -3
- package/tx/importers/import-loinc.module.js +118 -56
- package/tx/importers/import-ndc.module.js +56 -44
- package/tx/importers/import-rxnorm.module.js +118 -79
- package/tx/importers/import-sct.module.js +125 -61
- package/tx/importers/import-unii.module.js +37 -36
- package/tx/importers/subset-loinc.module.js +49 -48
- package/tx/importers/subset-rxnorm.module.js +29 -28
- package/tx/importers/terminal.js +18 -0
- package/tx/importers/tx-import-base.js +13 -12
- package/tx/importers/tx-import-settings.js +5 -4
- package/tx/importers/tx-import.js +53 -52
- package/tx/library/canonical-resource.js +49 -0
- package/tx/library/codesystem.js +94 -5
- package/tx/library/designations.js +45 -2
- package/tx/library/extensions.js +13 -7
- package/tx/library/implicit-valuesets.js +66 -0
- package/tx/library/renderer.js +77 -21
- package/tx/library/sqlite-utilities.js +2 -0
- package/tx/library/tho-precedence.js +40 -0
- package/tx/library/ucum-parsers.js +11 -1
- package/tx/library/wait-for-cache.js +43 -0
- package/tx/library.js +321 -22
- package/tx/openapi-operations.js +6 -4
- package/tx/operation-context.js +460 -99
- package/tx/params.js +25 -0
- package/tx/problems.js +3 -5
- package/tx/provider.js +314 -67
- package/tx/resource-cache-sqlite.js +1050 -0
- package/tx/sct/documentation.md +45 -21
- package/tx/sct/ecl.js +81 -17
- package/tx/sct/editions.js +39 -0
- package/tx/sct/expressions.js +10 -1
- package/tx/sct/load-config.js +107 -0
- package/tx/sct/mmap.js +109 -0
- package/tx/sct/structures.js +175 -15
- package/tx/sct/text-index.js +5 -1
- package/tx/syndication/syndication-feed.js +2 -2
- package/tx/tx-html.js +44 -8
- package/tx/tx.fhir.org.yml +8 -8
- package/tx/tx.js +282 -80
- package/tx/usage-tracker.js +39 -11
- package/tx/vs/vs-api.js +19 -0
- package/tx/vs/vs-database.js +386 -62
- package/tx/vs/vs-package.js +44 -17
- package/tx/vs/vs-vsac.js +649 -101
- package/tx/workers/batch-validate.js +37 -10
- package/tx/workers/batch.js +6 -1
- package/tx/workers/cache-control.js +15 -4
- package/tx/workers/closure.js +156 -13
- package/tx/workers/compare.js +8 -5
- package/tx/workers/expand.js +1173 -289
- package/tx/workers/metadata.js +35 -0
- package/tx/workers/search.js +53 -1
- package/tx/workers/supplement-filters.js +145 -0
- package/tx/workers/translate.js +169 -4
- package/tx/workers/validate.js +223 -119
- package/tx/workers/worker.js +123 -31
- package/tx/xversion/xv-valueset.js +7 -4
- package/utilities/submit-test-report.js +3 -1
- package/utilities/tx-stress-test.js +482 -0
- package/vcl/readme.md +37 -0
- package/vcl/vcl-parser.js +15 -0
- package/xig/readme.md +52 -0
- package/xig/xig-template.html +5 -8
- package/xig/xig.js +238 -422
- package/changelog-template.md +0 -22
- package/registry/test-runner.js +0 -237
- package/release.sh +0 -80
- package/test-scripts/repro-re2-wasm-leak.js +0 -34
- package/tsconfig.eslint.json +0 -9
- package/tx/html/dash-metrics.liquid +0 -147
- package/tx/html/home-metrics.liquid +0 -225
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026, Health Intersections Pty Ltd (http://www.healthintersections.com.au)
|
|
3
|
+
//
|
|
4
|
+
// Licensed under BSD-3: https://opensource.org/license/bsd-3-clause
|
|
5
|
+
//
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The nationality step - the first processing step: which nation an IPS document is from,
|
|
9
|
+
* as far as the document itself says. Few national IGs give a way to tell, so this is a
|
|
10
|
+
* short list of signs, to grow as more are found:
|
|
11
|
+
*
|
|
12
|
+
* US the Bundle claims the US Patient Summary profile (Bundle-us-pcs), or a US Core
|
|
13
|
+
* extension is used anywhere in the document
|
|
14
|
+
* AU the Bundle claims the AU Patient Summary profile (au-ps-bundle)
|
|
15
|
+
*
|
|
16
|
+
* The result is { jurisdiction, reasons: [{ jurisdiction, text }] }: jurisdiction is the
|
|
17
|
+
* nation's ISO 3166 two-letter code, or null when it is unknown - nothing points to a nation,
|
|
18
|
+
* or the signs point to more than one (then the reasons say which).
|
|
19
|
+
*
|
|
20
|
+
* @module ips/nationality
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const US_CORE_EXTENSIONS = 'http://hl7.org/fhir/us/core/StructureDefinition/';
|
|
24
|
+
// how many extension urls a reason names
|
|
25
|
+
const MAX_NAMED = 3;
|
|
26
|
+
|
|
27
|
+
/** The profiles a resource claims (meta.profile), without their versions. */
|
|
28
|
+
function profilesOf(resource) {
|
|
29
|
+
const p = resource?.meta?.profile;
|
|
30
|
+
return Array.isArray(p) ? p.filter(x => typeof x === 'string').map(x => x.split('|')[0]) : [];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Every extension url in the document, at any depth, each once. */
|
|
34
|
+
function extensionUrls(node, found = new Set()) {
|
|
35
|
+
if (Array.isArray(node)) {
|
|
36
|
+
for (const n of node) {
|
|
37
|
+
extensionUrls(n, found);
|
|
38
|
+
}
|
|
39
|
+
} else if (node && typeof node === 'object') {
|
|
40
|
+
for (const [k, v] of Object.entries(node)) {
|
|
41
|
+
if ((k === 'extension' || k === 'modifierExtension') && Array.isArray(v)) {
|
|
42
|
+
for (const e of v) {
|
|
43
|
+
if (e && typeof e.url === 'string') {
|
|
44
|
+
found.add(e.url);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
extensionUrls(v, found);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return found;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** A Bundle profile that says which nation a document is from. */
|
|
55
|
+
function profileRule(jurisdiction, url, name) {
|
|
56
|
+
return (bundle) => (profilesOf(bundle).includes(url) ? `the Bundle claims the ${name} profile (${url})` : null);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const RULES = [
|
|
60
|
+
{ jurisdiction: 'US', test: profileRule('US', 'http://hl7.org/fhir/us/pcs/StructureDefinition/Bundle-us-pcs', 'US Patient Summary') },
|
|
61
|
+
{
|
|
62
|
+
jurisdiction: 'US',
|
|
63
|
+
test: (bundle, urls) => {
|
|
64
|
+
const core = [...urls].filter(u => u.startsWith(US_CORE_EXTENSIONS)).sort();
|
|
65
|
+
if (core.length === 0) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
const named = core.slice(0, MAX_NAMED).map(u => u.substring(US_CORE_EXTENSIONS.length)).join(', ');
|
|
69
|
+
return `US Core extension${core.length === 1 ? ' is' : 's are'} used (${named}${core.length > MAX_NAMED ? ` and ${core.length - MAX_NAMED} more` : ''})`;
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
{ jurisdiction: 'AU', test: profileRule('AU', 'http://hl7.org.au/fhir/ps/StructureDefinition/au-ps-bundle', 'AU Patient Summary') }
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Which nation a document is from.
|
|
77
|
+
* @param {Object} bundle
|
|
78
|
+
* @returns {{jurisdiction: string|null, reasons: Array<{jurisdiction: string, text: string}>}}
|
|
79
|
+
*/
|
|
80
|
+
function determineNationality(bundle) {
|
|
81
|
+
const urls = extensionUrls(bundle);
|
|
82
|
+
const reasons = [];
|
|
83
|
+
for (const rule of RULES) {
|
|
84
|
+
const text = rule.test(bundle, urls);
|
|
85
|
+
if (text) {
|
|
86
|
+
reasons.push({ jurisdiction: rule.jurisdiction, text });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const nations = [...new Set(reasons.map(r => r.jurisdiction))];
|
|
90
|
+
return { jurisdiction: nations.length === 1 ? nations[0] : null, reasons };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** What the result says, in a sentence. */
|
|
94
|
+
function nationalityText(result) {
|
|
95
|
+
if (!result) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
const reasons = Array.isArray(result.reasons) ? result.reasons : [];
|
|
99
|
+
if (result.jurisdiction) {
|
|
100
|
+
return `From ${result.jurisdiction}: ${reasons.map(r => r.text).join('; ')}`;
|
|
101
|
+
}
|
|
102
|
+
if (reasons.length > 0) {
|
|
103
|
+
const nations = [...new Set(reasons.map(r => r.jurisdiction))];
|
|
104
|
+
return `Nationality unknown: the document has signs of more than one nation (${nations.map(n => `${n}: ${reasons.filter(r => r.jurisdiction === n).map(r => r.text).join('; ')}`).join(' - ')})`;
|
|
105
|
+
}
|
|
106
|
+
return 'Nationality unknown: nothing in the document says which nation it is from (only US and Australian documents can be recognised so far)';
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const nationalityStep = {
|
|
110
|
+
name: 'nationality',
|
|
111
|
+
async run(row, project, module) {
|
|
112
|
+
const started = Date.now();
|
|
113
|
+
const result = determineNationality(row.bundle);
|
|
114
|
+
module.store.saveResult(row.id, 'nationality', { ms: Date.now() - started, ...result });
|
|
115
|
+
module.store.setNationality(row.id, result.jurisdiction);
|
|
116
|
+
return `nationality: ${result.jurisdiction || 'unknown'}`;
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
module.exports = { determineNationality, nationalityText, nationalityStep, RULES };
|
package/ips/pipeline.js
CHANGED
|
@@ -9,13 +9,16 @@
|
|
|
9
9
|
* One process works through the queue (ips.js: processQueue), and runs these steps over
|
|
10
10
|
* each document, in this order:
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
12
|
+
* 0. nationality which nation the document is from, as far as it says (nationality.js)
|
|
13
|
+
* 1. analysis look at each resource and section for what a reader should know (analysis.js)
|
|
14
|
+
* 2. translate translate CodeableConcepts that fail the terminology check to codes that pass it
|
|
15
|
+
* 3. narrative generate the narrative with the IPS presenter (presenter.js), into the analysed copy
|
|
16
|
+
* 4. validation validate the (translated) document with the FHIR validator (fhir-validator-wrapper)
|
|
17
|
+
* 5. national validate it against each national profile (the national IGs' profiles on Bundle)
|
|
18
|
+
* 6. terminology check each CodeableConcept against the value sets for international exchange
|
|
19
|
+
* 7. piqi measure the quality of the document with PIQI (not yet)
|
|
20
|
+
* 8. profiles check the document against each clinical profile (profiles.js)
|
|
21
|
+
* 9. ai assess the document for consistency and its terminology with an AI service (ai-client.js)
|
|
19
22
|
*
|
|
20
23
|
* Each step is { name, run(row, project, module) -> result }. A step keeps what it finds in
|
|
21
24
|
* one of two places: as a result stored against the document (store.saveResult - the
|
|
@@ -34,6 +37,8 @@ const { Presenter } = require('./presenter');
|
|
|
34
37
|
const ai = require('./ai-client');
|
|
35
38
|
const { evaluateAsync } = require('./fhirpath-ips');
|
|
36
39
|
const { checkNarrative } = require('../library/narrative-check');
|
|
40
|
+
const { analysisStep } = require('./analysis');
|
|
41
|
+
const { nationalityStep } = require('./nationality');
|
|
37
42
|
|
|
38
43
|
const IPS_BUNDLE_PROFILE = 'http://hl7.org/fhir/uv/ips/StructureDefinition/Bundle-uv-ips';
|
|
39
44
|
const VALIDATE_TIMEOUT_MS = 120000;
|
|
@@ -99,14 +104,13 @@ const validation = {
|
|
|
99
104
|
}
|
|
100
105
|
const cfg = module.config.validator || {};
|
|
101
106
|
const profiles = Array.isArray(cfg.profiles) ? cfg.profiles : [IPS_BUNDLE_PROFILE];
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
const checkIpsCodes = cfg.checkIpsCodes !== false;
|
|
107
|
+
// no checkIpsCodes: validator 7.0.4 dropped its IPS code checks, and refuses the parameter
|
|
108
|
+
// ("Unable to process param checkIpsCodes"), failing every validation
|
|
105
109
|
const started = Date.now();
|
|
106
110
|
// fhir-validator-wrapper 1.5.0 or later: earlier versions sent the profiles in a form the
|
|
107
111
|
// validator service refuses
|
|
108
112
|
// the analysed copy: the translations are in it, and the validator should see them
|
|
109
|
-
const outcome = await v.validate(JSON.stringify(row.analysed || row.bundle), { profiles,
|
|
113
|
+
const outcome = await v.validate(JSON.stringify(row.analysed || row.bundle), { profiles, timeout: VALIDATE_TIMEOUT_MS });
|
|
110
114
|
if (!outcome || outcome.resourceType !== 'OperationOutcome') {
|
|
111
115
|
throw new Error('The validator did not return an OperationOutcome');
|
|
112
116
|
}
|
|
@@ -122,6 +126,64 @@ const validation = {
|
|
|
122
126
|
}
|
|
123
127
|
};
|
|
124
128
|
|
|
129
|
+
/**
|
|
130
|
+
* National profiles: the document is validated against each national profile - the
|
|
131
|
+
* national IG's package is loaded into the validator first (once), then the document is
|
|
132
|
+
* validated against the IG's profile on Bundle, as the validation step validates it. For
|
|
133
|
+
* each profile, the result keeps the OperationOutcome and whether the document passed: no
|
|
134
|
+
* errors (or fatal issues). A profile whose package can't be loaded, or that the validator
|
|
135
|
+
* can't validate against, is kept with the reason (pass is then null), and doesn't stop the
|
|
136
|
+
* pipeline.
|
|
137
|
+
*/
|
|
138
|
+
const national = {
|
|
139
|
+
name: 'national',
|
|
140
|
+
async run(row, project, module) {
|
|
141
|
+
const list = module.store.nationalProfiles();
|
|
142
|
+
const started = Date.now();
|
|
143
|
+
if (list.length === 0) {
|
|
144
|
+
module.store.saveResult(row.id, 'national', { ms: 0, profiles: [] });
|
|
145
|
+
return 'national profiles: none';
|
|
146
|
+
}
|
|
147
|
+
const v = module.validator;
|
|
148
|
+
if (!v) {
|
|
149
|
+
throw new Error('The validator is not configured (modules.ips.validator)');
|
|
150
|
+
}
|
|
151
|
+
if (!v.isRunning()) {
|
|
152
|
+
throw new Error('The validator is not running');
|
|
153
|
+
}
|
|
154
|
+
const doc = JSON.stringify(row.analysed || row.bundle);
|
|
155
|
+
const profiles = [];
|
|
156
|
+
for (const np of list) {
|
|
157
|
+
const entry = { id: np.id, jurisdiction: np.jurisdiction, packageId: np.packageId, version: np.version, url: np.url };
|
|
158
|
+
const t0 = Date.now();
|
|
159
|
+
try {
|
|
160
|
+
await module.loadNationalPackage(np.packageId, np.version);
|
|
161
|
+
const outcome = await v.validate(doc, { profiles: [np.url], timeout: VALIDATE_TIMEOUT_MS });
|
|
162
|
+
if (!outcome || outcome.resourceType !== 'OperationOutcome') {
|
|
163
|
+
throw new Error('The validator did not return an OperationOutcome');
|
|
164
|
+
}
|
|
165
|
+
entry.counts = issueCounts(outcome);
|
|
166
|
+
entry.pass = !entry.counts.error && !entry.counts.fatal;
|
|
167
|
+
entry.outcome = outcome;
|
|
168
|
+
} catch (e) {
|
|
169
|
+
entry.pass = null;
|
|
170
|
+
entry.error = e.message;
|
|
171
|
+
}
|
|
172
|
+
entry.ms = Date.now() - t0;
|
|
173
|
+
profiles.push(entry);
|
|
174
|
+
}
|
|
175
|
+
module.store.saveResult(row.id, 'national', {
|
|
176
|
+
ms: Date.now() - started,
|
|
177
|
+
validatorVersion: typeof v.jarVersion === 'function' ? v.jarVersion() : null,
|
|
178
|
+
profiles
|
|
179
|
+
});
|
|
180
|
+
const passed = profiles.filter(p => p.pass === true).length;
|
|
181
|
+
const failed = profiles.filter(p => p.pass === false).length;
|
|
182
|
+
const unchecked = profiles.length - passed - failed;
|
|
183
|
+
return `national profiles: ${passed} passed, ${failed} failed` + (unchecked ? `, ${unchecked} not checked` : '');
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
|
|
125
187
|
// ---- the terminology check ------------------------------------------------------------
|
|
126
188
|
|
|
127
189
|
/**
|
|
@@ -485,6 +547,56 @@ function translationOf(coding) {
|
|
|
485
547
|
};
|
|
486
548
|
}
|
|
487
549
|
|
|
550
|
+
const SNOMED_CT = 'http://snomed.info/sct';
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* The SNOMED CT edition a document's codes are taken to be from when they don't say: the
|
|
554
|
+
* edition of a national profile for the nation the document is from (the nationality step).
|
|
555
|
+
* Only the translate step uses it - the terminology check takes the codes as they are, so a
|
|
556
|
+
* code that is fine as it is passes whatever its edition, and one whose edition says otherwise
|
|
557
|
+
* doesn't.
|
|
558
|
+
* @returns {{edition: string, packageId: string, version: string, jurisdiction: string}|null}
|
|
559
|
+
*/
|
|
560
|
+
function defaultSnomedEdition(module, row) {
|
|
561
|
+
const jurisdiction = module.store.readResult(row.id, 'nationality')?.result?.jurisdiction;
|
|
562
|
+
if (!jurisdiction) {
|
|
563
|
+
return null;
|
|
564
|
+
}
|
|
565
|
+
const np = module.store.nationalProfiles().find(p => p.jurisdiction === jurisdiction && p.snomedEdition);
|
|
566
|
+
return np ? { edition: np.snomedEdition, packageId: np.packageId, version: np.version, jurisdiction } : null;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/** A concept with its SNOMED CT codings that have no version given the edition (a copy; the concept is unchanged). */
|
|
570
|
+
function withSnomedEdition(concept, edition) {
|
|
571
|
+
if (!edition || !Array.isArray(concept.coding)) {
|
|
572
|
+
return concept;
|
|
573
|
+
}
|
|
574
|
+
return { ...concept, coding: concept.coding.map(c => (c && c.system === SNOMED_CT && !c.version ? { ...c, version: `${SNOMED_CT}/${edition}` } : c)) };
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/** The SNOMED CT edition a version names (its module id, without the release date), or the version itself. */
|
|
578
|
+
function editionOf(system, version) {
|
|
579
|
+
if (system === SNOMED_CT && typeof version === 'string') {
|
|
580
|
+
const m = version.match(/^http:\/\/snomed\.info\/x?sct\/(\d+)(?:\/version\/\d+)?$/);
|
|
581
|
+
if (m) {
|
|
582
|
+
return m[1];
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
return version;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Whether a concept already has a coding: the same system and code, and - when both say - the
|
|
590
|
+
* same version. A version that differs makes it a different coding: the same SNOMED CT code in
|
|
591
|
+
* the International Edition is what a translation from a national edition is for, even though
|
|
592
|
+
* the code is the same (SNOMED CT versions are compared by edition, not release). A coding with
|
|
593
|
+
* no version already stands for the code, whatever the translation's version.
|
|
594
|
+
*/
|
|
595
|
+
function hasCoding(codings, coding) {
|
|
596
|
+
return codings.some(x => x && x.system === coding.system && x.code === coding.code &&
|
|
597
|
+
(!x.version || !coding.version || editionOf(x.system, x.version) === editionOf(coding.system, coding.version)));
|
|
598
|
+
}
|
|
599
|
+
|
|
488
600
|
/** A CodeableConcept as the server should see it: without our extensions. */
|
|
489
601
|
function withoutOurExtensions(concept) {
|
|
490
602
|
const c = structuredClone(concept);
|
|
@@ -525,6 +637,8 @@ const translate = {
|
|
|
525
637
|
const budget = { left: MAX_CHECKS_PER_DOCUMENT };
|
|
526
638
|
const cache = module.txCache || (module.txCache = new Map());
|
|
527
639
|
const started = Date.now();
|
|
640
|
+
// SNOMED CT codes with no version are taken to be from the edition of the document's nation
|
|
641
|
+
const defaultEdition = defaultSnomedEdition(module, row);
|
|
528
642
|
// what happened to each concept, for the translation view
|
|
529
643
|
const entries = [];
|
|
530
644
|
// the same resource in two sections, and the same concept in two places: once
|
|
@@ -558,11 +672,11 @@ const translate = {
|
|
|
558
672
|
} else if (budget.left <= 0) {
|
|
559
673
|
translation = { matches: [], failed: true, check, message: `not translated - the document has more codes than are checked (${MAX_CHECKS_PER_DOCUMENT})` };
|
|
560
674
|
} else {
|
|
561
|
-
const cacheKey = `translate|${key}`;
|
|
675
|
+
const cacheKey = `translate|${defaultEdition ? defaultEdition.edition : ''}|${key}`;
|
|
562
676
|
translation = cache.get(cacheKey);
|
|
563
677
|
if (!translation) {
|
|
564
678
|
budget.left--;
|
|
565
|
-
translation = await translateConcept(module.tx, valueSets, withoutOurExtensions(c.concept), target);
|
|
679
|
+
translation = await translateConcept(module.tx, valueSets, withSnomedEdition(withoutOurExtensions(c.concept), defaultEdition?.edition), target);
|
|
566
680
|
if (!translation.failed) {
|
|
567
681
|
if (cache.size >= MAX_CACHED_ANSWERS) {
|
|
568
682
|
cache.delete(cache.keys().next().value);
|
|
@@ -592,7 +706,7 @@ const translate = {
|
|
|
592
706
|
}
|
|
593
707
|
let added = 0;
|
|
594
708
|
for (const m of translation.matches) {
|
|
595
|
-
if (codings
|
|
709
|
+
if (hasCoding(codings, m.coding)) {
|
|
596
710
|
entry.added.push({ ...m, present: true });
|
|
597
711
|
continue; // already there
|
|
598
712
|
}
|
|
@@ -617,7 +731,7 @@ const translate = {
|
|
|
617
731
|
entry.message = translation.matches.length > 0 ? 'The translation gave only codings the concept already has' : (translation.message || 'The terminology server has no translation');
|
|
618
732
|
}
|
|
619
733
|
}
|
|
620
|
-
module.store.saveResult(row.id, 'translate', { ms: Date.now() - started, server: module.tx.url, counts, entries });
|
|
734
|
+
module.store.saveResult(row.id, 'translate', { ms: Date.now() - started, server: module.tx.url, counts, entries, ...(defaultEdition ? { defaultEdition } : {}) });
|
|
621
735
|
return `translate: ${counts.translated} of ${counts.tried} concepts translated (${counts.codings} codings added)` +
|
|
622
736
|
(counts.failed ? `, ${counts.failed} not translated (see the log)` : '');
|
|
623
737
|
}
|
|
@@ -1003,10 +1117,10 @@ const aiStep = {
|
|
|
1003
1117
|
};
|
|
1004
1118
|
|
|
1005
1119
|
/** The steps, in order. Only the ones that exist yet. */
|
|
1006
|
-
const STEPS = [translate, narrative, validation, terminology, profilesStep, aiStep];
|
|
1120
|
+
const STEPS = [nationalityStep, analysisStep, translate, narrative, validation, national, terminology, profilesStep, aiStep];
|
|
1007
1121
|
|
|
1008
1122
|
module.exports = {
|
|
1009
|
-
STEPS, translate, narrative, validation, terminology, profiles: profilesStep, ai: aiStep, checkConstraint, valueSetsIn, lookupDisplay, checkConcept, documentForAi, assessmentOf, aiSystemPrompt, AI_SYSTEM_PROMPT, checkOf, addCheck, stripChecks, translateConcept, translationOf,
|
|
1123
|
+
STEPS, analysis: analysisStep, translate, narrative, validation, national, terminology, profiles: profilesStep, ai: aiStep, checkConstraint, valueSetsIn, lookupDisplay, checkConcept, documentForAi, assessmentOf, aiSystemPrompt, AI_SYSTEM_PROMPT, checkOf, addCheck, stripChecks, translateConcept, translationOf, defaultSnomedEdition, withSnomedEdition, hasCoding,
|
|
1010
1124
|
validateCodeInValueSet, validateCodeInSystem, issueCounts, issueSummary,
|
|
1011
1125
|
IPS_BUNDLE_PROFILE, CHECK_EXTENSION, TRANSLATION_EXTENSION, MAX_CHECKS_PER_DOCUMENT, MAX_CACHED_ANSWERS
|
|
1012
1126
|
};
|
package/ips/presenter.js
CHANGED
|
@@ -969,6 +969,6 @@ const GENERIC = {
|
|
|
969
969
|
};
|
|
970
970
|
|
|
971
971
|
module.exports = {
|
|
972
|
-
Presenter, COLUMNS, GENERIC, describeResource, NONE,
|
|
972
|
+
Presenter, COLUMNS, GENERIC, describeResource, codeText, NONE,
|
|
973
973
|
dateText, periodText, quantityText, rangeText, humanNameText, addressText, identifierText, contactPointText, annotationText
|
|
974
974
|
};
|