Skip to content

sdc-template-extract: a nested templateExtractContext is evaluated against the template's response item, not against its enclosing context #2171

Open

Description

@pavlushkin

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

  1. A Questionnaire with a group symptoms that references the contained template #obsTemplate, and inside it a repeating group entry with a string name and a repeating string codes. The template's component has a context item.where(linkId='entry'), code.text from name, and a code.coding with its own context over the entry's codes:
{
  "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 }
          ]
        }
      ]
    }
  ]
}
  1. A QuestionnaireResponse with two entries: cough with codes c1, c2, and fever with code f1.
{
  "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" }] }
          ]
        }
      ]
    }
  ]
}
  1. 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 templateExtractContext gets its own entry in a flat templateExtractPathMap (walkTemplate.ts :89-111). The entry holds the context's path and expression, not the context that encloses it.
  • evaluateAndInsertIntoPath evaluates 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, via getCombinedExpression, expressionManipulation.ts:8-21). So a relative inner expression is evaluated against the group symptoms, 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:

  1. While walking the template, record each context's nearest enclosing context.
  2. 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.
  3. 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions