From 37817db6f782847473175ec0f28cd4cab10fec91 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 Apr 2026 16:18:09 +0200 Subject: [PATCH 1/7] build(deps): bump com.fasterxml.jackson.core:jackson-core (#32) Bumps [com.fasterxml.jackson.core:jackson-core](https://github.com/FasterXML/jackson-core) from 2.18.0 to 2.18.6. - [Commits](https://github.com/FasterXML/jackson-core/compare/jackson-core-2.18.0...jackson-core-2.18.6) --- updated-dependencies: - dependency-name: com.fasterxml.jackson.core:jackson-core dependency-version: 2.18.6 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- linter-core/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/linter-core/pom.xml b/linter-core/pom.xml index 6186007..17ab91c 100644 --- a/linter-core/pom.xml +++ b/linter-core/pom.xml @@ -79,7 +79,7 @@ com.fasterxml.jackson.core jackson-core - 2.18.0 + 2.18.6 From 82f6ea4202b98ad3b360ba75c49eaa6938a56590 Mon Sep 17 00:00:00 2001 From: khalilmalla95 Date: Tue, 21 Apr 2026 17:10:34 +0200 Subject: [PATCH 2/7] Add new linting error codes for unresolved ValueSet bindings and fixedUri/CodeSystem mismatches. --- .../src/main/java/dev/dsf/linter/output/LintingType.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/linter-core/src/main/java/dev/dsf/linter/output/LintingType.java b/linter-core/src/main/java/dev/dsf/linter/output/LintingType.java index 374b8d2..e065a40 100644 --- a/linter-core/src/main/java/dev/dsf/linter/output/LintingType.java +++ b/linter-core/src/main/java/dev/dsf/linter/output/LintingType.java @@ -142,6 +142,11 @@ public enum LintingType { STRUCTURE_DEFINITION_SLICE_MAX_TOO_HIGH("StructureDefinition slice max exceeds base max."), STRUCTURE_DEFINITION_SLICE_MIN_SUM_EXCEEDS_MAX("StructureDefinition slice min sum exceeds max."), STRUCTURE_DEFINITION_SLICE_MIN_SUM_ABOVE_BASE_MIN("StructureDefinition slice min sum above base min."), + STRUCTURE_DEFINITION_BINDING_VALUESET_UNRESOLVED("StructureDefinition binding.valueSet (strength=required) references unknown ValueSet."), + STRUCTURE_DEFINITION_BINDING_VALUESET_UNRESOLVED_NON_REQUIRED("StructureDefinition binding.valueSet references unknown ValueSet."), + STRUCTURE_DEFINITION_FIXED_URI_CODESYSTEM_NOT_FOUND("StructureDefinition fixedUri not found in project; fixedCode validation is skipped."), + STRUCTURE_DEFINITION_FIXED_CODE_NOT_IN_CODESYSTEM("StructureDefinition fixedCode is not a known code in the referenced CodeSystem."), + // ==================== FHIR TASK ==================== FHIR_TASK_IDENTIFIER_INVALID_FORMAT("Task identifier with system 'http://dsf.dev/sid/task-identifier' has invalid format. Expected: {process-url}/{process-version}/{task-example-name}"), @@ -182,6 +187,9 @@ public enum LintingType { FHIR_TASK_REQUESTER_ID_NO_PLACEHOLDER("Task requester ID missing placeholder."), FHIR_TASK_RECIPIENT_ID_NOT_EXIST("Task recipient ID does not exist."), FHIR_TASK_RECIPIENT_ID_NO_PLACEHOLDER("Task recipient ID missing placeholder."), + FHIR_TASK_INPUT_FIXED_URI_MISMATCH("Task input coding.system does not match fixedUri from StructureDefinition."), + FHIR_TASK_INPUT_FIXED_CODE_MISMATCH("Task input coding.code does not match fixedCode from StructureDefinition."), + FHIR_TASK_INPUT_PAIR_NOT_ALLOWED_BY_SD("Task input (system, code) pair is not defined by any fixedUri/fixedCode constraint in the StructureDefinition."), // ==================== FHIR VALUE SET ==================== FHIR_VALUE_SET_MISSING_URL("ValueSet is missing URL."), From e42ce95a205e5fdf5d2d2d0fcd8af578835f2c81 Mon Sep 17 00:00:00 2001 From: khalilmalla95 Date: Tue, 21 Apr 2026 17:11:47 +0200 Subject: [PATCH 3/7] Add method to check if ValueSet contains a specific URL in FhirResourceExtractor --- .../util/resource/FhirResourceExtractor.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/linter-core/src/main/java/dev/dsf/linter/util/resource/FhirResourceExtractor.java b/linter-core/src/main/java/dev/dsf/linter/util/resource/FhirResourceExtractor.java index b1c852b..f8c0241 100644 --- a/linter-core/src/main/java/dev/dsf/linter/util/resource/FhirResourceExtractor.java +++ b/linter-core/src/main/java/dev/dsf/linter/util/resource/FhirResourceExtractor.java @@ -132,6 +132,20 @@ public boolean activityDefinitionContainsInstantiatesCanonical(Document doc, return evaluateXPathExists(doc, xpathExpr); } + /** + * Checks whether the given ValueSet {@link Document} contains a {@code } element + * whose {@code value} attribute exactly matches the provided canonical URL. + * + * @param doc the parsed XML {@link Document} representing a FHIR ValueSet + * @param url the canonical URL to search for (without version suffix) + * @return {@code true} if a matching {@code } element exists; {@code false} otherwise + * @throws XPathExpressionException if an XPath evaluation error occurs + */ + public boolean valueSetContainsUrl(Document doc, String url) throws XPathExpressionException { + return evaluateXPathExists(doc, + "/*[local-name()='ValueSet']/*[local-name()='url' and @value='" + url + "']"); + } + /** * Checks whether the given Questionnaire {@link Document} contains a {@code } element * whose {@code value} attribute exactly matches the provided canonical URL. From 735c6d1e89d139c7274302ce7d3a990e90e490cc Mon Sep 17 00:00:00 2001 From: khalilmalla95 Date: Tue, 21 Apr 2026 17:12:16 +0200 Subject: [PATCH 4/7] Add method to check if ValueSet exists for a given canonical URL in FhirResourceLocator --- .../util/resource/FhirResourceLocator.java | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/linter-core/src/main/java/dev/dsf/linter/util/resource/FhirResourceLocator.java b/linter-core/src/main/java/dev/dsf/linter/util/resource/FhirResourceLocator.java index d2421d6..5a6d526 100644 --- a/linter-core/src/main/java/dev/dsf/linter/util/resource/FhirResourceLocator.java +++ b/linter-core/src/main/java/dev/dsf/linter/util/resource/FhirResourceLocator.java @@ -39,6 +39,7 @@ public final class FhirResourceLocator { private static final String ACTIVITY_DEFINITION_DIR = "fhir/ActivityDefinition"; private static final String STRUCTURE_DEFINITION_DIR = "fhir/StructureDefinition"; private static final String QUESTIONNAIRE_DIR = "fhir/Questionnaire"; + private static final String VALUE_SET_DIR = "fhir/ValueSet"; private final ResourceProvider provider; private final FhirResourceExtractor extractor; @@ -197,6 +198,24 @@ public boolean questionnaireExists(String formKey, File projectRoot) { ); } + /** + * Checks if a ValueSet exists for the given canonical URL. + *

+ * Automatically removes version suffixes from the URL before searching. + *

+ * + * @param url the canonical URL of the ValueSet to search for + * @param projectRoot the project root directory (currently unused, kept for API compatibility) + * @return true if a ValueSet with the specified URL exists + */ + public boolean valueSetExists(String url, File projectRoot) { + String base = ResourcePathNormalizer.removeVersionSuffix(url); + return searchInDirectories( + entry -> checkValueSetForUrl(entry, base), + VALUE_SET_DIR + ); + } + /** * Checks if any ActivityDefinition has the specified message name. * @@ -324,6 +343,17 @@ private boolean checkQuestionnaireForUrl(FhirResourceEntry entry, String url) { }); } + private boolean checkValueSetForUrl(FhirResourceEntry entry, String url) { + return checkFhirResource(entry, "ValueSet", + doc -> { + try { + return extractor.valueSetContainsUrl(doc, url); + } catch (XPathExpressionException e) { + throw new RuntimeException(e); + } + }); + } + private boolean checkFhirResource(FhirResourceEntry entry, String expectedRootName, Predicate checkLogic) { From df18a9994188bd62423b22dac5d6c8095908e6e6 Mon Sep 17 00:00:00 2001 From: khalilmalla95 Date: Tue, 21 Apr 2026 17:15:20 +0200 Subject: [PATCH 5/7] Add checks for unresolved binding.valueSet references and fixedUri/CodeSystem mismatches in FhirStructureDefinitionLinter --- .../fhir/FhirStructureDefinitionLinter.java | 207 ++++++++++++++++++ 1 file changed, 207 insertions(+) diff --git a/linter-core/src/main/java/dev/dsf/linter/fhir/FhirStructureDefinitionLinter.java b/linter-core/src/main/java/dev/dsf/linter/fhir/FhirStructureDefinitionLinter.java index 609a7d5..ba064d1 100644 --- a/linter-core/src/main/java/dev/dsf/linter/fhir/FhirStructureDefinitionLinter.java +++ b/linter-core/src/main/java/dev/dsf/linter/fhir/FhirStructureDefinitionLinter.java @@ -4,10 +4,15 @@ import dev.dsf.linter.output.LintingType; import dev.dsf.linter.output.item.*; import dev.dsf.linter.util.resource.FhirAuthorizationCache; +import dev.dsf.linter.util.resource.FhirResourceLocator; import dev.dsf.linter.util.linting.AbstractFhirInstanceLinter; +import dev.dsf.linter.util.linting.LintingUtils; import org.w3c.dom.Document; +import org.w3c.dom.Node; import org.w3c.dom.NodeList; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathFactory; import java.io.File; import java.util.*; @@ -140,6 +145,13 @@ public List lint(Document doc, File resFile) /* 4 - slice-count vs. min/max */ checkSliceCardinality(doc, resFile, ref, issues); + + /* 5 - binding.valueSet references */ + checkBindingValueSets(doc, resFile, ref, issues); + + /* 6 - fixedUri/fixedCode against project CodeSystems */ + checkFixedCodings(doc, resFile, ref, issues); + return issues; } @@ -438,6 +450,201 @@ private void checkSliceCardinality(Document doc, } } } + + /* CHECK 5: BINDING VALUE SET REFERENCES */ + /** + * Validates that every {@code binding.valueSet} canonical URL referenced in the differential + * can be resolved to a {@code ValueSet} resource within the project. + * + *

When a differential element declares a {@code binding} with a {@code strength} and a + * {@code valueSet} URL, the referenced ValueSet must exist in the project (but it could be a foreign URL). If it does not: + *

    + *
  • An WARN is reported when {@code strength = "required"}
  • + *
  • An INFO is reported for all other strengths ({@code extensible}, {@code preferred}, {@code example})
  • + *
+ * + * @param doc the StructureDefinition DOM document + * @param file the original file (used for error messages) + * @param ref a human-readable reference derived from the file or resource URL + * @param out the list where linting results are appended + */ + private void checkBindingValueSets(Document doc, + File file, + String ref, + List out) { + NodeList elements = xp(doc, ELEMENTS_XP); + if (elements == null) return; + + File projectRoot = LintingUtils.getProjectRoot(file.toPath()); + FhirResourceLocator locator = FhirResourceLocator.create(projectRoot); + + for (int i = 0; i < elements.getLength(); i++) { + Node elem = elements.item(i); + String strength = val(elem, ".//*[local-name()='binding']/*[local-name()='strength']/@value"); + String valueSetUrl = val(elem, ".//*[local-name()='binding']/*[local-name()='valueSet']/@value"); + + if (blank(strength) || blank(valueSetUrl)) continue; + + String elemId = val(elem, "./@id"); + boolean found = locator.valueSetExists(valueSetUrl, projectRoot); + + if (!found) { + if ("required".equals(strength)) { + out.add(new FhirElementLintItem(LinterSeverity.WARN, + LintingType.STRUCTURE_DEFINITION_BINDING_VALUESET_UNRESOLVED, + file, ref, + "Element '" + elemId + "': binding.valueSet '" + valueSetUrl + + "' (strength=required) could not be resolved to a known ValueSet. Are you sure?")); + } else { + out.add(new FhirElementLintItem(LinterSeverity.INFO, + LintingType.STRUCTURE_DEFINITION_BINDING_VALUESET_UNRESOLVED_NON_REQUIRED, + file, ref, + "Element '" + elemId + "': binding.valueSet '" + valueSetUrl + + "' (strength=" + strength + ") could not be resolved to a known ValueSet.")); + } + } else { + out.add(ok(file, ref, + "element '" + elemId + "': binding.valueSet '" + valueSetUrl + "' resolved (OK)")); + } + } + } + + /* CHECK 6: fixedUri / fixedCode AGAINST PROJECT CODE SYSTEMS */ + /** + * Validates that every {@code fixedUri} declared on a {@code *.system} element within the + * differential resolves to a known CodeSystem in the project, and that the corresponding + * {@code fixedCode} (if present on the same slice's {@code *.code} element) is a code that + * actually exists in that CodeSystem. + * + *

Both checks are reported as {@link LinterSeverity#WARN} so that downstream consumers can + * distinguish them from hard structural errors.

+ * + * @param doc the StructureDefinition DOM document + * @param file the original file (used for error messages) + * @param ref a human-readable reference derived from the file or resource URL + * @param out the list where linting results are appended + */ + private void checkFixedCodings(Document doc, + File file, + String ref, + List out) { + // Key: direct parent path of the .system / .code element + // (e.g. "Task.output:data-set-status.type.coding" for both + // "...type.coding.system" and "...type.coding.code") + // This avoids collisions when a slice has multiple .system paths + // (e.g. "type.coding.system" AND "value[x].system"). + Map systemByParentPath = new HashMap<>(); + Map codeByParentPath = new HashMap<>(); + + try { + // Elements in the differential that carry fixedUri and whose @id contains ".system" + NodeList sysElems = (NodeList) XPathFactory.newInstance().newXPath() + .compile(DIFF_ELEM_XP + "[./*[local-name()='fixedUri'] and contains(@id,'.system')]") + .evaluate(doc, XPathConstants.NODESET); + for (int i = 0; i < sysElems.getLength(); i++) { + String id = val(sysElems.item(i), "./@id"); + String fixedUri = val(sysElems.item(i), "./*[local-name()='fixedUri']/@value"); + if (id != null && fixedUri != null) { + String parentPath = extractParentPath(id); + if (parentPath != null) systemByParentPath.put(parentPath, fixedUri); + } + } + + // Elements in the differential that carry fixedCode and whose @id contains ".code" + NodeList codeElems = (NodeList) XPathFactory.newInstance().newXPath() + .compile(DIFF_ELEM_XP + "[./*[local-name()='fixedCode'] and contains(@id,'.code')]") + .evaluate(doc, XPathConstants.NODESET); + for (int i = 0; i < codeElems.getLength(); i++) { + String id = val(codeElems.item(i), "./@id"); + String fixedCode = val(codeElems.item(i), "./*[local-name()='fixedCode']/@value"); + if (id != null && fixedCode != null) { + String parentPath = extractParentPath(id); + if (parentPath != null) codeByParentPath.put(parentPath, fixedCode); + } + } + } catch (Exception e) { + return; + } + + if (systemByParentPath.isEmpty()) return; + + for (Map.Entry entry : systemByParentPath.entrySet()) { + String parentPath = entry.getKey(); + String fixedUri = entry.getValue(); + String sliceRoot = extractSliceRoot(parentPath); + + if (!FhirAuthorizationCache.containsSystem(fixedUri)) { + out.add(new FhirElementLintItem(LinterSeverity.INFO, + LintingType.STRUCTURE_DEFINITION_FIXED_URI_CODESYSTEM_NOT_FOUND, + file, ref, + "Slice '" + sliceRoot + "' (" + parentPath + "): fixedUri='" + fixedUri + + "' not found in project resources. fixedCode validation is skipped.")); + continue; + } + + String fixedCode = codeByParentPath.get(parentPath); + if (fixedCode != null) { + if (FhirAuthorizationCache.isUnknown(fixedUri, fixedCode)) { + out.add(new FhirElementLintItem(LinterSeverity.ERROR, + LintingType.STRUCTURE_DEFINITION_FIXED_CODE_NOT_IN_CODESYSTEM, + file, ref, + "Slice '" + sliceRoot + "' (" + parentPath + "): fixedCode='" + fixedCode + + "' is not a known code in CodeSystem '" + fixedUri + "'.")); + } else { + out.add(ok(file, ref, "Slice '" + sliceRoot + "' (" + parentPath + "): fixedUri/fixedCode pair valid (OK)")); + } + } else { + out.add(ok(file, ref, "Slice '" + sliceRoot + "' (" + parentPath + "): fixedUri='" + fixedUri + "' is a known CodeSystem (OK)")); + } + } + } + + /** + * Returns the direct parent path of an element ID by removing the last path segment. + *

+ * Used to pair a {@code fixedUri} on a {@code *.system} element with the matching + * {@code fixedCode} on the sibling {@code *.code} element that shares the same parent. + *

+ * Examples: + *

    + *
  • {@code "Task.input:target-endpoints.type.coding.system"} → {@code "Task.input:target-endpoints.type.coding"}
  • + *
  • {@code "Task.output:data-set-status.value[x].system"} → {@code "Task.output:data-set-status.value[x]"}
  • + *
+ * + * @param elementId the full element ID string + * @return the parent path (everything before the last {@code '.'}), or {@code null} if not applicable + */ + private static String extractParentPath(String elementId) { + if (elementId == null) return null; + int lastDot = elementId.lastIndexOf('.'); + if (lastDot < 0) return null; + return elementId.substring(0, lastDot); + } + + /** + * Extracts the slice root from a StructureDefinition element ID. + *

+ * The slice root is the portion of the element ID up to (but not including) the first + * {@code '.'} character that appears after the slice discriminator {@code ':'}. + *

+ * Examples: + *

    + *
  • {@code "Task.input:target-endpoints.type.coding.system"} → {@code "Task.input:target-endpoints"}
  • + *
  • {@code "Task.output:ping-status.value[x].system"} → {@code "Task.output:ping-status"}
  • + *
+ * + * @param elementId the full element ID string + * @return the slice root, or {@code null} if the ID has no slice discriminator + */ + private static String extractSliceRoot(String elementId) { + if (elementId == null) return null; + int colon = elementId.indexOf(':'); + if (colon < 0) return null; + int dot = elementId.indexOf('.', colon + 1); + if (dot < 0) return null; + return elementId.substring(0, dot); + } + /* HELPERS */ /** * Resolves the canonical reference for issue reporting from the StructureDefinition. From 2b0e61b097766e7a9370625e5ffe2ba5e6744c0b Mon Sep 17 00:00:00 2001 From: khalilmalla95 Date: Tue, 21 Apr 2026 17:16:42 +0200 Subject: [PATCH 6/7] Add validation for Task.input fixedUri/fixedCode constraints in FhirTaskLinter --- .../dev/dsf/linter/fhir/FhirTaskLinter.java | 201 ++++++++++++++++++ 1 file changed, 201 insertions(+) diff --git a/linter-core/src/main/java/dev/dsf/linter/fhir/FhirTaskLinter.java b/linter-core/src/main/java/dev/dsf/linter/fhir/FhirTaskLinter.java index 304d400..180908b 100644 --- a/linter-core/src/main/java/dev/dsf/linter/fhir/FhirTaskLinter.java +++ b/linter-core/src/main/java/dev/dsf/linter/fhir/FhirTaskLinter.java @@ -214,6 +214,7 @@ public List lint(Document doc, File resFile) { checkPlaceholders(doc, resFile, ref, issues); lintTaskIdentifier(doc, resFile, ref, issues); lintInputs(doc, resFile, ref, issues); + lintFixedConstraints(doc, resFile, ref, issues); lintTerminology(doc, resFile, ref, issues); lintRequesterAuthorization(doc, resFile, ref, issues); lintRecipientAuthorization(doc, resFile, ref, issues); @@ -517,6 +518,200 @@ private void lintTerminology(Document doc, File f, String ref, ListValidation direction + *

The check is intentionally Task → StructureDefinition:

+ *
    + *
  1. Read all actual {@code (system, code)} pairs from {@code Task.input}.
  2. + *
  3. Verify each pair against allowed pairs from the referenced StructureDefinition.
  4. + *
+ * + *

What is checked

+ *
    + *
  1. If an actual Task input code exists in SD constraints but with a different system + * → {@link LintingType#FHIR_TASK_INPUT_FIXED_URI_MISMATCH}
  2. + *
  3. If an actual Task input system exists in SD constraints but with a different code + * → {@link LintingType#FHIR_TASK_INPUT_FIXED_CODE_MISMATCH}
  4. + *
  5. If an actual Task input {@code (system, code)} pair is not defined by any SD + * fixedUri/fixedCode constraint (excluding BPMN message inputs) + * → {@link LintingType#FHIR_TASK_INPUT_PAIR_NOT_ALLOWED_BY_SD}
  6. + *
+ * + * @param doc the Task DOM document + * @param f the Task resource file + * @param ref a human-readable reference (e.g. instantiatesCanonical) + * @param out the list where linting results are appended + */ + private void lintFixedConstraints(Document doc, File f, String ref, List out) { + String profileUrl = val(doc, TASK_XP + "/*[local-name()='meta']/*[local-name()='profile']/@value"); + if (blank(profileUrl)) return; + + List constraints = loadFixedCodingConstraints(determineProjectRoot(f), profileUrl); + if (constraints.isEmpty()) return; + + NodeList ins = xp(doc, INPUT_XP); + if (ins == null || ins.getLength() == 0) return; + + List actualPairs = new ArrayList<>(); + for (int i = 0; i < ins.getLength(); i++) { + Node in = ins.item(i); + String sys = val(in, CODING_SYS_XP); + String code = val(in, CODING_CODE_XP); + if (!blank(sys) || !blank(code)) { + actualPairs.add(new String[]{sys, code}); + } + } + + Map> expectedSystemsByCode = new HashMap<>(); + Map> expectedCodesBySystem = new HashMap<>(); + Set allowedPairs = new HashSet<>(); + for (FixedCoding constraint : constraints) { + expectedSystemsByCode + .computeIfAbsent(constraint.code(), k -> new HashSet<>()) + .add(constraint.system()); + expectedCodesBySystem + .computeIfAbsent(constraint.system(), k -> new HashSet<>()) + .add(constraint.code()); + allowedPairs.add(constraint.system() + "#" + constraint.code()); + } + + Set reported = new HashSet<>(); + for (String[] actual : actualPairs) { + String actualSys = actual[0]; + String actualCode = actual[1]; + if (blank(actualSys) || blank(actualCode)) continue; + if (allowedPairs.contains(actualSys + "#" + actualCode)) continue; + + boolean handled = false; + + Set expectedSystems = expectedSystemsByCode.get(actualCode); + if (expectedSystems != null && !expectedSystems.contains(actualSys)) { + String key = "uri|" + actualSys + "|" + actualCode; + if (reported.add(key)) { + out.add(new FhirElementLintItem(LinterSeverity.ERROR, + LintingType.FHIR_TASK_INPUT_FIXED_URI_MISMATCH, f, ref, + "Task.input with code='" + actualCode + "': system='" + actualSys + + "' does not match expected fixedUri(s)=" + expectedSystems + ".")); + } + handled = true; + } + + if (!handled) { + Set expectedCodes = expectedCodesBySystem.get(actualSys); + if (expectedCodes != null && !expectedCodes.contains(actualCode)) { + String key = "code|" + actualSys + "|" + actualCode; + if (reported.add(key)) { + out.add(new FhirElementLintItem(LinterSeverity.ERROR, + LintingType.FHIR_TASK_INPUT_FIXED_CODE_MISMATCH, f, ref, + "Task.input with system='" + actualSys + "': code='" + actualCode + + "' does not match expected fixedCode(s)=" + expectedCodes + ".")); + } + handled = true; + } + } + + // pair is completely unrecognized by the SD's fixedUri/fixedCode constraints. + // Exclude bpmn-message inputs as those are validated separately in lintInputs(). + if (!handled && !SYSTEM_BPMN_MSG.equals(actualSys)) { + String key = "unallowed|" + actualSys + "|" + actualCode; + if (reported.add(key)) { + out.add(new FhirElementLintItem(LinterSeverity.ERROR, + LintingType.FHIR_TASK_INPUT_PAIR_NOT_ALLOWED_BY_SD, f, ref, + "Task.input pair (system='" + actualSys + "', code='" + actualCode + + "') is not defined by any fixedUri/fixedCode constraint in the StructureDefinition.")); + } + } + } + } + + /** + * Parses the StructureDefinition identified by {@code profileUrl} and extracts all + * {@code (sliceName, fixedUri, fixedCode)} triples from {@code Task.input} slice elements. + * + *

Only slices that declare both a {@code fixedUri} on the + * {@code Task.input:sliceName.type.coding.system} element and a {@code fixedCode} on the + * {@code Task.input:sliceName.type.coding.code} element are included in the result.

+ * + * @param projectRoot the project root used to locate the StructureDefinition file + * @param profileUrl the canonical URL of the Task profile + * @return an unmodifiable list of {@link FixedCoding} constraints; empty if the SD cannot + * be loaded or no matching constraints are found + */ + private List loadFixedCodingConstraints(File projectRoot, String profileUrl) { + FhirResourceLocator locator = FhirResourceLocator.create(projectRoot); + File sdFile = locator.findStructureDefinitionFile(profileUrl, projectRoot); + if (sdFile == null) return List.of(); + + Document sd; + try { + try { sd = FhirResourceParser.parseXml(sdFile.toPath()); } + catch (Exception e) { sd = FhirResourceParser.parseJsonToXml(sdFile.toPath()); } + } catch (Exception e) { + return List.of(); + } + + Map systemBySlice = new HashMap<>(); + Map codeBySlice = new HashMap<>(); + + try { + NodeList sysElems = (NodeList) XPathFactory.newInstance().newXPath() + .compile("//*[local-name()='element'" + + " and starts-with(@id,'Task.input:')" + + " and contains(@id,'.type.coding.system')]") + .evaluate(sd, XPathConstants.NODESET); + for (int i = 0; i < sysElems.getLength(); i++) { + String id = sysElems.item(i).getAttributes().getNamedItem("id").getNodeValue(); + String fixedUri = AbstractFhirInstanceLinter.extractSingleNodeValue( + sysElems.item(i), "./*[local-name()='fixedUri']/@value"); + if (fixedUri != null) { + String sliceName = extractSliceName(id); + if (sliceName != null) systemBySlice.put(sliceName, fixedUri); + } + } + + NodeList codeElems = (NodeList) XPathFactory.newInstance().newXPath() + .compile("//*[local-name()='element'" + + " and starts-with(@id,'Task.input:')" + + " and contains(@id,'.type.coding.code')]") + .evaluate(sd, XPathConstants.NODESET); + for (int i = 0; i < codeElems.getLength(); i++) { + String id = codeElems.item(i).getAttributes().getNamedItem("id").getNodeValue(); + String fixedCode = AbstractFhirInstanceLinter.extractSingleNodeValue( + codeElems.item(i), "./*[local-name()='fixedCode']/@value"); + if (fixedCode != null) { + String sliceName = extractSliceName(id); + if (sliceName != null) codeBySlice.put(sliceName, fixedCode); + } + } + } catch (Exception e) { + return List.of(); + } + + return systemBySlice.entrySet().stream() + .filter(e -> codeBySlice.containsKey(e.getKey())) + .map(e -> new FixedCoding(e.getKey(), e.getValue(), codeBySlice.get(e.getKey()))) + .toList(); + } + + /** + * Extracts the slice name from a StructureDefinition element ID of the form + * {@code Task.input:sliceName.some.path}. + * + * @param elementId the full element ID string + * @return the slice name, or {@code null} if the ID does not match the expected pattern + */ + private static String extractSliceName(String elementId) { + final String prefix = "Task.input:"; + if (elementId == null || !elementId.startsWith(prefix)) return null; + String afterColon = elementId.substring(prefix.length()); + int dot = afterColon.indexOf('.'); + if (dot < 0) return null; + return afterColon.substring(0, dot); + } + private String computeReference(Document doc, File file) { String canon = val(doc, TASK_XP + "/*[local-name()='instantiatesCanonical']/@value"); if (!blank(canon)) return canon.split("\\|")[0]; @@ -559,6 +754,12 @@ private boolean instCanonDetermine(Document taskDoc, File taskFile) { return actFile == null; } + /** + * Holds a (system, code) pair extracted from {@code fixedUri} / {@code fixedCode} constraints + * in a StructureDefinition slice definition. + */ + private record FixedCoding(String sliceName, String system, String code) {} + private record SliceCard(int min, int max) {} private Map loadInputCardinality(File projectRoot, String profileUrl) { From 871a00f14e413d8e90a799524a5a13711d3ed3f0 Mon Sep 17 00:00:00 2001 From: khalilmalla95 Date: Wed, 22 Apr 2026 12:43:35 +0200 Subject: [PATCH 7/7] Change lint severity from INFO to WARN for fixedUri/CodeSystem not found in FhirStructureDefinitionLinter --- .../java/dev/dsf/linter/fhir/FhirStructureDefinitionLinter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/linter-core/src/main/java/dev/dsf/linter/fhir/FhirStructureDefinitionLinter.java b/linter-core/src/main/java/dev/dsf/linter/fhir/FhirStructureDefinitionLinter.java index ba064d1..3fb99e3 100644 --- a/linter-core/src/main/java/dev/dsf/linter/fhir/FhirStructureDefinitionLinter.java +++ b/linter-core/src/main/java/dev/dsf/linter/fhir/FhirStructureDefinitionLinter.java @@ -574,7 +574,7 @@ private void checkFixedCodings(Document doc, String sliceRoot = extractSliceRoot(parentPath); if (!FhirAuthorizationCache.containsSystem(fixedUri)) { - out.add(new FhirElementLintItem(LinterSeverity.INFO, + out.add(new FhirElementLintItem(LinterSeverity.WARN, LintingType.STRUCTURE_DEFINITION_FIXED_URI_CODESYSTEM_NOT_FOUND, file, ref, "Slice '" + sliceRoot + "' (" + parentPath + "): fixedUri='" + fixedUri