Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added

- Extension-codes supplements: a code in a code space the code system reserves for users (`codesystem-reservedCodeSpace`, a regular expression) is a valid code, whether or not anything defines it; with nothing defining it, its display is the code space's `default-display`, or 'User-assigned', and (as a placeholder) another display only gets an information issue. An `extension-codes` supplement in scope defines such codes, with their displays: a global one as if the code system defined them (a different display is an error, or a warning with lenient display validation), one that is referenced as a display that isn't the only correct one (a different display is a warning). Codes from supplements are only in an expansion when the value set lists them. ISO 3166's user-assigned codes work the same way: an extension-codes supplement's display replaces the 'User-assigned' placeholder. So do ICF's qualified (post-coordinated) codes and SNOMED CT post-coordinated expressions (matched as written, or in their minimal rendering): an extension-codes supplement's display is the code's display, and the generated one stays valid
- Reserved code spaces: an expansion of all of a code system that reserves code spaces (`codesystem-reservedCodeSpace`) is marked unclosed (`valueset-unclosed`, with the reason), since `$validate-code` accepts codes in those spaces that the expansion can't list
- `$cache-control` mode=start also returns the issued cache-id in the `X-Cache-Id` response header (the body is unchanged), so a request log taken at the proxy (nginx `$sent_http_x_cache_id`) can tie each cache-id to the request that created it, for replay
- IPS: a Structure view (after Narrative) showing a document's logical structure as an expandable tree: what the Composition refers to, then its sections, each with its entries and what else it refers to, and each resource referred to expanding into its own references. A reference that doesn't resolve in the document is `not-in-document`; a resource already shown isn't expanded again

### Changed
Expand Down
3 changes: 2 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,5 @@ LABEL org.opencontainers.image.title="FHIRsmith" \

# Expose port and define command
EXPOSE 3000
CMD ["node", "server.js"]
# --expose-gc: the memory guard forces a GC while it refuses requests (library/memory-guard.js)
CMD ["node", "--expose-gc", "server.js"]
120 changes: 101 additions & 19 deletions ips/analysis.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,15 @@
* modifier a modifier extension, or implicitRules (maroon)
* status inactive, entered in error, unknown, or missing status (navy)
* subject a subject other than the document's, or a focus (dark green)
* attention a future date, a Patient's link, a Composition that isn't final
* attention a date in the future where one isn't expected, a Patient's link, a
* Composition that isn't final
* note anything else (a section's emptyReason, a section's author)
*
* @module ips/analysis
*/

const { structureTree, flatten, isObject, text } = require('./structure');
const { typeOf } = require('./terminology');

// the resources whose status (or lifecycleStatus) is required, as the element that holds it
const STATUS_ELEMENTS = {
Expand All @@ -45,11 +47,59 @@ const STATUS_ELEMENTS = {
// extensions not worth mentioning
const IGNORED_EXTENSIONS = new Set(['http://hl7.org/fhir/StructureDefinition/textLink']);

// resources where a date in the future is to be expected
const FUTURE_OK = new Set(['Appointment', 'AppointmentResponse']);
// ---- dates in the future ----
//
// Only elements the (R4) type model says are dates (date, dateTime, instant) are looked at,
// so a string or an identifier that happens to look like a date isn't one. Some dates are
// about the future by their nature, wherever they are; some resources are about the future
// - always, or while their status says the thing hasn't happened yet.

// the statuses of a request (or plan) that is over: its dates should be past
const CLOSED = new Set(['completed', 'revoked', 'cancelled', 'entered-in-error', 'stopped', 'failed', 'rejected']);
const open = (r) => !CLOSED.has(r.status);

// resources whose dates may be in the future, by type: a function of the resource that says
// whether they may (for that resource as it is)
const FUTURE_INTENT = {
// about the future, whatever their status
Appointment: () => true,
AppointmentResponse: () => true,
ImmunizationRecommendation: () => true,
Schedule: () => true,
Slot: () => true,
// requests and plans, until they are over
ServiceRequest: open,
MedicationRequest: open,
DeviceRequest: open,
NutritionOrder: open,
CommunicationRequest: open,
SupplyRequest: open,
VisionPrescription: open,
CarePlan: open,
Task: open,
Goal: (r) => !CLOSED.has(r.lifecycleStatus),
// events, while they are still to come
Procedure: (r) => r.status === 'preparation',
Encounter: (r) => r.status === 'planned',
MedicationStatement: (r) => r.status === 'intended',
MedicationUsage: (r) => r.status === 'intended',
DeviceUseStatement: (r) => r.status === 'intended',
DeviceUsage: (r) => r.status === 'intended'
};

/** Whether a resource's dates may be in the future. */
function mayBeFuture(r) {
const f = FUTURE_INTENT[r.resourceType];
return !!f && f(r);
}

// dates that are about the future by their nature, wherever they are: the end of a period
// (a name, an address, an identifier valid until; a prescription's validity), an expiry,
// a due date. By element name, or by the type they are in (a Timing is a schedule)
const FUTURE_ELEMENTS = new Set(['end', 'expirationDate', 'dueDate']);
const FUTURE_TYPES = new Set(['Timing']);
const DATE_TYPES = new Set(['date', 'dateTime', 'instant']);

// keys whose values are dates even when they are just a year
const DATE_KEY = /date|time|instant|period|start|end|issued|recorded|onset|abatement|occurrence|effective|authored|when|birth|deceased|lastUpdated|created/i;
const DATE_VALUE = /^(\d{4})(?:-(\d\d)(?:-(\d\d)(?:T(\d\d):(\d\d)(?::(\d\d)(?:\.\d+)?)?(Z|[+-]\d\d:\d\d)?)?)?)?$/;

/** The earliest moment a date/dateTime/instant can mean, as ms, or null if it isn't one. */
Expand Down Expand Up @@ -107,17 +157,49 @@ function extensionsIn(node, prefix, skip) {
return out;
}

/** The dates after now in a resource or section, as [path, value]. */
function futureDates(node, prefix, skip, now) {
/**
* The dates after now in a resource or section, as [path, value]: the elements whose type is
* date, dateTime or instant, walking the resource with the type model - but not those about
* the future by their nature (FUTURE_ELEMENTS, FUTURE_TYPES), nor inside extensions (what
* their dates mean isn't known), nor elements the model doesn't know.
* @param {Object} node - the resource, or a section
* @param {string} modelPath - the model's path for node: the resource type, or Composition.section
* @param {string} prefix - the path shown for node
* @param {string[]} skip - keys of node not to look in (a Composition's sections)
* @param {number} now - in ms
*/
function futureDates(node, modelPath, prefix, skip, now) {
const out = [];
walk(node, prefix, skip, (k, v, at, path) => {
if (typeof v === 'string' && k !== 'div' && (v.includes('-') || DATE_KEY.test(k))) {
const t = earliest(v);
if (t !== null && t > now) {
out.push([path, v]);
const go = (obj, model, shown, top) => {
for (const [k, v] of Object.entries(obj)) {
if (v === null || k === 'resourceType' || k === 'contained' || k === 'extension' || k === 'modifierExtension' ||
k.startsWith('_') || (top && skip.includes(k)) || FUTURE_ELEMENTS.has(k)) {
continue;
}
const resolved = typeOf(`${model}.${k}`);
if (!resolved || FUTURE_TYPES.has(resolved.type)) {
continue;
}
const items = Array.isArray(v) ? v : [v];
items.forEach((item, i) => {
const path = `${shown}.${k}${Array.isArray(v) ? `[${i}]` : ''}`;
if (DATE_TYPES.has(resolved.type)) {
const t = typeof item === 'string' ? earliest(item) : null;
if (t !== null && t > now) {
out.push([path, item]);
}
} else if (isObject(item)) {
if (resolved.type === 'BackboneElement' || resolved.type === 'Element') {
go(item, resolved.path, path, false);
} else if (/^[A-Z]/.test(resolved.type) && !['Resource', 'Narrative', 'Extension'].includes(resolved.type)) {
// a datatype: its own elements are defined on the type
go(item, resolved.type, path, false);
}
}
});
}
});
};
go(node, modelPath, prefix, true);
return out;
}

Expand Down Expand Up @@ -163,7 +245,7 @@ function sameReference(a, fromA, b, fromB, resolve) {
}

/** The findings common to resources and sections: extensions, narrative, focus, future dates. */
async function common(findings, thing, prefix, skip, options, cache) {
async function common(findings, thing, modelPath, prefix, skip, options, cache) {
for (const [kind, url, path] of extensionsIn(thing, prefix, skip)) {
if (!cache.has(url)) {
let def = null;
Expand All @@ -183,8 +265,8 @@ async function common(findings, thing, prefix, skip, options, cache) {
if (focus.length) {
findings.push({ kind: 'subject', text: `Has a focus: ${focus.map(referenceWords).join(', ')}` });
}
if (!FUTURE_OK.has(thing.resourceType)) {
for (const [path, v] of futureDates(thing, prefix, skip, options.now)) {
if (!mayBeFuture(thing)) {
for (const [path, v] of futureDates(thing, modelPath, prefix, skip, options.now)) {
findings.push({ kind: 'attention', text: `A date in the future: ${path} = ${v}` });
}
}
Expand Down Expand Up @@ -239,7 +321,7 @@ async function analyseResource(node, ctx) {
findings.push({ kind: 'attention', text: `Linked${text(link.type) ? ` (${link.type})` : ''} to ${referenceWords(link.other)}${target ? '' : ' - not in the document'}` });
}
}
await common(findings, r, prefix, skip, ctx.options, ctx.cache);
await common(findings, r, type || 'Resource', prefix, skip, ctx.options, ctx.cache);
return finish(findings, narrativeOf(r));
}

Expand All @@ -259,7 +341,7 @@ async function analyseSection(node, ctx) {
(parent.length ? ` (${parent.map(referenceWords).join(', ')})` : ' (none)') });
}
}
await common(findings, sec, 'section', ['section'], ctx.options, ctx.cache);
await common(findings, sec, 'Composition.section', 'section', ['section'], ctx.options, ctx.cache);
return finish(findings, narrativeOf(sec));
}

Expand Down Expand Up @@ -328,4 +410,4 @@ const analysisStep = {
}
};

module.exports = { analyseDocument, analysisStep, extensionsIn, futureDates, earliest, STATUS_ELEMENTS };
module.exports = { analyseDocument, analysisStep, extensionsIn, futureDates, mayBeFuture, earliest, STATUS_ELEMENTS, FUTURE_INTENT };
14 changes: 12 additions & 2 deletions ips/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -214,8 +214,18 @@ Composition itself without its sections), keeps a list of findings, by the node'
status where the resource requires one (`lifecycleStatus` for a Goal);
- in dark green: a `subject` (or `patient`) that isn't the document's - not the same
resource in the document, nor the same reference, nor the same identifier - and any `focus`;
- dates in the future (except in an Appointment or AppointmentResponse): a date with no time
zone is in the future only if it is everywhere;
- dates in the future, where one isn't expected. Only elements the (R4) type model says are
dates (date, dateTime, instant) are looked at - so not an identifier or a note that looks
like a date - and not inside extensions. Some dates are about the future by their nature and
are never mentioned: the end of a period (a name's, address's or identifier's validity, a
prescription's), expiry dates, due dates, and schedules (Timing). Some resources may have
future dates: Appointment, AppointmentResponse, ImmunizationRecommendation, Schedule and
Slot always; requests and plans (ServiceRequest, MedicationRequest, DeviceRequest,
NutritionOrder, CommunicationRequest, SupplyRequest, VisionPrescription, CarePlan, Task, and
a Goal by its lifecycleStatus) until they are over (completed, revoked, cancelled,
entered-in-error, stopped, failed or rejected); and events still to come - a Procedure in
preparation, a planned Encounter, an intended MedicationStatement or DeviceUseStatement. A
date with no time zone is in the future only if it is everywhere;
- a Patient's `link`s; a Composition whose status isn't `final`;
- for a section: its `emptyReason`, and an author other than that of what contains it (the
Composition, or the containing section - which may have inherited its own);
Expand Down
2 changes: 1 addition & 1 deletion ips/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -880,7 +880,7 @@ function renderAnalysis(row, options = {}) {
'extensions (with their definitions, from the validator, where it knows them; textLink is left out), <span class="ips-an-modifier">modifier extensions and implicitRules</span>, ' +
'<span class="ips-an-status">records that are inactive, entered in error, of unknown status, or missing a status they should have</span>, ' +
'<span class="ips-an-subject">a subject that is not the document\'s, or a focus</span>, ' +
'<span class="ips-an-attention">dates in the future (except in Appointments), a Patient\'s links, and a Composition that is not final</span>, ' +
'<span class="ips-an-attention">dates in the future where none is expected (not period ends, expiry or due dates, schedules, nor appointments, requests and plans that are not over, or events still to come), a Patient\'s links, and a Composition that is not final</span>, ' +
'a section\'s emptyReason or an author other than its container\'s, and whether there is narrative. A resource with nothing else to say is All OK. References to resources that are not in the document are left out.</p></div>';
return h;
}
Expand Down
2 changes: 1 addition & 1 deletion ips/terminology.js
Original file line number Diff line number Diff line change
Expand Up @@ -391,5 +391,5 @@ function terminologyOf(bundle) {

module.exports = {
terminologyOf, conceptsOf, systemName, describeVersion, sctEdition, linkForSystem, linkForCode, isExternalSystem, isIgnored,
sectionRank, SECTION_ORDER, IGNORED, SYSTEM_NAMES
sectionRank, SECTION_ORDER, IGNORED, SYSTEM_NAMES, typeOf
};
Loading
Loading