Repository navigation
sdc-template-extract: a nested templateExtractContext is evaluated against the template's response item, not against its enclosing context #2171
Description
Summary
In @aehrc/sdc-template-extract 1.0.15 and 1.0.16, a templateExtractContext inside the scope of another context is not evaluated once per result of the enclosing context. Its expression is appended to the template's response item path, whatever contexts enclose it. So a nested context written relative to its parent, as SDC defines it, extracts nothing, and one written from the template's response item collects the results of every parent instance into the first copy of the element. Extraction reports success with no issues in both cases.
SDC (Template-based extraction, same text in STU4): "For every result returned by the expression, a new copy of the templated property will be cloned and scanning the child properties will continue. [...] Any context results will change the context to use for processing any child properties (and templateExtractValues on the same node)."
A templateExtractValue below a context already follows this rule: in the example below, code.text is evaluated per entry. Only a nested context does not.
Steps to reproduce
- A Questionnaire with a group
symptomsthat references the contained template#obsTemplate, and inside it a repeating groupentrywith a stringnameand a repeating stringcodes. The template'scomponenthas a contextitem.where(linkId='entry'),code.textfromname, and acode.codingwith its own context over the entry'scodes:
{
"resourceType": "Questionnaire",
"id": "Q",
"status": "active",
"contained": [
{
"resourceType": "Observation",
"id": "obsTemplate",
"status": "final",
"code": { "text": "Symptoms" },
"component": [
{
"extension": [
{
"url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-templateExtractContext",
"valueString": "item.where(linkId='entry')"
}
],
"code": {
"_text": {
"extension": [
{
"url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-templateExtractValue",
"valueString": "item.where(linkId='name').answer.value"
}
]
},
"coding": [
{
"extension": [
{
"url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-templateExtractContext",
"valueString": "item.where(linkId='codes').answer"
}
],
"system": "http://example.org",
"_code": {
"extension": [
{
"url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-templateExtractValue",
"valueString": "value"
}
]
}
}
]
}
}
]
}
],
"item": [
{
"linkId": "symptoms",
"type": "group",
"extension": [
{
"url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-templateExtract",
"extension": [{ "url": "template", "valueReference": { "reference": "#obsTemplate" } }]
}
],
"item": [
{
"linkId": "entry",
"type": "group",
"repeats": true,
"item": [
{ "linkId": "name", "type": "string" },
{ "linkId": "codes", "type": "string", "repeats": true }
]
}
]
}
]
}- A QuestionnaireResponse with two entries:
coughwith codesc1,c2, andfeverwith codef1.
{
"resourceType": "QuestionnaireResponse",
"status": "completed",
"item": [
{
"linkId": "symptoms",
"item": [
{
"linkId": "entry",
"item": [
{ "linkId": "name", "answer": [{ "valueString": "cough" }] },
{ "linkId": "codes", "answer": [{ "valueString": "c1" }, { "valueString": "c2" }] }
]
},
{
"linkId": "entry",
"item": [
{ "linkId": "name", "answer": [{ "valueString": "fever" }] },
{ "linkId": "codes", "answer": [{ "valueString": "f1" }] }
]
}
]
}
]
}- Call
inAppExtract(response, questionnaire, null).
Expected: two components, each with its own entry's codings:
"component": [
{ "code": { "text": "cough", "coding": [
{ "system": "http://example.org", "code": "c1" },
{ "system": "http://example.org", "code": "c2" }
] } },
{ "code": { "text": "fever", "coding": [
{ "system": "http://example.org", "code": "f1" }
] } }
]Actual: extractSuccess is true with no issues, and neither component has a coding:
"component": [
{ "code": { "text": "cough" } },
{ "code": { "text": "fever" } }
]With the inner context written from the template's response item instead:
| Inner context | Extracted component |
|---|---|
item.where(linkId='codes').answer (relative to the entry) |
cough and fever, no codings |
item.where(linkId='entry').item.where(linkId='codes').answer |
cough with all three codings c1, c2, f1; fever with none |
Neither form gives one coding per code of its own entry.
Reproduced with @aehrc/sdc-template-extract@1.0.15 and @1.0.16 (identical output), fhirpath 4.10.1 and Node 22, calling inAppExtract from the CJS build directly.
Cause
All in packages/sdc-template-extract/src/SDCExtractQuestionnaireResponseOperation/utils/, at 0fca4c36 (the 1.0.15 release commit). None of these files changed in 1.0.16 (3dc8dbb9), and the package is the same on main at 9ec3e213.
- Each
templateExtractContextgets its own entry in a flattemplateExtractPathMap(walkTemplate.ts:89-111). The entry holds the context's path and expression, not the context that encloses it. evaluateAndInsertIntoPathevaluates every context from the template's response item: it appends the context expression to the response item path as a string (evaluateTemplateExtractPath.ts:32-47, viagetCombinedExpression,expressionManipulation.ts:8-21). So a relative inner expression is evaluated against the groupsymptoms, where it finds nothing, and an absolute one finds the codes of every entry, which are then inserted into the first copy of the element.
valueString contexts have been handled this way since the first implementation (9821d96, May 2025), so every release up to 1.0.16 is affected. No templateExtractContext in the package's test Questionnaires is nested inside another.
#1620 (index misalignment, on hold) needs a nested answer tied to its parent instance; per-context evaluation would give that.
Suggested fix
Evaluate a nested context once per result of its enclosing context, instead of once from the template's response item:
- While walking the template, record each context's nearest enclosing context.
- Evaluate a top-level context against the template's response item, as now. Evaluate a nested context once for each result of its enclosing context, with that result as the FHIRPath input.
- Insert what a nested context produces into the copy of the element made for that enclosing result.
This changes what an existing nested context expression means: it becomes relative to its enclosing context, as SDC defines it, instead of to the template's response item. An absolute expression that works around the current behaviour would need rewriting. The package's test Questionnaires have no nested context, so none of them changes.
#2172 needs the same per-context evaluation to bind a context's name for the contexts below it.
Tests to add (each asserting the extracted resources and the issues):
- the example above: two components, each with its own entry's codings;
- the same with a third level of nesting;
- an enclosing context with no results: no copy, and the nested context not evaluated.
Activity