fhirsmith 0.17.0 → 0.18.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.
Files changed (131) hide show
  1. package/CHANGELOG.md +113 -0
  2. package/README.md +80 -13
  3. package/config-template.json +106 -1
  4. package/ips/ai-client.js +263 -0
  5. package/ips/composer.js +805 -0
  6. package/ips/editor-schema.js +289 -0
  7. package/ips/editor.js +290 -0
  8. package/ips/fhirpath-ips.js +397 -0
  9. package/ips/ips-codeable-concepts.md +1742 -0
  10. package/ips/ips-template.html +125 -0
  11. package/ips/ips-valuesets.js +159 -0
  12. package/ips/ips.js +1736 -0
  13. package/ips/membership.js +179 -0
  14. package/ips/pipeline.js +1012 -0
  15. package/ips/presenter.js +974 -0
  16. package/ips/profile-editor.js +578 -0
  17. package/ips/profiles.js +506 -0
  18. package/ips/readme.md +460 -0
  19. package/ips/render.js +1482 -0
  20. package/ips/search-params.js +83 -0
  21. package/ips/search.js +667 -0
  22. package/ips/share.js +178 -0
  23. package/ips/store.js +650 -0
  24. package/ips/terminology.js +395 -0
  25. package/ips/tx-client.js +202 -0
  26. package/ips/valuesets/CodeSystem-absent-unknown-uv-ips.json +206 -0
  27. package/ips/valuesets/ValueSet-absent-or-unknown-immunizations-uv-ips.json +59 -0
  28. package/ips/valuesets/ValueSet-ips-allergy-code.json +59 -0
  29. package/ips/valuesets/ValueSet-ips-allergy-manifestation.json +356 -0
  30. package/ips/valuesets/ValueSet-ips-allergy-substance.json +37 -0
  31. package/ips/valuesets/ValueSet-ips-body-site.json +26 -0
  32. package/ips/valuesets/ValueSet-ips-clinical-finding-or-none.json +35 -0
  33. package/ips/valuesets/ValueSet-ips-clinical-finding.json +26 -0
  34. package/ips/valuesets/ValueSet-ips-condition-code.json +72 -0
  35. package/ips/valuesets/ValueSet-ips-condition-severity.json +30 -0
  36. package/ips/valuesets/ValueSet-ips-condition-stage.json +26 -0
  37. package/ips/valuesets/ValueSet-ips-exposure-route.json +26 -0
  38. package/ips/valuesets/ValueSet-ips-imaging-procedure.json +25 -0
  39. package/ips/valuesets/ValueSet-ips-immunization-status-reason.json +43 -0
  40. package/ips/valuesets/ValueSet-ips-international-codes.json +25 -0
  41. package/ips/valuesets/ValueSet-ips-medication-ingredient.json +26 -0
  42. package/ips/valuesets/ValueSet-ips-medication-status-reason.json +37 -0
  43. package/ips/valuesets/ValueSet-ips-medication.json +50 -0
  44. package/ips/valuesets/ValueSet-ips-observation-code.json +18 -0
  45. package/ips/valuesets/ValueSet-ips-observation-interpretation.json +18 -0
  46. package/ips/valuesets/ValueSet-ips-procedure-followup.json +51 -0
  47. package/ips/valuesets/ValueSet-ips-procedure-outcome.json +30 -0
  48. package/ips/valuesets/ValueSet-ips-procedure-reason.json +37 -0
  49. package/ips/valuesets/ValueSet-ips-procedure.json +116 -0
  50. package/ips/valuesets/ValueSet-ips-route.json +46 -0
  51. package/ips/valuesets/ValueSet-ips-target-disease.json +171 -0
  52. package/ips/valuesets/ValueSet-ips-vaccine.json +31 -0
  53. package/ips/valuesets/elements.json +44 -0
  54. package/library/daily-report.js +432 -0
  55. package/library/fhir-search-values.js +138 -0
  56. package/library/html-server.js +32 -23
  57. package/library/mailer.js +77 -0
  58. package/library/narrative-check.js +21 -2
  59. package/library/npm-audit.js +25 -0
  60. package/library/package-manager.js +4 -2
  61. package/library/sqlite-session-store.js +29 -1
  62. package/package.json +8 -3
  63. package/packages/packages.js +34 -3
  64. package/publisher/publisher.js +29 -0
  65. package/registry/auth-store.js +366 -0
  66. package/registry/auth.js +872 -0
  67. package/registry/crawler.js +41 -1
  68. package/registry/documentation/api.md +162 -0
  69. package/registry/documentation/configuration.md +93 -0
  70. package/registry/documentation/documents.md +74 -0
  71. package/registry/documentation/internals.md +151 -0
  72. package/registry/documentation/readme.md +100 -0
  73. package/registry/documentation/security.md +199 -0
  74. package/registry/documents.js +309 -0
  75. package/registry/openapi.yaml +30 -0
  76. package/registry/registry-template.html +1 -1
  77. package/registry/registry.js +230 -44
  78. package/registry/uptime.js +154 -0
  79. package/server.js +105 -44
  80. package/shl/shl.js +64 -9
  81. package/stats-db.js +93 -0
  82. package/stats.js +158 -1
  83. package/testing/search.js +2 -108
  84. package/testing/store.js +5 -0
  85. package/testing/testing.js +20 -0
  86. package/token/token-template.html +5 -4
  87. package/token/token.js +17 -10
  88. package/translations/Messages.properties +10 -0
  89. package/tx/README.md +52 -0
  90. package/tx/closure/closure-store.js +14 -0
  91. package/tx/cs/cs-api.js +92 -14
  92. package/tx/cs/cs-cs.js +64 -3
  93. package/tx/cs/cs-icd11.js +366 -112
  94. package/tx/cs/cs-icf.js +431 -0
  95. package/tx/cs/cs-loinc.js +296 -27
  96. package/tx/cs/cs-omop.js +10 -3
  97. package/tx/cs/cs-snomed.js +1210 -80
  98. package/tx/cs/cs-ucum.js +6 -0
  99. package/tx/cs/icd11-expressions.js +191 -78
  100. package/tx/importers/csv-records.js +35 -0
  101. package/tx/importers/export-icd11-codesystem.js +58 -12
  102. package/tx/importers/icd11-schema.md +9 -5
  103. package/tx/importers/import-loinc.module.js +177 -41
  104. package/tx/importers/import-sct.module.js +66 -1
  105. package/tx/importers/subset-loinc.module.js +25 -6
  106. package/tx/library/codesystem.js +4 -1
  107. package/tx/library/designations.js +23 -30
  108. package/tx/library.js +98 -4
  109. package/tx/openapi-operations.js +6 -1
  110. package/tx/params.js +8 -1
  111. package/tx/provider.js +24 -6
  112. package/tx/sct/documentation.md +251 -0
  113. package/tx/sct/ecl.js +758 -46
  114. package/tx/sct/expressions.js +113 -52
  115. package/tx/sct/structures.js +8 -2
  116. package/tx/syndication/syndication-feed.js +218 -0
  117. package/tx/syndication/syndication-store.js +415 -0
  118. package/tx/tx-html.js +15 -1
  119. package/tx/tx.fhir.org.yml +3 -4
  120. package/tx/tx.js +144 -2
  121. package/tx/vs/vs-vsac.js +25 -6
  122. package/tx/workers/closure.js +20 -0
  123. package/tx/workers/expand.js +35 -8
  124. package/tx/workers/lookup.js +61 -6
  125. package/tx/workers/subsumes.js +1 -1
  126. package/tx/workers/translate.js +166 -15
  127. package/tx/workers/validate.js +138 -6
  128. package/utilities/make-sct-loinc-subset.js +250 -0
  129. package/utilities/make-sct-test-extension.js +264 -0
  130. package/xig/xig.js +32 -0
  131. package/registry/readme.md +0 -124
package/ips/readme.md ADDED
@@ -0,0 +1,460 @@
1
+ # IPS module
2
+
3
+ Collections of International Patient Summary documents, called projects, each with its own
4
+ FHIR endpoint and web pages. It runs at `/ips` when `modules.ips.enabled` is true.
5
+
6
+ ## Configuration
7
+
8
+ ```json
9
+ "ips": {
10
+ "enabled": true,
11
+ "database": "ips/ips-content.db",
12
+ "adminUsername": "admin",
13
+ "adminPassword": "<secret>",
14
+ "sessionSecret": "<random string>",
15
+ "cookieSecure": true,
16
+ "loginAttemptsPerWindow": 20,
17
+ "maxSize": 10485760,
18
+ "rateLimit": { "windowMinutes": 1, "max": 60 },
19
+ "processing": { "enabled": true, "intervalSeconds": 5 },
20
+ "txServer": "/tx/r4"
21
+ }
22
+ ```
23
+
24
+ Everything is in one SQLite database; a relative path resolves under the data folder's
25
+ `databases` directory. The administrator's login is `adminUsername` (default `admin`) with
26
+ `adminPassword` - without a password there is no administrator, so nothing can be created.
27
+ `sessionSecret` signs the login cookie; `cookieSecure: false` is for local testing over
28
+ plain http (see the publisher's notes on nginx and `X-Forwarded-Proto`). `maxSize` is the
29
+ biggest document accepted, in bytes; `rateLimit` is submissions per IP address (`max: 0`
30
+ turns it off). `txServer` is the terminology server the module uses - for links from the
31
+ terminology view to code systems, and for the analysis of documents: either an http(s) URL
32
+ of an external server, or a path on this server (`/tx/r4`) when the tx module is enabled,
33
+ in which case requests are dispatched to it in-process (`ips/tx-client.js`), with no
34
+ network. Without it there are no links and no analysis.
35
+
36
+ ## Projects, the administrator, and managers
37
+
38
+ `/ips` lists the projects. The administrator logs in at `/ips/login` and, at
39
+ `/ips/admin/projects` and `/ips/admin/users`, creates and deletes projects and users, and
40
+ says which users manage which projects. A project has a code - 2-32 lower case letters,
41
+ digits and hyphens, fixed once it exists - and lives at `/ips/{code}`. Deleting a project
42
+ deletes its documents.
43
+
44
+ A project's managers (and the administrator) can:
45
+
46
+ - set the project's submission token at `/ips/{code}/settings`. When one is set, a document
47
+ can only be submitted with it: in the `token` field of the submission form, or as
48
+ `Authorization: Bearer <token>` on a `POST`. Managers can read the token back, so it is
49
+ stored as it is, not hashed
50
+ - delete documents, and put a document back in the processing queue
51
+
52
+ Logins work as the publisher's and the testing module's do: a session cookie
53
+ (`ips.sid`, SameSite=Lax, HTTPS-only unless `cookieSecure` is false), bcrypt password
54
+ hashes, a rate limited login, an Origin check and a CSRF token on every form post - except
55
+ the submission form, which anyone may post (with the project's token, if it has one), so it
56
+ carries no CSRF token and starts no session; the Origin check still applies.
57
+
58
+ ## Limits
59
+
60
+ A document is at most `maxSize` bytes. The module reads request bodies itself: the server's body parsers leave `/ips` alone, the submission token is checked before a document is read, and a document is read no bigger than `maxSize`. When it is indexed, at most 200 values are kept per
61
+ search parameter; a search takes at most 5 sort keys, and a page of the FHIR search carries at
62
+ most 32MB of documents (the `next` link follows on from where the page stopped). The
63
+ terminology check asks the terminology server at most 2000 questions per document (the rest
64
+ are left unknown) and remembers 20000 answers. Narratives are shown as parsed, never as the
65
+ string submitted. Error responses carry no internal detail; the reason a document's
66
+ processing failed is shown only to the project's managers.
67
+
68
+ ## Documents
69
+
70
+ A document is an R4 `Bundle` of type `document` whose first entry is a `Composition` whose
71
+ subject is a `Patient` in the bundle. Nothing else about its content is checked (that is for
72
+ processing, later), except that no narrative in it - any resource's `text.div`, or a
73
+ section's - may have active content (script, event handlers, `javascript:` URLs, frames,
74
+ forms and the like; see `library/narrative-check.js`), because the web site shows the
75
+ narratives as they are.
76
+
77
+ POST /ips/{code}/Bundle
78
+ Content-Type: application/fhir+json
79
+ Authorization: Bearer <token> (only if the project has one)
80
+
81
+ or paste the JSON (or pick a file) at `/ips/{code}/submit`. The server gives the document its
82
+ id - a serial number, one series across all projects - sets `meta.lastUpdated` to the time it
83
+ was received, and drops `meta.versionId`: documents can't be changed once received.
84
+ Responses follow the spec: `201 Created` with a `Location` header, and the stored document,
85
+ or nothing (`Prefer: return=minimal`), or an OperationOutcome (`Prefer: return=OperationOutcome`).
86
+ Errors are OperationOutcomes: 400, 401 (token), 413 (too big), 415 (not JSON), 429 (rate
87
+ limit). The submitter's IP address is recorded, for tracing abuse, and never returned.
88
+
89
+ GET /ips/{code}/Bundle/{id} the document (JSON), or a view of it for a browser
90
+ GET /ips/{code}/Bundle?{params} search
91
+ GET /ips/{code}/metadata CapabilityStatement
92
+
93
+ A browser (or `_format=html`) gets pages: `/ips/{code}` is the project's page - its name
94
+ and description in a banner, "Submit a document" (and, for managers, "Settings") buttons,
95
+ the filterable list (patient name, gender, document date, document id or patient identifier,
96
+ processing status; the FHIR parameters below work there too), and at the foot a
97
+ documentation section with the endpoint's URL, the CapabilityStatement link and a table of
98
+ the requests it answers - and `/ips/{code}/Bundle/{id}` shows the document.
99
+ The document page's header names the patient, the document's date, when and how it was
100
+ received and its size, with links to the FHIR JSON and to download it (`?download=1`, sent
101
+ as an attachment named `ips-{code}-{id}.json`). Below it, each processing step is shown
102
+ with where it is (tick: done, cross: failed, gear: running, moon: waiting - the step's note,
103
+ or for managers the reason it failed, on hover), and managers get "Process again" and
104
+ "Delete" buttons. The views of a document are tabs, chosen with `?view=`: `narrative` (the
105
+ default - the Composition's narrative followed by each section's, with the section titles),
106
+ `translation` (what the translate step did with each concept, below), `generated` (the
107
+ narrative this server generates, below), `validation` (below), `terminology`, `ai` (the AI assessment, below) and `json`
108
+ (pretty-printed, monospaced).
109
+ `terminology` lists every CodeableConcept in the document (`ips/terminology.js`): the
110
+ Composition's own, then each section's entries in the IPS section order (Patient Story,
111
+ Problems, Allergies, Medications, ... Results; sections not in that list after, in document
112
+ order), found by walking each resource against the R4 type model. Section codes, every
113
+ `category`, `Composition.type`, `Composition.event.code`, and the clinical and verification
114
+ status of Conditions and AllergyIntolerances are left out (`IGNORED` in terminology.js). Codings are shown as the Java
115
+ DataRenderer shows them: the code system by name with the URL on hover, the version
116
+ described after it in brackets - for SNOMED CT the edition's short code and release
117
+ ("US rel. Mar 2024", from `tx/sct/editions.js`, with the edition's full name on hover) - then
118
+ the code and display. The external terminologies (SNOMED CT, LOINC, RxNorm, NCI Thesaurus,
119
+ ISO 3166) link name and code to their own browsers (SNOMED CT to the edition's URI,
120
+ `http://snomed.info/sct/{edition}/id/{code}`, when the version names a known edition); any
121
+ other code system links to its page on the terminology server. More
122
+ views will come.
123
+
124
+ ### Searching
125
+
126
+ A search is over Bundle; the Composition's and Patient's parameters are reached by chaining
127
+ through `composition` and then `patient` (or `subject`). The standard R4 parameters are
128
+ indexed (`ips/search-params.js` lists them, with their FHIRPath expressions):
129
+
130
+ _id, _lastUpdated the server's id, and when it received the document
131
+ processing-status submitted | processing | processed | error (this module's own)
132
+ identifier, type, timestamp, composition Bundle
133
+ composition.date, composition.type, composition.title, composition.status, composition.author, ...
134
+ composition.patient.name, composition.patient.birthdate, composition.patient.gender,
135
+ composition.patient.identifier, composition.patient.address-city, ...
136
+
137
+ Strings match from the start, case-insensitively, with `:exact` and `:contains`; tokens are
138
+ `code` or `system|code` (`|code` for no system, `system|` for any code in the system), with
139
+ `:not`; dates take the usual prefixes (`ge2026`, `lt2026-03-01`) and compare as ranges;
140
+ references match as written, or by `Type/id` or `id` at the end of what was written;
141
+ `:missing` works everywhere. Comma separated values are ORed, repeated parameters ANDed.
142
+ `_sort` takes any of these (`-` for descending), `_count`/`_offset` page, `_summary=count`
143
+ gives the total alone. Unknown parameters are ignored, with a warning OperationOutcome in the
144
+ result - unless `Prefer: handling=strict`, when they are a 400.
145
+
146
+ Each document is indexed once, when it arrives: every parameter's expression is evaluated
147
+ with fhirpath.js against the Bundle, the Composition and the Patient, and the values go into
148
+ one index table (`ips_index`). Adding a parameter to `search-params.js` indexes new documents
149
+ only; the index for existing ones would need rebuilding.
150
+
151
+ ## Processing
152
+
153
+ A received document's status is `submitted`. A background loop (every `intervalSeconds`)
154
+ takes submitted documents in turn, marks each `processing`, runs the pipeline's steps over
155
+ it (`ips/pipeline.js`), and marks it `processed` with the steps' notes, or `error` with the
156
+ message of the step that failed. Each step's own state (`waiting`, `running`, `done` or
157
+ `failed`, with its note or error) is kept in its result row as it goes, which is what the
158
+ document page's step line shows; processing again clears every step's result first. The pipeline, in order, will be: **translate** CodeableConcepts
159
+ to the international terminologies; generate the **narrative**; **validate** the document;
160
+ measure its quality with PIQI; check it against the **clinical profiles**; **assess** it with AI. Translation, the narrative,
161
+ validation, the **terminology check**, the clinical profiles check and the AI assessment exist so far (PIQI does not). A step keeps what it finds in one of two places: as a result stored against the
162
+ document in `document_results` (one row per step - the validation outcome), or in the
163
+ **analysed copy** - a second copy of the document (`documents.analysed_json`) that the steps
164
+ adorn with extensions, so that the master document is never changed. The analysed copy is
165
+ rebuilt from the master on every run, so a document can always be processed again.
166
+ Anything left `processing` when the server stopped is queued again at startup. Managers can
167
+ queue a document again from its page, though not while it is being processed.
168
+
169
+ ### Translation
170
+
171
+ The first step. Every CodeableConcept the terminology view lists is checked as the
172
+ terminology check (below) checks it; one that fails - no coding in the element's value set,
173
+ or, for an element with no value set of its own, no international coding - is sent to the
174
+ terminology server to translate: `ConceptMap/$translate` with `sourceCodeableConcept` (the
175
+ concept as submitted) and `targetScope` (the element's value set, or the fallback), the
176
+ value set and what it draws on as `tx-resource`. The server chooses the concept maps. Every
177
+ match that comes back (whatever its relationship) is added to the concept in the analysed
178
+ copy as a Coding, unless the concept already has that system and code, with an extension
179
+ saying where it came from:
180
+ `http://healthintersections.com.au/fhirsmith/StructureDefinition/ips-translation`, with
181
+ `server` (the terminology server, as configured), `conceptMap` (the match's `originMap`, when
182
+ the server names one) and `relationship` (the match's, R4's `equivalence` accepted too). The
183
+ terminology view marks such a coding "translated", with the detail on hover. A concept the
184
+ server could not say anything about (unknown) is not translated; a translation the server
185
+ fails on is logged and the step goes on. Translations are cached as the check's answers are,
186
+ and counted against the same per-document limit. The step's note says how many concepts
187
+ were tried and translated, and its result (`document_results`, step `translate`) records
188
+ what happened to every concept - not needed (already in its value set), not checked (the
189
+ server could not say), translated (the codings that came back, with their concept maps and
190
+ relationships, and which were already present), no translation, or failed - which the
191
+ document's `translation` view shows as a table.
192
+
193
+ ### Narrative
194
+
195
+ The second step: the IPS presenter (`ips/presenter.js`) generates the document's narrative
196
+ from the analysed copy (so the translations are in it), as xhtml. It follows the FHIR rules
197
+ for presenting a document - the Composition's title, its subject, and its facts (date,
198
+ status, type, authors, attesters, custodian, encounter, event, confidentiality), then the
199
+ sections - with the patient summarised the way the Java PatientRenderer does (name, gender,
200
+ date of birth and identifiers on one line, then other names, contact details, address,
201
+ marital status, language, managing organization, general practitioner and contacts) and the
202
+ other people and organizations named by their names. The sections come in the IPS order
203
+ (`SECTION_ORDER` in terminology.js), sections the IPS doesn't know after them in document
204
+ order, nested sections after their parent with a smaller heading. Each section is titled
205
+ with its own title, else the IPS's default for its code. A section with entries is a table
206
+ of its resources, one row each (a resource referenced twice appears once), with the columns
207
+ that suit the resource type (`COLUMNS` in presenter.js: roughly the IPS profile's
208
+ must-support elements - AllergyIntolerance, Condition, MedicationStatement, MedicationRequest,
209
+ Medication, Immunization, Procedure, Observation, DiagnosticReport, DeviceUseStatement,
210
+ Device, Flag, Consent, CarePlan and ClinicalImpression have their own; anything else, or a
211
+ section of mixed types, gets the generic ones: resource, category, name, type, code, value,
212
+ status); a column no resource in the section has a value for is left out, and a resource's
213
+ nested parts (reactions, body sites, dosages, components, reference ranges, notes...) are
214
+ bullet points in a follow-up row beneath it. A section with no entries shows its own
215
+ narrative (parsed, not copied, and headed "Narrative as submitted"), else its emptyReason,
216
+ else "Nothing found to render".
217
+ References to a Medication show the medication's code; a reason or evidence that is a
218
+ reference is followed, an Observation shown as "code: value"; medication[x] and the other
219
+ choice elements are rendered by their type. A condition's or allergy's clinical and
220
+ verification statuses share one Status column ("Active / Confirmed"). An Observation's
221
+ single reference range is a column (several go beneath), and a blood pressure with systolic
222
+ and diastolic components is shown as one value (120/80 mmHg). Dates are dd-mmm yy, date
223
+ times dd-mmm yy hh:nn (tz), partial dates as far as they go. A coded value shows its text, else the first coding's
224
+ display, else any coding's display, else what the terminology server's `CodeSystem/$lookup`
225
+ says for a coding (cached, and at most 500 questions per document), else the code, else
226
+ `--`. The output is plain xhtml (tables with class names, no styles or scripts), every
227
+ value escaped.
228
+
229
+ The narrative is kept as the step's result (the whole document, and each section) and
230
+ written into the analysed copy as the Composition's `text` and each section's `text`
231
+ (status `generated`), which is what the validator then sees. The document's `generated`
232
+ view shows it.
233
+
234
+ ### Validation
235
+
236
+ `modules.ips.validator` configures a FHIR validator service, exactly as the SHL module's
237
+ does (`fhir-validator-wrapper`: the validator CLI jar in the data folder's `bin` directory,
238
+ run as a service on its own `port`, with `txServer` - tx.fhir.org, by default - and the IPS
239
+ package loaded). It starts in the background when the module does, and the queue waits for
240
+ it. Each document - its analysed copy, so the translations are seen (the validator will
241
+ remark on the translation extension until it is given its definition) - is validated against the IPS Bundle profile
242
+ (`http://hl7.org/fhir/uv/ips/StructureDefinition/Bundle-uv-ips`; set `profiles` to validate
243
+ against something else) with the validator's IPS code checks on (`checkIpsCodes`), and the
244
+ OperationOutcome is stored with the document and shown on
245
+ its `validation` view: the counts, each issue's severity, code, location and message, and
246
+ the raw OperationOutcome. Without a configured, running validator the step fails, and the
247
+ document's status is `error`.
248
+
249
+ ### Terminology check
250
+
251
+ After validation, every CodeableConcept the terminology view lists is checked for its
252
+ suitability for international exchange, against this module's own value sets - not the IPS
253
+ profiles' bindings. The value sets are FHIR ValueSets in `ips/valuesets/`
254
+ (`ValueSet-ips-*.json`, canonical `http://healthintersections.com.au/fhirsmith/ValueSet/ips-*`),
255
+ and `elements.json` there says which one each element is held to, by path
256
+ (`Condition.code` → `ips-condition-code`, `MedicationStatement.medicationCodeableConcept`,
257
+ `MedicationRequest.medicationCodeableConcept` and `Medication.code` → `ips-medication`,
258
+ `Observation.code` and `DiagnosticReport.code` → `ips-observation-code` (any LOINC code),
259
+ ...). They are built on SNOMED CT International (every SNOMED CT include is pinned to the
260
+ International edition, `http://snomed.info/sct/900000000000207008`), LOINC, EDQM routes and
261
+ a few HL7 codes; `ips-codeable-concepts.md` lists every CodeableConcept element the IPS
262
+ profiles have, with their bindings, as the review the value sets came from. The folder also
263
+ carries the IPS's absent/unknown ValueSet and CodeSystem, which the vaccine and fallback
264
+ value sets draw on; they go with each question as `tx-resource`, so the server need not
265
+ know the IPS. The value sets are read at startup (`ips/ips-valuesets.js`); a folder that
266
+ doesn't hold together (an element mapped to a value set that isn't there, an include of
267
+ one that isn't) is logged and the step fails.
268
+
269
+ Elements not in `elements.json` have no value set of their own. For every element there is
270
+ a fallback: a coding is international anyway if it is in `ips-international-codes` (any
271
+ SNOMED CT International or LOINC code, or an IPS absent/unknown code), or if its system is
272
+ an HL7 terminology (`http://terminology.hl7.org/...` or `http://hl7.org/fhir/...`, but not
273
+ the `http://hl7.org/fhir/sid/` URLs that name other people's terminologies) and the code
274
+ exists in it (`CodeSystem/$validate-code`). The fallback is coded logic, not a value set,
275
+ for the HL7 case.
276
+
277
+ For each concept the terminology server is asked, with `ValueSet/$validate-code`, whether
278
+ any of its codings is in the element's value set (the value set itself and what it draws on
279
+ go with the request); failing that, whether any coding passes the fallback. The verdict is
280
+ written onto the concept in the analysed copy as an extension
281
+ (`http://healthintersections.com.au/fhirsmith/StructureDefinition/ips-valueset-check`:
282
+ `result` in | international | not-in | unknown, the `valueSet` it is held to or that
283
+ accepted it, the `system` for an HL7 code, the matching `coding`, a `message`), and the
284
+ terminology view's Analysis column shows it: a green tick when a coding is in the element's
285
+ value set (or, for an element with no value set of its own, when a coding is international),
286
+ an orange warning sign when no coding is in the element's value set but one is international,
287
+ a red cross when no coding is anywhere (a concept with no codings is a cross too), and a grey
288
+ question mark when the server could not say true or false. Answers are cached across
289
+ documents. A resubmitted analysed copy has its old verdicts stripped before it is checked
290
+ again.
291
+
292
+ ### Clinical profiles
293
+
294
+ The profiles step runs after the terminology check and before the AI assessment. It checks the document against every [clinical profile](#clinical-profiles). Each constraint is evaluated on the analysed copy of the document, so translated codings count. A constraint is met when its expression evaluates to `true`, and only `true`. For each profile, the step records one of three outcomes, along with the descriptions of the constraints the document doesn't meet:
295
+
296
+ - **conforms:** every constraint is met.
297
+ - **does not conform:** at least one constraint plainly fails.
298
+ - **could not be fully checked:** the only failures are constraints that couldn't be decided. Either the expression couldn't be evaluated, or it uses a value set the terminology server couldn't answer for.
299
+
300
+ Value set membership (`entryIn()`, `resourcesIn()`) is asked of the terminology server through one membership session for the whole document, so a question is asked once however many profiles use it. Definite answers are kept across documents. A constraint is given 60 seconds.
301
+
302
+ The **Clinical Profiles** view lists each profile, linked to it, with its outcome and the constraints the document doesn't meet. A constraint that couldn't be decided is marked as such, and its reason is shown to the project's managers. The document is checked against the profiles as they were when it was processed; reprocess it to check against them as they are now.
303
+
304
+ ### AI assessment
305
+
306
+ The last step, and off unless a project's managers turn it on (Settings → AI assessment),
307
+ because it sends the document to a third party and costs money: the analysed copy of the
308
+ document, as JSON, goes to an AI service with a prompt
309
+ (`AI_SYSTEM_PROMPT` in pipeline.js) asking for two assessments - the document as a whole,
310
+ for consistency and integrity (do its details agree with each other and the overall
311
+ picture; is anything missing) and its terminology (are the codes consistent with each other
312
+ and their text; are any wrong, contradictory or misplaced) - answered as JSON: a grade A-F,
313
+ a summary, consistency findings (severity, finding, where), what is missing, and terminology
314
+ findings (severity, finding, the codes). Whether the narrative goes is the project's choice
315
+ (its setting, else the server's `sendNarrative`, else not): without it the document is
316
+ smaller and cheaper; with it, the narratives are the ones the sender wrote (from the master -
317
+ the analysed copy's were generated by the narrative step), and the prompt asks for a third
318
+ assessment, what the narrative says that the structured data does not back up, and the
319
+ reverse. The submission page says so when it is on.
320
+
321
+ The service is an account - provider, base URL, model, API key - of one of two shapes
322
+ (`ips/ai-client.js`): `openai`, the chat-completions API that OpenAI, OpenRouter, LiteLLM,
323
+ Groq, Ollama, Azure and others speak (so a routing service is just a base URL), or
324
+ `anthropic`, the Messages API. A project may keep its own account (its managers enter it on
325
+ the settings page; the key is encrypted at rest with AES-256-GCM under `modules.ips.aiKeySecret`,
326
+ else the `sessionSecret`, so a copy of the database is not a copy of the account); a
327
+ project with none uses the server's default, `modules.ips.ai`; with neither, the step is
328
+ skipped. Each project may make `dailyCap` assessments in a rolling day (its own setting,
329
+ else the server's, else 50; the calls are counted in `ai_calls`), and a document bigger
330
+ than `maxBytes` (300000) is not sent; both are skips with a note, not failures. A service
331
+ that fails is a failure (the document's status is `error`), and still counts against the
332
+ cap. The result (model, token counts, cost, bytes sent, the tidied assessment and the
333
+ model's raw answer) is the step's result; the `ai` view shows the grade, the summary, the
334
+ findings as tables, and the raw answer behind a button. What each call used - model, tokens
335
+ in and out, cost - is kept in `ai_calls`, and the settings page totals it for the project
336
+ (today, the last 30 days, all time). The cost is what the service reports (OpenRouter's
337
+ `usage.cost`), else worked out from a price list - USD per million tokens by model name or
338
+ a prefix of it: Anthropic's current models are built in (`DEFAULT_PRICES` in ai-client.js,
339
+ and they will go stale), and `modules.ips.ai.prices` (`"gpt-4o": { "input": 2.5, "output": 10 }`)
340
+ adds to or overrides them - else not known, and the totals say how many calls had no known
341
+ cost. The answer is the model's opinion, and the page
342
+ says so.
343
+
344
+ ## The editor
345
+
346
+ `/ips/{code}/compose` is an editor for composing a document in the browser ("Compose a
347
+ document" on the project's page; "Edit" on a document's page opens the editor with what
348
+ the form can hold of that document, and submitting makes a new document). The page is the
349
+ patient and the document's header (title, author - a practitioner or an organization -
350
+ custodian, date), then a panel per IPS section in the IPS order; the three the IPS requires
351
+ (problems, allergies, medications) are open, the rest collapsed. Each section is filled one
352
+ of three ways: entries - rows of the fields the presenter renders for that resource type,
353
+ coded fields looking codes up as you type; a narrative written by hand (paragraphs at
354
+ blank lines); or a reason it is empty (the `list-empty-reason` codes). Optional sections
355
+ can also be left out. What the form holds is declared in `ips/editor-schema.js` (the
356
+ fields, their types, the value set each coded field is bound to, where each lands in the
357
+ resource) and drives the form (`ips/editor.js`, served at `/ips/editor.js`), the building
358
+ of the document (`ips/composer.js`: the Bundle, the Composition with its sections, the
359
+ Patient, the author, each row's resource - a blood pressure as components, a device as a
360
+ Device and a DeviceUseStatement - and the narrative, generated by the presenter for the
361
+ data sections) and the reading of a document back into the form.
362
+
363
+ Lookups go to `/ips/{code}/lookup?vs={value set id}&q={text}`, which runs
364
+ `ValueSet/$expand` with a filter on the terminology server against one of the module's
365
+ own value sets (`ips/valuesets/`), returning up to 20 codes (rate-limited on their own:
366
+ `rateLimit.lookupMax`, 600 a minute). Anything can also be entered as text. The browser
367
+ keeps the model in localStorage until it is submitted. Submitting (`POST /ips/{code}/compose`,
368
+ JSON `{ action: 'submit' | 'download', model, token }`) builds the document, checks it as
369
+ a submission is checked (the project's token applies), and stores it like any other
370
+ (source `editor`), or hands it back as a file without storing it; the problems with a model
371
+ come back as a list for the page to show.
372
+
373
+ ## Clinical profiles
374
+
375
+ A clinical profile says what a kind of clinical document is expected to contain. It is a StructureDefinition: a profile on Bundle (the document) with one element, the root, whose constraints are the expectations. Each constraint has a description and a FHIRPath expression on the document. Profiles are shared across all projects (`/ips/StructureDefinition`). Everyone can see them; only the administrator creates, edits, uploads and deletes them.
376
+
377
+ The administrator edits only the title and the constraints. The server sets everything else:
378
+
379
+ - the id: `profile-` plus a random private id;
380
+ - the url: `[base]/ips/StructureDefinition/profile-{id}`, fixed when the profile is made;
381
+ - the name, derived from the title (`Cardiology referral` → `CardiologyReferral`);
382
+ - each constraint's key (`cp-1`, `cp-2`...). A key is kept for as long as its constraint is, and never reused;
383
+ - severity `error`, status `active` and the date.
384
+
385
+ Most expectations take one of two shapes, using the module's own [FHIRPath functions](#fhirpath-functions):
386
+
387
+ ```
388
+ Bundle.section(system, code [, min, max]).exists()
389
+ Bundle.section(system, code [, min, max]).entry(system, code [, min, max]).exists()
390
+ ```
391
+
392
+ Rules about the resources in the document, whatever section they are in, take a third shape:
393
+
394
+ ```
395
+ Bundle.resources(system, code [, type]).{the rest of the rule}
396
+ ```
397
+
398
+ Entries and resources can be matched by value set instead of by one code: `Bundle.section(...).entryIn(valueSet [, min, max]).exists()`, and `Bundle.resourcesIn(valueSet [, type])...`. Sections are always matched by code. An example rule: `Bundle.resources('http://snomed.info/sct', '38341003', 'Condition').all(clinicalStatus.coding.code = 'active')`.
399
+
400
+ The editor (`/ips/StructureDefinition/new`, or Edit on a profile) is in three parts, and so is the page that shows a profile:
401
+
402
+ - **Required sections and entries:** the first two shapes, each a row of a table with four columns (the section, its cardinality, the entry code and its cardinality).
403
+ - **Rules about resources:** the third shape, each a row of a table with four columns (the type, which is optional, the system, the code, and the rest of the rule as FHIRPath starting with a dot).
404
+ - **Other rules:** any other FHIRPath.
405
+
406
+ A rule moves between the parts when it is switched between table and FHIRPath, and it lands in the part whose shape it fits. A section is one of the IPS sections, or any code in any system. An entry or resource code is a code in a named code system (SNOMED CT, LOINC, ICD-10...) or in any other given by URL, and it is looked up on the terminology server (`GET /ips/lookup?system=&q=`, for the administrator only). Choosing "In a value set..." as the system matches entries or resources by value set instead. The code box becomes a value set search (`GET /ips/lookup-vs?q=`, for the administrator only): this module's own value sets that match the title come first, then the terminology server's (a `ValueSet?title=` search). A suggestion shows its title, with its URL underneath. A canonical can also be typed or pasted in, with `|version` to pin a version. When the editor opens, it looks up the title of each value set by URL (`?url=`). If the terminology server doesn't know a value set, it says so as a warning, not an error: the value set has to be known when documents are checked, not when the profile is saved. A cardinality is blank (no bounds) or `min..max`, where max may be `*`. Before a profile is saved, every expression is parsed and run once against an empty document. That catches unknown functions and wrong numbers of arguments as well as syntax errors.
407
+
408
+ A profile can be downloaded as JSON (`?download=1`), and a downloaded profile can be uploaded again as a new profile, with a new id, url and keys. An upload must be no more than a profile like this: a profile on Bundle with no snapshot, and one differential element (the root) carrying constraints that have only a key, an `error` severity, a description, an expression and a source. Anything more is rejected, with the reasons. A profile downloaded when clinical profiles were on Composition is accepted and moved onto Bundle, as profiles already stored are when the server starts: `Composition.section(...)` becomes `Bundle.section(...)`, and any other `Composition.x` becomes `Bundle.entry.resource.ofType(Composition).x`.
409
+
410
+ As FHIR: `GET /ips/StructureDefinition` is a searchset of them all, and `GET /ips/StructureDefinition/{id}` is one of them.
411
+
412
+ ## FHIRPath functions
413
+
414
+ `ips/fhirpath-ips.js` adds two functions to fhirpath.js, for expressions on IPS documents:
415
+
416
+ - `section(system, code [, min, max])`: the input is documents (Bundles) or Compositions. On a document it moves into the document's Composition by itself, which is why a clinical profile's rules, evaluated on the document, can say `Bundle.section(...)`. For each input, it returns the Composition's sections, nested ones included, whose `code` has a coding with that system and code.
417
+ - `resources(system, code [, type])`: the input is documents, or anything in one (a Composition, say). It returns every resource in the document whose `code`, `medicationCodeableConcept` or `vaccineCode` has a coding with that system and code, and that is of the given type when a type is given.
418
+ - `entryIn(valueSet [, min, max])` and `resourcesIn(valueSet [, type])`: as `entry()` and `resources()`, but they match a code in a value set (a canonical, with or without `|version`) rather than one given code. A resource is in when any coding of its code is. These two ask a terminology server, so the evaluation must be asynchronous (`evaluateAsync`), with a `membership` function in the options. A `MembershipSession` (`ips/membership.js`, made by the module's `membershipSession()`) provides one: it asks `ValueSet/$validate-code` with the CodeableConcept, only for what a rule actually reaches. Within a session, each question is asked once, and there's a budget of 500 per document. Definite answers are also kept in the module's cache, so the next document doesn't ask again. When the server can't answer (it doesn't know the value set, say), the answer is "not a member" for that run, and the session's `unresolved()` lists the value set and the server's message, so a rule that failed for that reason can be told apart from one that plainly failed. The module's own value sets (`ips/valuesets/`) are sent with the question, so any server can answer for them; any other value set must be one the server knows.
419
+ - `entry(system, code [, min, max])`: the input is sections, usually from `section()`. For each one, it returns its resolved entries whose `code`, `medicationCodeableConcept` or `vaccineCode` has a coding with that system and code. `medicationReference` is not followed.
420
+
421
+ `min` and `max` are optional (both or neither), and `max` may be `'*'`. The bounds apply to each input item: when the number found is outside them, that item adds nothing. That isn't an error. References are resolved in the Bundle the section is in, or failing that the Bundle the expression was evaluated on:
422
+
423
+ - absolute `fullUrl`s (`urn:uuid:`, `http…`) match as they are;
424
+ - a relative `Type/id` is resolved against the base of the Composition's `fullUrl`, or matched by type and id when there's no usable base;
425
+ - `#id` is a resource contained in the Composition.
426
+
427
+ Anything still unresolved goes to the caller's `resolve(reference, composition)`, if there is one.
428
+
429
+ ```js
430
+ const { evaluate, ipsFunctions } = require('./fhirpath-ips');
431
+ evaluate(bundle, "section('http://loinc.org', '11450-4', 1, 1).entry('http://snomed.info/sct', '38341003', 1, '*').exists()");
432
+ // or, with fhirpath.js directly:
433
+ fhirpath.evaluate(bundle, expr, null, r4Model, { userInvocationTable: ipsFunctions({ resolve }) });
434
+ ```
435
+
436
+ ## Sharing as SHL / VHL
437
+
438
+ With `modules.ips.shl` configured, a document's page has "Share as SHL" and "Share as VHL"
439
+ buttons (for anyone who can see the document). Each makes a QR code in a panel over the
440
+ page, to scan or copy, with what the code carries in a box beside a Copy button, and the
441
+ link to the manifest; without scripts, the same lands on its own page. The steps are the
442
+ ones the healthintersections.com.au passport page takes, here on the server (`ips/share.js`):
443
+ an entry is created on the SHL server (`POST {shl}/create`, with the server's password and
444
+ the number of days the copy lives), the document (the master, as submitted) is encrypted
445
+ as a JWE (`dir` / `A256GCM`, a random key) and uploaded (`POST {shl}/upload`), and the
446
+ shlink payload - the manifest link, the key, flag `U`, a label naming the patient - is
447
+ base64url-encoded. An SHL's code carries `{viewer}#shlink:/{payload}`; a VHL's carries
448
+ `HC1:` and the SHL server's signature of `shlink://{payload}` (`POST {shl}/sign`: a
449
+ COSE_Sign1 of the GDHCN payload, deflated and base45). The SHL server is this server's own
450
+ shl module (`"server": "/shl"`, dispatched in-process, the password taken from
451
+ `modules.shl.password` unless given) or another FHIRsmith's (an https URL and its password).
452
+ Shares are rate-limited as submissions are, and a document whose JWE is over 2MB is not
453
+ shared this way. The QR codes are made with `qrcode-generator`, error correction L.
454
+
455
+ ### Validation view
456
+
457
+ The view shows everything by default; its "Show" selector (`?level=warning` or
458
+ `?level=error`) hides the lesser issues - the summary line still counts them all.
459
+ `fhir-validator-wrapper` 1.5.0 or later is needed: earlier versions sent the profiles in a
460
+ form the validator service refuses.