diff --git a/input/resources/Questionnaire-ACP-zib2020.json b/input/resources/Questionnaire-ACP-zib2020.json index af32add..ac50f65 100644 --- a/input/resources/Questionnaire-ACP-zib2020.json +++ b/input/resources/Questionnaire-ACP-zib2020.json @@ -2,15 +2,15 @@ "resourceType": "Questionnaire", "id": "ACP-zib2020", "url": "https://api.iknl.nl/docs/pzp/r4/Questionnaire/ACP-zib2020", - "version": "1.0.0-rc2", + "version": "1.0.0-rc3", "name": "ACPzib2020", - "title": "Uniform vastleggen proactieve zorgpanning (PZP) o.b.v. zibs2020 - ReleaseCandidate2 03-03-2026", + "title": "Uniform vastleggen proactieve zorgpanning (PZP) o.b.v. zibs2020 - ReleaseCandidate3 02-06-2026", "status": "draft", "experimental": false, "publisher": "Published by PZNL & executed by IKNL", - "copyright": "This form is subject to copyright, user rights and a disclaimer, as specified for all IKNL information standards. For details, see the paragraph on Gebruikersrechten en disclaimer at https://iknl.nl/onderzoek/eenheid-van-taal.", + "description": "This form was developed to clearly document agreements resulting from the advance care planning (ACP) process. It is NOT a checklist. It can only be completed by a healthcare provider after a professional and nuanced conversation. For advice on conducting these conversations, please refer to the guideline for proactive care planning in the palliative phase and Palliaweb, see https://palliaweb.nl/zorgpraktijk/proactieve-zorgplanning. \nEnter 'unknown' if a topic is not discussed or if the patient does not (yet) have an opinion.When transferring to a long-term care setting, consider adding conversation records about advance care planning (ACP) to the transfer documents.", "purpose": "This form was developed to clearly document agreements resulting from the advance care planning (ACP) process.", - "description": "This form was developed to clearly document agreements resulting from the advance care planning (ACP) process. It is NOT a checklist. It can only be completed by a healthcare provider after a professional and nuanced conversation. For advice on conducting these conversations, please refer to the guideline for advance care planning in the palliative phase and Palliaweb, see https://palliaweb.nl/zorgpraktijk/proactieve-zorgplanning. \nEnter 'unknown' if a topic is not discussed or if the patient does not (yet) have an opinion.When transferring to a long-term care setting, consider adding conversation records about advance care planning (ACP) to the transfer documents.", + "copyright": "This form is subject to copyright, user rights and a disclaimer, as specified for all IKNL information standards. For details, see the paragraph on Gebruikersrechten en disclaimer at https://iknl.nl/onderzoek/eenheid-van-taal.", "item": [ { "linkId": "963", @@ -51,7 +51,7 @@ { "linkId": "967", "text": "Geboortedatum patiënt", - "type": "date", + "type": "dateTime", "required": false, "repeats": false }, @@ -96,50 +96,7 @@ "type": "choice", "required": false, "repeats": false, - "answerOption": [ - { - "valueCoding": { - "system": "http://fhir.nl/fhir/NamingSystem/uzi-rolcode", - "code": "01.000", - "display": "Arts" - } - }, - { - "valueCoding": { - "system": "http://fhir.nl/fhir/NamingSystem/uzi-rolcode", - "code": "01.015", - "display": "Huisarts" - } - }, - { - "valueCoding": { - "system": "http://fhir.nl/fhir/NamingSystem/uzi-rolcode", - "code": "01.047", - "display": "Specialist ouderengeneeskunde" - } - }, - { - "valueCoding": { - "system": "http://fhir.nl/fhir/NamingSystem/uzi-rolcode", - "code": "30.000", - "display": "Verpleegkundige" - } - }, - { - "valueCoding": { - "system": "http://fhir.nl/fhir/NamingSystem/uzi-rolcode", - "code": "81.000", - "display": "Physician assistant" - } - }, - { - "valueCoding": { - "system": "http://fhir.nl/fhir/NamingSystem/uzi-rolcode", - "code": "99.000", - "display": "Zorgverlener andere zorg" - } - } - ] + "answerValueSet": "http://decor.nictiz.nl/fhir/ValueSet/2.16.840.1.113883.2.4.3.11.60.121.11.22--20200901000000" } ] }, @@ -151,7 +108,7 @@ "repeats": false, "item": [ { - "linkId": "1406", + "linkId": "1651", "text": "Is de patiënt op dit moment wilsbekwaam m.b.t. medische behandelbeslissingen?", "type": "boolean", "required": false, @@ -611,15 +568,15 @@ { "valueCoding": { "system": "http://terminology.hl7.org/CodeSystem/v3-AddressUse", - "code": "MC", - "display": "Mobiel telefoonnummer" + "code": "PG", + "display": "Pieper" } }, { "valueCoding": { "system": "http://terminology.hl7.org/CodeSystem/v3-AddressUse", - "code": "PG", - "display": "Pieper" + "code": "MC", + "display": "Mobiel telefoonnummer" } } ] @@ -865,148 +822,168 @@ ] }, { - "linkId": "997", - "text": "Rol", - "type": "choice", - "required": false, - "repeats": true, - "answerOption": [ - { - "valueCoding": { - "system": "urn:oid:2.16.840.1.113883.2.4.3.11.22.472", - "code": "03", - "display": "Curator (juridisch)" - } - }, - { - "valueCoding": { - "system": "urn:oid:2.16.840.1.113883.2.4.3.11.22.472", - "code": "15", - "display": "Mentor" - } - }, - { - "valueCoding": { - "system": "http://snomed.info/sct", - "code": "310141000146103", - "display": "Schriftelijk gemachtigde" - } - }, - { - "valueCoding": { - "system": "urn:oid:2.16.840.1.113883.2.4.3.11.22.472", - "code": "09", - "display": "Anders" - } - } - ], + "linkId": "1652", + "text": "Contactperso(o)n(en)", + "type": "group", + "repeats": false, "item": [ { - "linkId": "414", - "text": "Anders, namelijk:", - "type": "string", + "linkId": "997", + "text": "Rol", + "type": "choice", "required": false, - "repeats": false - } - ] - }, - { - "linkId": "998", - "text": "Relatie tot patiënt", - "type": "choice", - "required": false, - "repeats": true, - "answerOption": [ - { - "valueCoding": { - "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", - "code": "DOMPART", - "display": "Partner" - } - }, - { - "valueCoding": { - "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", - "code": "HUSB", - "display": "Echtgenoot" - } - }, - { - "valueCoding": { - "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", - "code": "WIFE", - "display": "Echtgenote" - } - }, - { - "valueCoding": { - "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", - "code": "FTH", - "display": "Vader" - } - }, - { - "valueCoding": { - "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", - "code": "MTH", - "display": "Moeder" - } - }, - { - "valueCoding": { - "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", - "code": "SONC", - "display": "Zoon" - } - }, - { - "valueCoding": { - "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", - "code": "DAUC", - "display": "Dochter" - } - }, - { - "valueCoding": { - "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", - "code": "BRO", - "display": "Broer" - } - }, - { - "valueCoding": { - "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", - "code": "SIS", - "display": "Zuster" - } + "repeats": true, + "answerOption": [ + { + "valueCoding": { + "system": "urn:oid:2.16.840.1.113883.2.4.3.11.22.472", + "code": "03", + "display": "Curator (juridisch)" + } + }, + { + "valueCoding": { + "system": "urn:oid:2.16.840.1.113883.2.4.3.11.22.472", + "code": "15", + "display": "Mentor" + } + }, + { + "valueCoding": { + "system": "http://snomed.info/sct", + "code": "310141000146103", + "display": "Schriftelijk gemachtigde" + } + }, + { + "valueCoding": { + "system": "urn:oid:2.16.840.1.113883.2.4.3.11.22.472", + "code": "09", + "display": "Anders" + } + } + ], + "item": [ + { + "linkId": "414", + "text": "Anders, namelijk:", + "type": "string", + "enableWhen": [ + { + "question": "997", + "operator": "=", + "answerCoding": { + "system": "urn:oid:2.16.840.1.113883.2.4.3.11.22.472", + "code": "09", + "display": "Anders" + } + } + ], + "enableBehavior": "any", + "required": false, + "repeats": false + } + ] }, { - "valueCoding": { - "system": "http://terminology.hl7.org/CodeSystem/v3-NullFlavor", - "code": "OTH", - "display": "Anders" - } - } - ], - "item": [ - { - "linkId": "413", - "text": "Anders, namelijk:", - "type": "string", - "enableWhen": [ + "linkId": "998", + "text": "Relatie tot patiënt", + "type": "choice", + "required": false, + "repeats": true, + "answerOption": [ { - "question": "998", - "operator": "=", - "answerCoding": { + "valueCoding": { + "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", + "code": "DOMPART", + "display": "Partner" + } + }, + { + "valueCoding": { + "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", + "code": "HUSB", + "display": "Echtgenoot" + } + }, + { + "valueCoding": { + "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", + "code": "WIFE", + "display": "Echtgenote" + } + }, + { + "valueCoding": { + "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", + "code": "FTH", + "display": "Vader" + } + }, + { + "valueCoding": { + "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", + "code": "MTH", + "display": "Moeder" + } + }, + { + "valueCoding": { + "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", + "code": "SONC", + "display": "Zoon" + } + }, + { + "valueCoding": { + "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", + "code": "DAUC", + "display": "Dochter" + } + }, + { + "valueCoding": { + "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", + "code": "BRO", + "display": "Broer" + } + }, + { + "valueCoding": { + "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", + "code": "SIS", + "display": "Zuster" + } + }, + { + "valueCoding": { "system": "http://terminology.hl7.org/CodeSystem/v3-NullFlavor", "code": "OTH", "display": "Anders" } } ], - "enableBehavior": "any", - "required": false, - "repeats": false + "item": [ + { + "linkId": "413", + "text": "Anders, namelijk:", + "type": "string", + "enableWhen": [ + { + "question": "998", + "operator": "=", + "answerCoding": { + "system": "http://terminology.hl7.org/CodeSystem/v3-NullFlavor", + "code": "OTH", + "display": "Anders" + } + } + ], + "enableBehavior": "any", + "required": false, + "repeats": false + } + ] } ] } @@ -1043,36 +1020,7 @@ "type": "choice", "required": false, "repeats": false, - "answerOption": [ - { - "valueCoding": { - "system": "http://snomed.info/sct", - "code": "385987000", - "display": "Curatief / actief ziektebeleid" - } - }, - { - "valueCoding": { - "system": "http://snomed.info/sct", - "code": "1351964001", - "display": "Palliatief met als doel levensverlenging én symptoomverlichting" - } - }, - { - "valueCoding": { - "system": "http://snomed.info/sct", - "code": "225353007", - "display": "Palliatief met als doel symptoomverlichting, waarbij levensverlenging niet gewenst is" - } - }, - { - "valueCoding": { - "system": "http://terminology.hl7.org/CodeSystem/v3-NullFlavor", - "code": "UNK", - "display": "Nog onbekend" - } - } - ] + "answerValueSet": "https://api.iknl.nl/docs/pzp/r4/ValueSet/ACP-MedicalPolicyGoal" } ] }, @@ -1654,10 +1602,19 @@ "valueCoding": { "system": "http://terminology.hl7.org/CodeSystem/v3-NullFlavor", "code": "OTH", - "display": "Other" + "display": "Anders" }, "initialSelected": true } + ], + "item": [ + { + "linkId": "1645", + "text": "Anders, namelijk:", + "type": "string", + "required": false, + "repeats": false + } ] }, { @@ -1708,29 +1665,7 @@ "type": "choice", "required": false, "repeats": false, - "answerOption": [ - { - "valueCoding": { - "system": "http://snomed.info/sct", - "code": "373066001", - "display": "Ja" - } - }, - { - "valueCoding": { - "system": "http://snomed.info/sct", - "code": "373067005", - "display": "Nee" - } - }, - { - "valueCoding": { - "system": "http://terminology.hl7.org/CodeSystem/v3-NullFlavor", - "code": "UNK", - "display": "Nog onbekend" - } - } - ], + "answerValueSet": "https://api.iknl.nl/docs/pzp/r4/ValueSet/ACP-YesNoUnknownVS", "item": [ { "linkId": "1008", @@ -1743,57 +1678,14 @@ "answerCoding": { "system": "http://snomed.info/sct", "code": "373066001", - "display": "Ja" + "display": "ja" } } ], "enableBehavior": "any", "required": false, "repeats": false, - "answerOption": [ - { - "valueCoding": { - "system": "http://snomed.info/sct", - "code": "72506001", - "display": "implanteerbare cardioverter-defibrillator" - } - }, - { - "valueCoding": { - "system": "http://snomed.info/sct", - "code": "465460004", - "display": "univentriculaire implanteerbare cardioverter-defibrillator" - } - }, - { - "valueCoding": { - "system": "http://snomed.info/sct", - "code": "468542000", - "display": "implanteerbare tweekamercardioverter-defibrillator" - } - }, - { - "valueCoding": { - "system": "http://snomed.info/sct", - "code": "704707009", - "display": "implanteerbare biventriculaire cardioverter-defibrillator" - } - }, - { - "valueCoding": { - "system": "http://snomed.info/sct", - "code": "1263462004", - "display": "pulsgenerator van defibrillator voor cardiale resynchronisatietherapie" - } - }, - { - "valueCoding": { - "system": "http://snomed.info/sct", - "code": "1236894001", - "display": "subcutane implanteerbare cardioverter-defibrillator" - } - } - ] + "answerValueSet": "https://api.iknl.nl/docs/pzp/r4/ValueSet/ACP-MedicalDeviceProductType-ICD" }, { "linkId": "1009", @@ -1806,7 +1698,7 @@ "answerCoding": { "system": "http://snomed.info/sct", "code": "373066001", - "display": "Ja" + "display": "ja" } } ], @@ -1828,7 +1720,7 @@ "answerCoding": { "system": "http://snomed.info/sct", "code": "373066001", - "display": "Ja" + "display": "ja" } } ], @@ -1864,7 +1756,7 @@ "answerCoding": { "system": "http://snomed.info/sct", "code": "373066001", - "display": "Ja" + "display": "ja" } } ], @@ -1946,6 +1838,24 @@ "type": "string", "required": false, "repeats": false + }, + { + "linkId": "1648", + "text": "Vaststellen wens en verwachting patiënt ([MeetMethode])", + "type": "choice", + "required": false, + "repeats": false, + "readOnly": true, + "answerOption": [ + { + "valueCoding": { + "system": "http://snomed.info/sct", + "code": "370819000", + "display": "Vaststellen van persoonlijke waarden en wensen met betrekking tot zorg" + }, + "initialSelected": true + } + ] } ] }, @@ -1957,7 +1867,7 @@ "repeats": false, "item": [ { - "linkId": "1430", + "linkId": "1649", "text": "Gewenste plek van overlijden ([MetingNaam])", "type": "choice", "required": false, @@ -1968,7 +1878,7 @@ "valueCoding": { "system": "http://snomed.info/sct", "code": "395091006", - "display": "Gewenste plek van overlijden" + "display": "Voorkeur voor plaats van overlijden (waarneembare entiteit)" }, "initialSelected": true } @@ -1980,50 +1890,7 @@ "type": "choice", "required": false, "repeats": false, - "answerOption": [ - { - "valueCoding": { - "system": "http://snomed.info/sct", - "code": "264362003", - "display": "Thuis" - } - }, - { - "valueCoding": { - "system": "http://snomed.info/sct", - "code": "22232009", - "display": "Ziekenhuis" - } - }, - { - "valueCoding": { - "system": "http://snomed.info/sct", - "code": "108344006", - "display": "Verpleeghuis" - } - }, - { - "valueCoding": { - "system": "http://snomed.info/sct", - "code": "284546000", - "display": "Hospice" - } - }, - { - "valueCoding": { - "system": "http://terminology.hl7.org/CodeSystem/v3-NullFlavor", - "code": "OTH", - "display": "Anders" - } - }, - { - "valueCoding": { - "system": "http://terminology.hl7.org/CodeSystem/v3-NullFlavor", - "code": "UNK", - "display": "Nog onbekend" - } - } - ], + "answerValueSet": "https://api.iknl.nl/docs/pzp/r4/ValueSet/ACP-PreferredPlaceOfDeath", "item": [ { "linkId": "1192", @@ -2067,36 +1934,7 @@ "type": "choice", "required": false, "repeats": false, - "answerOption": [ - { - "valueCoding": { - "system": "http://snomed.info/sct", - "code": "340181000146102", - "display": "Heeft euthanasieverklaring" - } - }, - { - "valueCoding": { - "system": "http://snomed.info/sct", - "code": "340201000146103", - "display": "Wenst geen euthanasie" - } - }, - { - "valueCoding": { - "system": "http://snomed.info/sct", - "code": "340191000146100", - "display": "Geen euthanasieverklaring, zou wel verzoek kunnen doen in bepaalde situaties" - } - }, - { - "valueCoding": { - "system": "http://terminology.hl7.org/CodeSystem/v3-NullFlavor", - "code": "UNK", - "display": "Nog onbekend" - } - } - ], + "answerValueSet": "https://api.iknl.nl/docs/pzp/r4/ValueSet/ACP-PositionRegardingEuthanasia", "item": [ { "linkId": "1194", @@ -2140,27 +1978,23 @@ "type": "choice", "required": false, "repeats": false, + "answerValueSet": "https://api.iknl.nl/docs/pzp/r4/ValueSet/ACP-YesNoUnknownVS" + }, + { + "linkId": "1650", + "text": "Keuze orgaandonatie vastgelegd in donorregister? ([MeetMethode])", + "type": "choice", + "required": false, + "repeats": false, + "readOnly": true, "answerOption": [ { "valueCoding": { "system": "http://snomed.info/sct", - "code": "373066001", - "display": "Ja" - } - }, - { - "valueCoding": { - "system": "http://snomed.info/sct", - "code": "373067005", - "display": "Nee" - } - }, - { - "valueCoding": { - "system": "http://terminology.hl7.org/CodeSystem/v3-NullFlavor", - "code": "UNK", - "display": "Nog onbekend" - } + "code": "1156040003", + "display": "Self reported (qualifier value)" + }, + "initialSelected": true } ] } @@ -2216,29 +2050,7 @@ "type": "choice", "required": false, "repeats": false, - "answerOption": [ - { - "valueCoding": { - "system": "http://snomed.info/sct", - "code": "373066001", - "display": "Ja" - } - }, - { - "valueCoding": { - "system": "http://snomed.info/sct", - "code": "373067005", - "display": "Nee" - } - }, - { - "valueCoding": { - "system": "http://terminology.hl7.org/CodeSystem/v3-NullFlavor", - "code": "UNK", - "display": "Nog onbekend" - } - } - ], + "answerValueSet": "https://api.iknl.nl/docs/pzp/r4/ValueSet/ACP-YesNoUnknownVS", "item": [ { "linkId": "1198", @@ -2261,36 +2073,14 @@ "answerCoding": { "system": "http://snomed.info/sct", "code": "373066001", - "display": "Ja" + "display": "ja" } } ], "enableBehavior": "any", "required": false, "repeats": false, - "answerOption": [ - { - "valueCoding": { - "system": "http://snomed.info/sct", - "code": "373066001", - "display": "Ja" - } - }, - { - "valueCoding": { - "system": "http://snomed.info/sct", - "code": "373067005", - "display": "Nee" - } - }, - { - "valueCoding": { - "system": "http://terminology.hl7.org/CodeSystem/v3-NullFlavor", - "code": "UNK", - "display": "Nog onbekend" - } - } - ] + "answerValueSet": "https://api.iknl.nl/docs/pzp/r4/ValueSet/ACP-YesNoUnknownVS" } ] }, @@ -2336,4 +2126,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/input/resources/QuestionnaireResponse-HendrikHartman-20201001.json b/input/resources/QuestionnaireResponse-HendrikHartman-20201001.json index 0263425..f1c39b5 100644 --- a/input/resources/QuestionnaireResponse-HendrikHartman-20201001.json +++ b/input/resources/QuestionnaireResponse-HendrikHartman-20201001.json @@ -4,13 +4,13 @@ "meta": { "tag": [ { - "code": "lformsVersion: 38.2.0" + "code": "lformsVersion: 42.2.0" } ] }, "status": "completed", "authored": "2025-08-25T19:14:50.150Z", - "questionnaire": "https://api.iknl.nl/docs/pzp/r4/Questionnaire/ACP-zib2020|1.0.0-rc2", + "questionnaire": "https://api.iknl.nl/docs/pzp/r4/Questionnaire/ACP-zib2020|1.0.0-rc3", "subject": { "reference": "Patient/ACP-Patient-HendrikHartman-Pat1", "display": "Patient, Hendrik Hartman" @@ -121,7 +121,7 @@ "valueBoolean": true } ], - "linkId": "1406", + "linkId": "1651", "text": "Is de patiënt op dit moment wilsbekwaam m.b.t. medische behandelbeslissingen?" }, { @@ -173,9 +173,9 @@ "answer": [ { "valueCoding": { - "system": "http://terminology.hl7.org/CodeSystem/v3-AddressUse", - "code": "HP", - "display": "Privé e-mailadres" + "system": "http://hl7.org/fhir/contact-point-use", + "code": "home", + "display": "Home" } } ], @@ -186,6 +186,19 @@ } ] }, + { + "linkId": "982", + "text": "Relatie tot patiënt (1)", + "answer": [ + { + "valueCoding": { + "system": "urn:oid:2.16.840.1.113883.2.4.3.11.22.472", + "code": "24", + "display": "Wettelijke vertegenwoordiger" + } + } + ] + }, { "answer": [ { @@ -229,15 +242,21 @@ "text": "Patiënt" }, { - "linkId": "998", - "text": "Relatie tot patiënt", - "answer": [ + "linkId": "1652", + "text": "Contactperso(o)n(en)", + "item": [ { - "valueCoding": { - "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", - "code": "BRO", - "display": "Broer" - } + "linkId": "998", + "text": "Relatie tot patiënt", + "answer": [ + { + "valueCoding": { + "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", + "code": "BRO", + "display": "Broer" + } + } + ] } ] } @@ -266,7 +285,7 @@ "valueCoding": { "system": "http://snomed.info/sct", "code": "1351964001", - "display": "Palliatief met als doel levensverlenging én symptoomverlichting" + "display": "levensverlengende behandeling" } } ], @@ -481,7 +500,7 @@ "valueCoding": { "system": "http://terminology.hl7.org/CodeSystem/v3-NullFlavor", "code": "OTH", - "display": "Other" + "display": "Anders" } } ], @@ -520,7 +539,7 @@ "valueCoding": { "system": "http://snomed.info/sct", "code": "373066001", - "display": "Ja" + "display": "ja" }, "item": [ { @@ -625,6 +644,19 @@ ], "linkId": "1190", "text": "Wens en verwachting patient ([MetingWaarde])" + }, + { + "answer": [ + { + "valueCoding": { + "system": "http://snomed.info/sct", + "code": "370819000", + "display": "Vaststellen van persoonlijke waarden en wensen met betrekking tot zorg" + } + } + ], + "linkId": "1648", + "text": "Vaststellen wens en verwachting patiënt ([MeetMethode])" } ] }, @@ -638,11 +670,11 @@ "valueCoding": { "system": "http://snomed.info/sct", "code": "395091006", - "display": "Gewenste plek van overlijden" + "display": "Voorkeur voor plaats van overlijden (waarneembare entiteit)" } } ], - "linkId": "1430", + "linkId": "1649", "text": "Gewenste plek van overlijden ([MetingNaam])" }, { @@ -694,7 +726,7 @@ "valueCoding": { "system": "http://terminology.hl7.org/CodeSystem/v3-NullFlavor", "code": "UNK", - "display": "Nog onbekend" + "display": "onbekend" }, "item": [ { @@ -737,12 +769,25 @@ "valueCoding": { "system": "http://snomed.info/sct", "code": "373066001", - "display": "Ja" + "display": "ja" } } ], "linkId": "1435", "text": "Keuze orgaandonatie in donorregister ([MetingWaarde])" + }, + { + "answer": [ + { + "valueCoding": { + "system": "http://snomed.info/sct", + "code": "1156040003", + "display": "Self reported (qualifier value)" + } + } + ], + "linkId": "1650", + "text": "Keuze orgaandonatie vastgelegd in donorregister? ([MeetMethode])" } ] } @@ -786,7 +831,7 @@ "valueCoding": { "system": "http://snomed.info/sct", "code": "373067005", - "display": "Nee" + "display": "nee" } } ], diff --git a/input/resources/QuestionnaireResponse-HendrikHartman-20221108.json b/input/resources/QuestionnaireResponse-HendrikHartman-20221108.json index f459957..3c2e052 100644 --- a/input/resources/QuestionnaireResponse-HendrikHartman-20221108.json +++ b/input/resources/QuestionnaireResponse-HendrikHartman-20221108.json @@ -4,13 +4,13 @@ "meta": { "tag": [ { - "code": "lformsVersion: 38.2.0" + "code": "lformsVersion: 42.2.0" } ] }, "status": "completed", "authored": "2025-08-25T19:18:32.253Z", - "questionnaire": "https://api.iknl.nl/docs/pzp/r4/Questionnaire/ACP-zib2020|1.0.0-rc2", + "questionnaire": "https://api.iknl.nl/docs/pzp/r4/Questionnaire/ACP-zib2020|1.0.0-rc3", "subject": { "reference": "Patient/ACP-Patient-HendrikHartman-Pat1", "display": "Patient, Hendrik Hartman" @@ -121,7 +121,7 @@ "valueBoolean": true } ], - "linkId": "1406", + "linkId": "1651", "text": "Is de patiënt op dit moment wilsbekwaam m.b.t. medische behandelbeslissingen?" }, { @@ -173,9 +173,9 @@ "answer": [ { "valueCoding": { - "system": "http://terminology.hl7.org/CodeSystem/v3-AddressUse", - "code": "HP", - "display": "Privé e-mailadres" + "system": "http://hl7.org/fhir/contact-point-use", + "code": "home", + "display": "Home" } } ], @@ -186,6 +186,19 @@ } ] }, + { + "linkId": "982", + "text": "Relatie tot patiënt (1)", + "answer": [ + { + "valueCoding": { + "system": "urn:oid:2.16.840.1.113883.2.4.3.11.22.472", + "code": "24", + "display": "Wettelijke vertegenwoordiger" + } + } + ] + }, { "answer": [ { @@ -229,15 +242,21 @@ "text": "Patiënt" }, { - "linkId": "998", - "text": "Relatie tot patiënt", - "answer": [ + "linkId": "1652", + "text": "Contactperso(o)n(en)", + "item": [ { - "valueCoding": { - "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", - "code": "BRO", - "display": "Broer" - } + "linkId": "998", + "text": "Relatie tot patiënt", + "answer": [ + { + "valueCoding": { + "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", + "code": "BRO", + "display": "Broer" + } + } + ] } ] } @@ -265,8 +284,8 @@ { "valueCoding": { "system": "http://snomed.info/sct", - "code": "225353007", - "display": "Palliatief met als doel symptoomverlichting, waarbij levensverlenging niet gewenst is" + "code": "713148004", + "display": "voorkomen en behandelen van symptomen" } } ], @@ -514,7 +533,7 @@ "valueCoding": { "system": "http://terminology.hl7.org/CodeSystem/v3-NullFlavor", "code": "OTH", - "display": "Other" + "display": "Anders" } } ], @@ -542,7 +561,7 @@ "valueCoding": { "system": "http://snomed.info/sct", "code": "373066001", - "display": "Ja" + "display": "ja" }, "item": [ { @@ -636,6 +655,19 @@ ], "linkId": "1190", "text": "Wens en verwachting patient ([MetingWaarde])" + }, + { + "answer": [ + { + "valueCoding": { + "system": "http://snomed.info/sct", + "code": "370819000", + "display": "Vaststellen van persoonlijke waarden en wensen met betrekking tot zorg" + } + } + ], + "linkId": "1648", + "text": "Vaststellen wens en verwachting patiënt ([MeetMethode])" } ] }, @@ -649,11 +681,11 @@ "valueCoding": { "system": "http://snomed.info/sct", "code": "395091006", - "display": "Gewenste plek van overlijden" + "display": "Voorkeur voor plaats van overlijden (waarneembare entiteit)" } } ], - "linkId": "1430", + "linkId": "1649", "text": "Gewenste plek van overlijden ([MetingNaam])" }, { @@ -662,7 +694,7 @@ "valueCoding": { "system": "http://snomed.info/sct", "code": "264362003", - "display": "Thuis" + "display": "thuis" }, "item": [ { @@ -705,7 +737,7 @@ "valueCoding": { "system": "http://snomed.info/sct", "code": "340201000146103", - "display": "Wenst geen euthanasie" + "display": "wil geen euthanasie" } } ], @@ -737,12 +769,25 @@ "valueCoding": { "system": "http://snomed.info/sct", "code": "373066001", - "display": "Ja" + "display": "ja" } } ], "linkId": "1435", "text": "Keuze orgaandonatie in donorregister ([MetingWaarde])" + }, + { + "answer": [ + { + "valueCoding": { + "system": "http://snomed.info/sct", + "code": "1156040003", + "display": "Self reported (qualifier value)" + } + } + ], + "linkId": "1650", + "text": "Keuze orgaandonatie vastgelegd in donorregister? ([MeetMethode])" } ] } @@ -786,7 +831,7 @@ "valueCoding": { "system": "http://snomed.info/sct", "code": "373066001", - "display": "Ja" + "display": "ja" }, "item": [ { @@ -810,7 +855,7 @@ "valueCoding": { "system": "http://snomed.info/sct", "code": "373066001", - "display": "Ja" + "display": "ja" } } ], diff --git a/input/resources/QuestionnaireResponse-SamiraVanDerSluijs-20251117.json b/input/resources/QuestionnaireResponse-SamiraVanDerSluijs-20251117.json index 69d226e..d33bbbe 100644 --- a/input/resources/QuestionnaireResponse-SamiraVanDerSluijs-20251117.json +++ b/input/resources/QuestionnaireResponse-SamiraVanDerSluijs-20251117.json @@ -130,7 +130,7 @@ { "answer": [ { - "valueString": "Patiënt is wilsbekwaam. Bij verandering van de situatie wordt haar partner haar wettelijk vertegenwoordiger." + "valueString": "Patiënt is wilsbekwaam. Bij verandering van de situatie wordt haar partner haar wettelijk vertegenwoordiger." } ], "linkId": "1407", @@ -139,7 +139,7 @@ ] } ], - "linkId": "1406", + "linkId": "1651", "text": "Is de patiënt op dit moment wilsbekwaam m.b.t. medische behandelbeslissingen?" }, { @@ -200,9 +200,9 @@ "answer": [ { "valueCoding": { - "system": "http://terminology.hl7.org/CodeSystem/v3-AddressUse", - "code": "MC", - "display": "Mobiel telefoonnummer" + "system": "http://hl7.org/fhir/contact-point-system", + "code": "phone", + "display": "Phone" } } ], @@ -213,9 +213,9 @@ "answer": [ { "valueCoding": { - "system": "http://terminology.hl7.org/CodeSystem/v3-AddressUse", - "code": "HP", - "display": "Telefoonnummer thuis" + "system": "http://hl7.org/fhir/contact-point-use", + "code": "home", + "display": "Home" } } ], @@ -241,9 +241,9 @@ "answer": [ { "valueCoding": { - "system": "http://terminology.hl7.org/CodeSystem/v3-AddressUse", - "code": "HP", - "display": "Privé e-mailadres" + "system": "http://hl7.org/fhir/contact-point-use", + "code": "home", + "display": "Home" } } ], @@ -343,9 +343,9 @@ "answer": [ { "valueCoding": { - "system": "http://terminology.hl7.org/CodeSystem/v3-AddressUse", - "code": "MC", - "display": "Mobiel telefoonnummer" + "system": "http://hl7.org/fhir/contact-point-system", + "code": "phone", + "display": "Phone" } } ], @@ -356,9 +356,9 @@ "answer": [ { "valueCoding": { - "system": "http://terminology.hl7.org/CodeSystem/v3-AddressUse", - "code": "HP", - "display": "Telefoonnummer thuis" + "system": "http://hl7.org/fhir/contact-point-use", + "code": "home", + "display": "Home" } } ], @@ -384,9 +384,9 @@ "answer": [ { "valueCoding": { - "system": "http://terminology.hl7.org/CodeSystem/v3-AddressUse", - "code": "HP", - "display": "Privé e-mailadres" + "system": "http://hl7.org/fhir/contact-point-use", + "code": "home", + "display": "Home" } } ], @@ -444,15 +444,21 @@ "text": "Patiënt" }, { - "linkId": "998", - "text": "Relatie tot patiënt", - "answer": [ + "linkId": "1652", + "text": "Contactperso(o)n(en)", + "item": [ { - "valueCoding": { - "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", - "code": "SIS", - "display": "Zuster" - } + "linkId": "998", + "text": "Relatie tot patiënt", + "answer": [ + { + "valueCoding": { + "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", + "code": "SIS", + "display": "Zuster" + } + } + ] } ] } @@ -480,8 +486,8 @@ { "valueCoding": { "system": "http://snomed.info/sct", - "code": "225353007", - "display": "Palliatief met als doel symptoomverlichting, waarbij levensverlenging niet gewenst is" + "code": "713148004", + "display": "voorkomen en behandelen van symptomen" } } ], @@ -641,7 +647,7 @@ { "answer": [ { - "valueString": "Alleen als de patiënt een kans heeft om weer uit het ziekenhuis te komen, anders hoeft het niet meer voor mevrouw" + "valueString": "Alleen als de patiënt een kans heeft om weer uit het ziekenhuis te komen, anders hoeft het niet meer voor mevrouw" } ], "linkId": "1394", @@ -729,7 +735,7 @@ "valueCoding": { "system": "http://terminology.hl7.org/CodeSystem/v3-NullFlavor", "code": "OTH", - "display": "Other" + "display": "Anders" } } ], @@ -768,7 +774,7 @@ "valueCoding": { "system": "http://snomed.info/sct", "code": "373066001", - "display": "Ja" + "display": "ja" }, "item": [ { @@ -868,11 +874,24 @@ { "answer": [ { - "valueString": "De kleinzoon van mevrouw van der Sluijs is geboren en mevrouw is dolgelukkig dat ze hem heeft kunnen zien. Ze merkt dat ze fysiek erg achteruit gaat. Mevrouw heeft daar nu vrede mee, in tegenstelling tot eerdere gesprekken." + "valueString": "De kleinzoon van mevrouw van der Sluijs is geboren en mevrouw is dolgelukkig dat ze hem heeft kunnen zien. Ze merkt dat ze fysiek erg achteruit gaat. Mevrouw heeft daar nu vrede mee, in tegenstelling tot eerdere gesprekken." } ], "linkId": "1190", "text": "Wens en verwachting patient ([MetingWaarde])" + }, + { + "answer": [ + { + "valueCoding": { + "system": "http://snomed.info/sct", + "code": "370819000", + "display": "Vaststellen van persoonlijke waarden en wensen met betrekking tot zorg" + } + } + ], + "linkId": "1648", + "text": "Vaststellen wens en verwachting patiënt ([MeetMethode])" } ] }, @@ -886,11 +905,11 @@ "valueCoding": { "system": "http://snomed.info/sct", "code": "395091006", - "display": "Gewenste plek van overlijden" + "display": "Voorkeur voor plaats van overlijden (waarneembare entiteit)" } } ], - "linkId": "1430", + "linkId": "1649", "text": "Gewenste plek van overlijden ([MetingNaam])" }, { @@ -899,7 +918,7 @@ "valueCoding": { "system": "http://snomed.info/sct", "code": "264362003", - "display": "Thuis" + "display": "thuis" }, "item": [ { @@ -942,7 +961,7 @@ "valueCoding": { "system": "http://snomed.info/sct", "code": "340201000146103", - "display": "Wenst geen euthanasie" + "display": "wil geen euthanasie" } } ], @@ -974,12 +993,25 @@ "valueCoding": { "system": "http://snomed.info/sct", "code": "373066001", - "display": "Ja" + "display": "ja" } } ], "linkId": "1435", "text": "Keuze orgaandonatie in donorregister ([MetingWaarde])" + }, + { + "answer": [ + { + "valueCoding": { + "system": "http://snomed.info/sct", + "code": "1156040003", + "display": "Self reported (qualifier value)" + } + } + ], + "linkId": "1650", + "text": "Keuze orgaandonatie vastgelegd in donorregister? ([MeetMethode])" } ] } @@ -1005,7 +1037,7 @@ { "answer": [ { - "valueString": "Mevrouw is gek op haar kleinzoon, dus brengt graag veel tijd met hem door. Verder is ze gek op muziek en luistert ze dat graag als ze alleen is." + "valueString": "Mevrouw is gek op haar kleinzoon, dus brengt graag veel tijd met hem door. Verder is ze gek op muziek en luistert ze dat graag als ze alleen is." } ], "linkId": "1196", @@ -1023,7 +1055,7 @@ "valueCoding": { "system": "http://snomed.info/sct", "code": "373066001", - "display": "Ja" + "display": "ja" }, "item": [ { @@ -1047,7 +1079,7 @@ "valueCoding": { "system": "http://snomed.info/sct", "code": "373066001", - "display": "Ja" + "display": "ja" } } ], diff --git a/input/resources/README.md b/input/resources/README.md index c680afb..3f4b5aa 100644 --- a/input/resources/README.md +++ b/input/resources/README.md @@ -68,80 +68,22 @@ Transform the exported metadata to follow IG standards. Use English for all meta ### Step 3: Save to Repository Save the adjusted Questionnaire as `Questionnaire-[id].json` in `input/resources/` -### Step 4: Fix Conditional Expressions +### Step 4: Replace all anwserOption with a answerValueSet reference +To ensure better maintainability and consistency, replace all `answerOption` arrays in the questionnaire items with a reference to an `answerValueSet`. Use a diff to identify all `answerValueSet` references and replace the corresponding `answerOption` arrays with the appropriate `ValueSet` reference. -Use [NLM Form Builder](https://formbuilder.nlm.nih.gov/) to correct invalid FHIRPath expressions: - -1. Select **"Start with existing form"** → **"Import from local file"** -2. Import your adjusted Questionnaire JSON -3. Review warnings for invalid FHIRPath conditions -4. Fix conditional displays (especially boolean comparisons) - - **Example fix needed:** - - Item: "Naam eerste contactpersoon" should display when: - - Question `984` ("Is de wettelijk vertegenwoordiger ook de eerste contactpersoon?") = `Nee (0)` - -### Step 5: Set Read-Only Treatment/Measurement Codes - -For sections **4. Behandelgrenzen** and **5. Behandelwensen**, configure treatment codes and measurement names as: -- **Read only**: Yes -- **Value method**: Pick initial value - -This simplifies QuestionnaireResponse creation. Example result: - -```json -"item": [ - { - "type": "choice", - "linkId": "1408", - "text": "Belangrijkste doel van behandeling ([MetingNaam])", - "required": false, - "repeats": false, - "readOnly": true, - "answerOption": [ - { - "valueCoding": { - "system": "http://snomed.info/sct", - "code": "180771000146100", - "display": "Focus van behandeling (waarneembare entiteit)" - }, - "initialSelected": true - } - ] - }, -] -``` - -### Step 6: Export and Replace +### Step 5: Export and Replace 1. In Form Builder, select top-right menu → **"Export"** → **"Export to file in FHIR R4 format"** 2. Save and replace the file in `input/resources/` -### Step 7: Expand ICD valueset -In the question concerning the 'ProductType van IC' (linkID 1008), replace the ICD code and display by the valueset values, including the system, the code and the display. Illustrated for first two answer options: -```json -"answerOption": [ - { - "valueCoding": { - "system": "http://snomed.info/sct", - "code": "72506001", - "display": "implanteerbare cardioverter-defibrillator" - } - }, - { - "valueCoding": { - "system": "http://snomed.info/sct", - "code": "465460004", - "display": "univentriculaire implanteerbare cardioverter-defibrillator" - } - }, -] -``` -### Step 8: Remove 'code' keys from questionnaire items with Python script +### Step 6: Populate item prefix with Python script +Run the Questionnaire Item Prefix Populator script (`/util\questionnaire_item_prefix_populator.py/`) that populates the `prefix` field for all questionnaire items based on their `linkId` values, following the pattern "Q[linkId]". This ensures consistent and clear identification of questionnaire items in the IG. + +### Step 7: Remove 'code' keys from questionnaire items with Python script Run the Questionnaire Item Code Remover script (`/util\questionnaire_item_code_remover.py/`) that removes 'code' keys from all questionnaire items, including incorrect 'code' properties. -### Step 9: Register in Configuration for better presentation in IG +### Step 8: Register in Configuration for better presentation in IG Add the Questionnaire to `sushi-config.yaml`: @@ -166,8 +108,11 @@ groups: ## QuestionnaireResponse Creation Process +### Step 0: Prepare Questionnaire +Run `util\questionnaire_item_anwserOption_expander.py` to expand answer options with proper display values, ensuring that the questionnaire is fully functional for data entry. + ### Step 1: Load Questionnaire -Use [LHC Forms](https://lhcforms.nlm.nih.gov/lhcforms/) to create example responses: +Use [LHC Forms](https://lhcforms.nlm.nih.gov/lhcforms/) to create example responses - use the expanded version of the questionnaire (output step 0): 1. Select **"Load From File"** 2. Choose the adjusted Questionnaire from `input/resources/` diff --git a/util/questionnaire_item_anwserOption_expander.py b/util/questionnaire_item_anwserOption_expander.py new file mode 100644 index 0000000..f12c90e --- /dev/null +++ b/util/questionnaire_item_anwserOption_expander.py @@ -0,0 +1,543 @@ +#!/usr/bin/env python3 +"""Expand Questionnaire ``answerValueSet`` references into inline ``answerOption`` lists. + +Why this exists +--------------- +The NLM LHC-Forms tool (https://lhcforms.nlm.nih.gov/) cannot resolve ``answerValueSet`` +references against a terminology server. To preview/use the ACP Questionnaire there, every +question that points at a ValueSet needs the matching codes materialised as ``answerOption``. + +What the script does +-------------------- +For every ``Questionnaire`` resource found in ``input/resources`` it walks the item tree and, +for each item that has an ``answerValueSet``, it: + + 1. Resolves the ValueSet, looking first in the local ``fsh-generated/resources`` folder and + then in the FHIR package cache (``~/.fhir/packages``). The packages to scan are taken + from the ``dependencies:`` block of ``sushi-config.yaml``; if that is missing, the + resolved dependency list in ``fhirpkg.lock.json`` is used instead. As a last resort the + whole package cache is searched. + 2. Expands it: + * a pre-computed ``expansion.contains`` is used as-is when present; + * otherwise ``compose.include`` is processed - explicit ``concept`` lists are used + directly, ``valueSet`` imports are resolved recursively, and a bare ``system`` + (whole code system) is enumerated from the resolved CodeSystem; + * ``compose.exclude`` entries are removed; + * the CodeSystem is consulted to fill in a missing ``display``. + 3. Replaces the item's ``answerValueSet`` with the resulting ``answerOption`` array. + +Because the form is Dutch, the Dutch (``nl-NL``) designation is preferred for the display, +falling back to the concept's base ``display`` and finally the code. + +Some ValueSets reference a CodeSystem we have no access to (e.g. the UZI based +``SpecialismeCodelijst``). For those a fixed answer list is supplied via ``HARDCODED_ANSWER_OPTIONS``. + +The original files are left untouched; a copy suffixed with ``-expanded`` is written next to +each source Questionnaire. + +Usage +----- + python util/questionnaire_item_anwserOption_expander.py + python util/questionnaire_item_anwserOption_expander.py --input-dir input/resources +""" + +from __future__ import annotations + +import argparse +import json +import re +import sys +from pathlib import Path + +# -------------------------------------------------------------------------------------- +# Configuration +# -------------------------------------------------------------------------------------- + +# Repository root (this file lives in /util/). +ROOT = Path(__file__).resolve().parent.parent + +# Where the source Questionnaires live and where expanded copies are written. +DEFAULT_INPUT_DIR = ROOT / "input" / "resources" + +# Locally generated conformance resources (ValueSets / CodeSystems produced by SUSHI). +FSH_GENERATED_DIR = ROOT / "fsh-generated" / "resources" + +# Resolved dependency list (authoritative); fall back to sushi-config.yaml if absent. +LOCK_FILE = ROOT / "fhirpkg.lock.json" +SUSHI_CONFIG = ROOT / "sushi-config.yaml" + +# FHIR package cache. Honour the standard override, otherwise ~/.fhir/packages. +import os + +FHIR_PACKAGE_CACHE = Path( + os.environ.get("FHIR_PACKAGE_CACHE", Path.home() / ".fhir" / "packages") +) + +# Suffix added to the expanded copy (before the .json extension). +OUTPUT_SUFFIX = "-expanded" + +# Preferred display language (the form is Dutch). +PREFERRED_LANGUAGE = "nl-NL" + +# SNOMED CT system URL. SNOMED designations often carry a trailing semantic tag in +# parentheses (e.g. "ja (kwalificatiewaarde)"); that tag is stripped from the display. +SNOMED_SYSTEM = "http://snomed.info/sct" + +# ValueSets that cannot be resolved from the packages (their CodeSystem is not available). +# Map the ValueSet canonical URL to a fixed list of FHIR answerOption entries. +HARDCODED_ANSWER_OPTIONS = { + # Functie (Specialisme) - SpecialismeCodelijst. Imports UZI based code lists that are + # not distributed in the package cache, so a representative subset is hardcoded. + "http://decor.nictiz.nl/fhir/ValueSet/2.16.840.1.113883.2.4.3.11.60.121.11.22--20200901000000": [ + {"valueCoding": {"system": "http://fhir.nl/fhir/NamingSystem/uzi-rolcode", "code": "01.000", "display": "Arts"}}, + {"valueCoding": {"system": "http://fhir.nl/fhir/NamingSystem/uzi-rolcode", "code": "01.015", "display": "Huisarts"}}, + {"valueCoding": {"system": "http://fhir.nl/fhir/NamingSystem/uzi-rolcode", "code": "01.047", "display": "Specialist ouderengeneeskunde"}}, + {"valueCoding": {"system": "http://fhir.nl/fhir/NamingSystem/uzi-rolcode", "code": "30.000", "display": "Verpleegkundige"}}, + {"valueCoding": {"system": "http://fhir.nl/fhir/NamingSystem/uzi-rolcode", "code": "81.000", "display": "Physician assistant"}}, + {"valueCoding": {"system": "http://fhir.nl/fhir/NamingSystem/uzi-rolcode", "code": "99.000", "display": "Zorgverlener andere zorg"}}, + ], +} + + +# -------------------------------------------------------------------------------------- +# Resource resolver: maps canonical URL -> file path for ValueSets and CodeSystems. +# -------------------------------------------------------------------------------------- + + +class ResourceResolver: + """Locate ValueSet / CodeSystem resources locally and in the FHIR package cache.""" + + def __init__(self) -> None: + # url -> Path. Local (fsh-generated) entries take precedence over package entries. + self._valuesets: dict[str, Path] = {} + self._codesystems: dict[str, Path] = {} + # Cache of loaded JSON resources keyed by path. + self._loaded: dict[Path, dict] = {} + self._scanned_full_cache = False + + self._index_local() + self._index_declared_packages() + + # -- index building ----------------------------------------------------------------- + + def _register(self, url: str | None, resource_type: str | None, path: Path) -> None: + if not url or not resource_type: + return + target = self._valuesets if resource_type == "ValueSet" else ( + self._codesystems if resource_type == "CodeSystem" else None + ) + if target is None: + return + # First registration wins (local before packages, declared deps before fallback). + target.setdefault(url, path) + + def _index_local(self) -> None: + """Index ValueSets / CodeSystems generated locally by SUSHI.""" + if not FSH_GENERATED_DIR.is_dir(): + return + for path in FSH_GENERATED_DIR.glob("*.json"): + try: + data = json.loads(path.read_text(encoding="utf-8")) + except (json.JSONDecodeError, OSError): + continue + if isinstance(data, dict): + self._register(data.get("url"), data.get("resourceType"), path) + # Cache it - we already paid the read cost. + self._loaded[path] = data + + def _package_dirs_from_dependencies(self) -> list[Path]: + """Determine which package directories to index from the project dependencies.""" + deps: dict[str, str] = {} + + # Primary source: the dependencies declared in sushi-config.yaml. + if SUSHI_CONFIG.is_file(): + deps.update(_parse_sushi_dependencies(SUSHI_CONFIG)) + + # Fallback: the resolved dependency list in fhirpkg.lock.json. + if not deps and LOCK_FILE.is_file(): + try: + lock = json.loads(LOCK_FILE.read_text(encoding="utf-8")) + deps.update(lock.get("dependencies", {})) + except (json.JSONDecodeError, OSError): + pass + + # hl7.fhir.r4.core is always needed for base FHIR code systems. + deps.setdefault("hl7.fhir.r4.core", "4.0.1") + + dirs: list[Path] = [] + for pkg_id, version in deps.items(): + candidate = FHIR_PACKAGE_CACHE / f"{pkg_id}#{version}" + if candidate.is_dir(): + dirs.append(candidate) + else: + print(f" ! dependency package not found in cache: {pkg_id}#{version}", + file=sys.stderr) + return dirs + + def _index_package_dir(self, pkg_dir: Path) -> None: + index_file = pkg_dir / "package" / ".index.json" + if not index_file.is_file(): + return + try: + index = json.loads(index_file.read_text(encoding="utf-8")) + except (json.JSONDecodeError, OSError): + return + for entry in index.get("files", []): + rtype = entry.get("resourceType") + if rtype not in ("ValueSet", "CodeSystem"): + continue + filename = entry.get("filename") + if not filename: + continue + self._register(entry.get("url"), rtype, pkg_dir / "package" / filename) + + def _index_declared_packages(self) -> None: + for pkg_dir in self._package_dirs_from_dependencies(): + self._index_package_dir(pkg_dir) + + def _index_full_cache(self) -> None: + """Fallback: index every package in the cache (used only when a URL is missing).""" + if self._scanned_full_cache or not FHIR_PACKAGE_CACHE.is_dir(): + return + self._scanned_full_cache = True + for pkg_dir in FHIR_PACKAGE_CACHE.iterdir(): + if pkg_dir.is_dir(): + self._index_package_dir(pkg_dir) + + # -- loading ------------------------------------------------------------------------ + + def _load(self, path: Path) -> dict | None: + if path in self._loaded: + return self._loaded[path] + try: + data = json.loads(path.read_text(encoding="utf-8")) + except (json.JSONDecodeError, OSError): + return None + self._loaded[path] = data + return data + + def get_valueset(self, url: str) -> dict | None: + url = _strip_version(url) + if url not in self._valuesets: + self._index_full_cache() + path = self._valuesets.get(url) + return self._load(path) if path else None + + def get_codesystem(self, url: str) -> dict | None: + url = _strip_version(url) + if url not in self._codesystems: + self._index_full_cache() + path = self._codesystems.get(url) + return self._load(path) if path else None + + +def _strip_version(url: str) -> str: + """Drop a trailing ``|version`` from a canonical reference.""" + return url.split("|", 1)[0] if url else url + + +def _parse_sushi_dependencies(config_path: Path) -> dict[str, str]: + """Minimal parser for the ``dependencies:`` block of sushi-config.yaml. + + Avoids a hard dependency on PyYAML. Only handles the simple ``id: version`` form, + which is what this project uses. + """ + deps: dict[str, str] = {} + try: + lines = config_path.read_text(encoding="utf-8").splitlines() + except OSError: + return deps + + in_block = False + dep_re = re.compile(r"^(\s*)([A-Za-z0-9._-]+)\s*:\s*([^\s#]+)") + for line in lines: + stripped = line.strip() + if not in_block: + if stripped.startswith("dependencies:"): + in_block = True + continue + # Stop when a new top-level (non-indented, non-comment) key starts. + if stripped and not line[0].isspace() and not stripped.startswith("#"): + break + if not stripped or stripped.startswith("#"): + continue + m = dep_re.match(line) + if m: + deps[m.group(2)] = m.group(3) + return deps + + +# -------------------------------------------------------------------------------------- +# ValueSet expansion +# -------------------------------------------------------------------------------------- + + +_SNOMED_SEMANTIC_TAG = re.compile(r"\s*\([^()]*\)\s*$") + + +def _clean_display(display: str | None, system: str | None) -> str | None: + """Strip the trailing SNOMED semantic tag (e.g. " (kwalificatiewaarde)") from a display.""" + if display and system == SNOMED_SYSTEM: + stripped = _SNOMED_SEMANTIC_TAG.sub("", display).strip() + if stripped: + return stripped + return display + + +def _dutch_display(concept: dict, system: str | None = None) -> str | None: + """Return the preferred display for a concept, favouring the Dutch designation. + + For SNOMED concepts a parenthesis-free designation is preferred, and any remaining + trailing semantic tag is stripped (so "ja (kwalificatiewaarde)" becomes "ja"). + """ + nl_values = [ + d["value"] for d in concept.get("designation", []) + if d.get("language") == PREFERRED_LANGUAGE and d.get("value") + ] + display = None + if nl_values: + # Prefer a Dutch designation without a parenthetical part, if one exists. + display = next((v for v in nl_values if "(" not in v), nl_values[0]) + else: + display = concept.get("display") + return _clean_display(display, system) + + +def _codesystem_display_map(codesystem: dict, system: str | None = None) -> dict[str, str]: + """Flatten a (possibly hierarchical) CodeSystem into code -> display.""" + result: dict[str, str] = {} + + def walk(concepts: list) -> None: + for concept in concepts: + code = concept.get("code") + if code is not None: + result[code] = _dutch_display(concept, system) or concept.get("display") or code + if concept.get("concept"): + walk(concept["concept"]) + + walk(codesystem.get("concept", [])) + return result + + +class ValueSetExpander: + def __init__(self, resolver: ResourceResolver) -> None: + self.resolver = resolver + + def expand(self, url: str) -> list[dict]: + """Expand a ValueSet URL into a list of FHIR ``answerOption`` entries.""" + if url in HARDCODED_ANSWER_OPTIONS: + return [dict(opt) for opt in HARDCODED_ANSWER_OPTIONS[url]] + + codings = self._expand_to_codings(url, seen=set()) + return [{"valueCoding": coding} for coding in codings] + + def _expand_to_codings(self, url: str, seen: set[str]) -> list[dict]: + url = _strip_version(url) + if url in seen: + return [] # guard against circular imports + seen.add(url) + + valueset = self.resolver.get_valueset(url) + if valueset is None: + raise LookupError(f"ValueSet not found: {url}") + + codings: list[dict] = [] + + # 1. Pre-computed expansion wins. + expansion = valueset.get("expansion", {}) + for contains in expansion.get("contains", []): + self._collect_contains(contains, codings) + if codings: + return _dedupe(codings) + + compose = valueset.get("compose", {}) + + # 2. Includes. + for include in compose.get("include", []): + codings.extend(self._expand_include(include, seen)) + + # 3. Excludes - drop matching (system, code) pairs. + excluded = set() + for exclude in compose.get("exclude", []): + for coding in self._expand_include(exclude, seen): + excluded.add((coding.get("system"), coding.get("code"))) + if excluded: + codings = [c for c in codings if (c.get("system"), c.get("code")) not in excluded] + + return _dedupe(codings) + + def _collect_contains(self, contains: dict, out: list[dict]) -> None: + if not contains.get("abstract") and contains.get("code") is not None: + coding = {} + if contains.get("system"): + coding["system"] = contains["system"] + coding["code"] = contains["code"] + display = _dutch_display(contains, contains.get("system")) + if display: + coding["display"] = display + out.append(coding) + for child in contains.get("contains", []): + self._collect_contains(child, out) + + def _expand_include(self, include: dict, seen: set[str]) -> list[dict]: + codings: list[dict] = [] + + # Imported ValueSets. + for imported_url in include.get("valueSet", []): + try: + codings.extend(self._expand_to_codings(imported_url, seen)) + except LookupError as exc: + print(f" ! could not resolve imported ValueSet: {exc}", file=sys.stderr) + + system = include.get("system") + + if "concept" in include and system: + # Explicit concept list; fill in missing displays from the CodeSystem. + cs_map: dict[str, str] | None = None + for concept in include["concept"]: + display = _dutch_display(concept, system) + if not display: + if cs_map is None: + codesystem = self.resolver.get_codesystem(system) + cs_map = _codesystem_display_map(codesystem, system) if codesystem else {} + display = cs_map.get(concept.get("code")) + coding = {"system": system, "code": concept.get("code")} + if display: + coding["display"] = display + codings.append(coding) + elif system and "filter" not in include: + # Whole code system - enumerate it from the resolved CodeSystem. + codesystem = self.resolver.get_codesystem(system) + if codesystem is None: + print(f" ! cannot enumerate system (CodeSystem not found): {system}", + file=sys.stderr) + else: + for code, display in _codesystem_display_map(codesystem, system).items(): + coding = {"system": system, "code": code} + if display: + coding["display"] = display + codings.append(coding) + elif system and "filter" in include: + print(f" ! filter-based include not supported for system: {system}", + file=sys.stderr) + + return codings + + +def _dedupe(codings: list[dict]) -> list[dict]: + """Remove duplicate (system, code) codings, preserving order.""" + seen: set[tuple] = set() + result: list[dict] = [] + for coding in codings: + key = (coding.get("system"), coding.get("code")) + if key not in seen: + seen.add(key) + result.append(coding) + return result + + +# -------------------------------------------------------------------------------------- +# Questionnaire processing +# -------------------------------------------------------------------------------------- + + +def process_items(items: list[dict], expander: ValueSetExpander, stats: dict) -> None: + """Recursively expand answerValueSet references on a list of Questionnaire items.""" + for item in items: + value_set_url = item.get("answerValueSet") + if value_set_url: + stats["found"] += 1 + try: + options = expander.expand(value_set_url) + except LookupError as exc: + stats["failed"] += 1 + print(f" ! {exc} (linkId={item.get('linkId')})", file=sys.stderr) + options = None + if options: + # Merge with any existing answerOption, then drop the reference so LHC + # uses the inline options instead of trying to resolve the ValueSet. + existing = item.get("answerOption", []) + item["answerOption"] = _merge_options(existing, options) + del item["answerValueSet"] + stats["expanded"] += 1 + print(f" + linkId={item.get('linkId')}: {len(options)} options " + f"from {value_set_url}") + elif options is not None: + print(f" ! linkId={item.get('linkId')}: 0 options from {value_set_url}", + file=sys.stderr) + + if item.get("item"): + process_items(item["item"], expander, stats) + + +def _merge_options(existing: list[dict], expanded: list[dict]) -> list[dict]: + """Append expanded options to any pre-existing ones, de-duplicating by coding.""" + seen: set[tuple] = set() + merged: list[dict] = [] + for opt in list(existing) + list(expanded): + coding = opt.get("valueCoding", {}) + key = (coding.get("system"), coding.get("code")) + if key not in seen: + seen.add(key) + merged.append(opt) + return merged + + +def process_questionnaire(path: Path, expander: ValueSetExpander) -> bool: + """Expand one Questionnaire file. Returns True if an expanded copy was written.""" + try: + data = json.loads(path.read_text(encoding="utf-8")) + except (json.JSONDecodeError, OSError) as exc: + print(f"! skipping {path.name}: {exc}", file=sys.stderr) + return False + + if not isinstance(data, dict) or data.get("resourceType") != "Questionnaire": + return False + + print(f"\nProcessing {path.name} ...") + stats = {"found": 0, "expanded": 0, "failed": 0} + process_items(data.get("item", []), expander, stats) + + if stats["found"] == 0: + print(" (no answerValueSet references found)") + return False + + out_path = path.with_name(f"{path.stem}{OUTPUT_SUFFIX}{path.suffix}") + out_path.write_text( + json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8" + ) + print(f" -> wrote {out_path.relative_to(ROOT)} " + f"(found {stats['found']}, expanded {stats['expanded']}, failed {stats['failed']})") + return True + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument("--input-dir", type=Path, default=DEFAULT_INPUT_DIR, + help=f"directory to scan for Questionnaires (default: {DEFAULT_INPUT_DIR})") + args = parser.parse_args() + + input_dir: Path = args.input_dir + if not input_dir.is_dir(): + print(f"Input directory does not exist: {input_dir}", file=sys.stderr) + return 1 + + print(f"FHIR package cache: {FHIR_PACKAGE_CACHE}") + resolver = ResourceResolver() + expander = ValueSetExpander(resolver) + + written = 0 + for path in sorted(input_dir.glob("*.json")): + # Skip the expanded copies themselves. + if path.stem.endswith(OUTPUT_SUFFIX): + continue + if process_questionnaire(path, expander): + written += 1 + + print(f"\nDone. Expanded copies written: {written}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())