Skill: elmariachi111/fhir-questionnaire (version 0.2.4)

--- name: design-fhir-loinc-questionnaires description: Helps creating FHIR conforming questionnaire definitions from plain requirement ideation docs. Contains scripts to look up LOINC and SNOMED CT codes for medical conditions, findings, observations, medications, procedures from a official coding APIs. No API keys required at the moment. metadata: dependencies: python>=3.8, jsonschema>=4.0.0 --- # FHIR Questionnaire Skill ## ⚠️ CRITICAL RULES - READ FIRST **NEVER suggest LOINC or SNOMED CT codes from memory or training data. ALWAYS use the search and query scripts in this skill.** When any clinical code is needed: 1. **For clinical questions/observations: ALWAYS run `python scripts/search_loinc.py "search term"` FIRST** 2. **For clinical concepts/conditions: ALWAYS run `python scripts/search_snomed.py "search term"` FIRST** 3. **ONLY use codes returned by the scripts** 4. **If search fails or returns no results, DO NOT make up codes** Clinical codes from AI memory are highly unreliable and will cause incorrect clinical coding. ## Network Access Requirements Requires whitelisted network access: - `clinicaltables.nlm.nih.gov` (LOINC search) - `tx.fhir.org` (FHIR terminology server for LOINC answer lists and SNOMED CT search) If network access fails, STOP. Do not suggest codes. ## Essential Scripts (Use These Every Time) ### 1. Search LOINC Codes **ALWAYS run this before suggesting any LOINC code (clinical questions/observations):** ```bash python scripts/search_loinc.py "depression screening" python scripts/search_loinc.py "blood pressure" --format fhir ``` ### 2. Search SNOMED CT Codes **ALWAYS run this before suggesting any SNOMED CT code (clinical concepts/conditions):** ```bash python scripts/search_snomed.py "diabetes" python scripts/search_snomed.py "hypertension" --format fhir python scripts/search_snomed.py "diabetes mellitus" --semantic-tag "disorder" ``` Note: The `--semantic-tag` filter works best when the semantic tag appears in the display name (e.g., "Diabetes mellitus (disorder)"). ### 3. Find Answer Options **For questions with standardized answers:** ```bash python scripts/query_valueset.py --loinc-code "72166-2" python scripts/query_valueset.py --loinc-code "72166-2" --format fhir ``` ### 4. Validate Questionnaire **Before finalizing:** ```bash python scripts/validate_questionnaire.py questionnaire.json ``` ## Templates Start with `assets/templates/`: - `minimal.json` - Bare bones structure - `basic.json` - Simple questionnaire - `advanced.json` - Complex with conditional logic ## Workflows ### Standardized Clinical Instruments (PHQ-9, GAD-7, etc.) ```bash # Step 1: Find panel code (NEVER skip this) python scripts/search_loinc.py "PHQ-9 panel" # Step 2: Find answer options python scripts/query_valueset.py --loinc-code "FOUND-CODE" --format fhir # Step 3: See examples/templates # Check references/examples.md for complete implementations ``` ### Custom Organizational Questionnaires ```bash # Step 1: Start with template cp assets/templates/advanced.json my-questionnaire.json # Step 2: For any clinical questions, search LOINC python scripts/search_loinc.py "body weight" # Step 3: Add answer options if available python scripts/query_valueset.py --loinc-code "FOUND-CODE" # Step 4: For custom questions without LOINC results, use inline answerOptions # (no coding system needed - just code + display) # Step 5: Validate python scripts/validate_questionnaire.py my-questionnaire.json ``` ### Custom Answer Lists (When LOINC Has No Match) When LOINC search returns no suitable answer list, use **inline answerOption with system-less valueCoding** by default. This is the simplest, spec-compliant approach for custom answer lists: ```json { "linkId": "sleep-quality", "type": "choice", "text": "How would you rate your sleep quality?", "answerOption": [ {"valueCoding": {"code": "good", "display": "Good"}}, {"valueCoding": {"code": "fair", "display": "Fair"}}, {"valueCoding": {"code": "poor", "display": "Poor"}} ] } ``` **Do NOT invent a coding system URI.** Omitting `system` is valid FHIR and signals that these are local, questionnaire-scoped codes. #### Opt-in: Reusable Welshare Coding System If the user explicitly requests reusable codes that can be shared across questionnaires, use the Welshare namespace (`http://codes.welshare.app`) via the helper script: ```bash python scripts/create_custom_codesystem.py --interactive ``` This creates a CodeSystem + ValueSet pair. To convert an inline answer list to the reusable format, add `"system": "http://codes.welshare.app/CodeSystem/<category>/<id>.json"` to each `valueCoding` and optionally reference the ValueSet via `answerValueSet`. See `references/loinc_guide.md` for details. ## Common Patterns - **Conditional display**: Use `enableWhen` to show/hide questions - **Repeating groups**: Set `"repeats": true` for medications, allergies, etc. - **Standardized answers**: Use `query_valueset.py --loinc-code "CODE"` for LOINC-backed answer lists - **Custom answers**: Use inline `answerOption` with `valueCoding` (no `system`) for non-standardized choices See `references/examples.md` for complete working examples. ## Script Reference ### search_loinc.py - Find LOINC Codes ```bash python scripts/search_loinc.py "blood pressure" python scripts/search_loinc.py "depression" --limit 10 --format fhir ``` ### search_snomed.py - Find SNOMED CT Codes ```bash python scripts/search_snomed.py "diabetes" python scripts/search_snomed.py "hypertension" --limit 10 --format fhir python scripts/search_snomed.py "asthma" --format table python scripts/search_snomed.py "diabetes mellitus" --semantic-tag "disorder" ``` **Formats:** `json` (default), `table`, `fhir` **Semantic tags (when present in results):** `disorder`, `finding`, `procedure`, `body structure`, `substance`, `organism` Note: Semantic tag filtering only works when tags are included in the display name from the terminology server. ### query_valueset.py - Find Answer Options ```bash python scripts/query_valueset.py --loinc-code "72166-2" python scripts/query_valueset.py --loinc-code "72166-2" --format fhir python scripts/query_valueset.py --search "smoking" ``` **Alternative servers** (if tx.fhir.org fails): - `--server https://hapi.fhir.org/baseR4` - `--server https://r4.ontoserver.csiro.au/fhir` ### validate_questionnaire.py - Validate Structure ```bash python scripts/validate_questionnaire.py questionnaire.json python scripts/validate_questionnaire.py questionnaire.json --verbose ``` ### extract_loinc_codes.py - Analyze Codes ```bash python scripts/extract_loinc_codes.py questionnaire.json python scripts/extract_loinc_codes.py questionnaire.json --validate ``` ### create_custom_codesystem.py - Reusable Custom Codes (Opt-in) ```bash python scripts/create_custom_codesystem.py --interactive ``` Only use when the user explicitly requests reusable codes across questionnaires. Uses the Welshare namespace: `http://codes.welshare.app`. Default for custom answers is inline `answerOption` without a coding system. ## Troubleshooting - **No LOINC results**: Use broader search terms (e.g., "depression" not "PHQ-9 question 1") - **Network errors**: Try alternative servers with `--server` flag - **Validation errors**: Check `references/fhir_questionnaire_spec.md` for requirements - **No answer list found**: Use inline `answerOption` with system-less `valueCoding` (code + display only). Do NOT fall back to a custom coding system unless the user explicitly requests it ## Deep Knowledge References We've assembled deep knowledge for you to consult on specific topics. Checkout the index file on See [REFERENCE.md](REFERENCE.md) and drill down the knowledge path for highly detailed instructions on modelling questionnaires. ## Reference Links - [FHIR Questionnaire Spec](https://hl7.org/fhir/questionnaire.html) - [LOINC Database](https://loinc.org) - [Complete Documentation](REFERENCE.md)

path: REFERENCE.md
content: # FHIR Questionnaire Reference Documentation

This document provides a navigation guide to all reference materials for the FHIR Questionnaire skill.

## Table of Contents

### Core Specification
- **[fhir_questionnaire_spec.md](references/fhir_questionnaire_spec.md)** - Complete FHIR R4 Questionnaire specification
  - Element definitions and data types
  - Item types (string, boolean, choice, group, etc.)
  - Conditional logic (enableWhen)
  - Answer options (answerOption, answerValueSet)
  - Validation rules and constraints

### Clinical Terminology
- **[loinc_guide.md](references/loinc_guide.md)** - LOINC coding guide and reference
  - Common LOINC questionnaire codes (PHQ-9, GAD-7, vital signs, demographics)
  - Search strategies and best practices
  - LOINC code structure and anatomy
  - Creating custom codes when LOINC isn't suitable
  - Answer list discovery

- **[snomed_guide.md](references/snomed_guide.md)** - SNOMED CT usage for clinical concepts (conditions, findings, procedures)

### Best Practices
- **[best_practices.md](references/best_practices.md)** - Design principles and guidelines
  - Questionnaire design principles
  - Coding and terminology guidelines
  - Conditional logic patterns
  - Validation checklist
  - Common antipatterns to avoid

### Examples
- **[examples.md](references/examples.md)** - Complete working examples
  - Simple patient intake questionnaire
  - PHQ-2 depression screening (standardized clinical instrument)
  - Conditional logic patterns
  - Repeating groups (medications, allergies)
  - Complex medical history questionnaire

### Schema
- **[schema/questionnaire.schema.json](references/schema/questionnaire.schema.json)** - JSON Schema definition
  - Authoritative source for FHIR R4 Questionnaire structure
  - Used by validation scripts
  - Machine-readable specification

## When to Read Each Reference

### Planning Phase
Start with:
1. **best_practices.md** - Understand design principles
2. **examples.md** - See complete working examples

### Implementation Phase
Reference as needed:
1. **fhir_questionnaire_spec.md** - Look up specific element definitions
2. **loinc_guide.md** - Find appropriate clinical codes
3. **schema/questionnaire.schema.json** - Verify structure requirements

### Validation Phase
Use:
1. **best_practices.md** - Review validation checklist
2. **fhir_questionnaire_spec.md** - Confirm conformance requirements


path: assets/templates/CodeSystem-example.json
content: {
  "resourceType": "CodeSystem",
  "id": "example-scale",
  "url": "http://codes.welshare.app/CodeSystem/category/example-scale.json",
  "version": "1.0.0",
  "name": "ExampleScale",
  "title": "Example Response Scale",
  "status": "active",
  "experimental": false,
  "date": "2025-01-01",
  "publisher": "Welshare",
  "description": "Example custom code system for response options",
  "content": "complete",
  "concept": [
    {
      "code": "never",
      "display": "Never"
    },
    {
      "code": "rarely",
      "display": "Rarely"
    },
    {
      "code": "sometimes",
      "display": "Sometimes"
    },
    {
      "code": "often",
      "display": "Often"
    },
    {
      "code": "always",
      "display": "Always"
    }
  ]
}


path: assets/templates/ValueSet-example.json
content: {
  "resourceType": "ValueSet",
  "id": "vs-example-scale",
  "url": "http://codes.welshare.app/ValueSet/category/example-scale.json",
  "title": "ValueSet - Example Response Scale",
  "status": "active",
  "description": "ValueSet containing all codes from Example Response Scale",
  "compose": {
    "include": [
      {
        "system": "http://codes.welshare.app/CodeSystem/category/example-scale.json"
      }
    ]
  }
}


path: assets/templates/advanced.json
content: {
  "resourceType": "Questionnaire",
  "id": "advanced-example",
  "meta": {
    "profile": ["http://hl7.org/fhir/StructureDefinition/Questionnaire"]
  },
  "url": "http://example.org/fhir/Questionnaire/advanced-example",
  "version": "1.0.0",
  "name": "AdvancedQuestionnaire",
  "title": "Advanced Questionnaire Template",
  "status": "draft",
  "experimental": false,
  "date": "2024-01-01",
  "publisher": "Example Healthcare Organization",
  "contact": [
    {
      "name": "Clinical Informatics Team",
      "telecom": [
        {
          "system": "email",
          "value": "[REDACTED_SECRET]"
        }
      ]
    }
  ],
  "description": "Comprehensive questionnaire template demonstrating advanced features",
  "purpose": "To serve as a starting point for complex clinical questionnaires",
  "code": [
    {
      "system": "http://loinc.org",
      "code": "XXXXX-X",
      "display": "Questionnaire concept"
    }
  ],
  "item": [
    {
      "linkId": "demographics",
      "type": "group",
      "text": "Demographics",
      "item": [
        {
          "linkId": "name",
          "type": "string",
          "code": [
            {
              "system": "http://loinc.org",
              "code": "54125-0",
              "display": "Patient name"
            }
          ],
          "text": "Full Name",
          "required": true
        },
        {
          "linkId": "dob",
          "type": "date",
          "code": [
            {
              "system": "http://loinc.org",
              "code": "21112-8",
              "display": "Birth date"
            }
          ],
          "text": "Date of Birth",
          "required": true
        },
        {
          "linkId": "gender",
          "type": "choice",
          "text": "Gender",
          "answerValueSet": "http://hl7.org/fhir/ValueSet/administrative-gender",
          "required": true
        }
      ]
    },
    {
      "linkId": "medical-history",
      "type": "group",
      "text": "Medical History",
      "item": [
        {
          "linkId": "has-conditions",
          "type": "boolean",
          "text": "Do you have any chronic medical conditions?",
          "required": true
        },
        {
          "linkId": "conditions-group",
          "type": "group",
          "text": "Conditions",
          "enableWhen": [
            {
              "question": "has-conditions",
              "operator": "=",
              "answerBoolean": true
            }
          ],
          "item": [
            {
              "linkId": "condition-list",
              "type": "open-choice",
              "text": "Select all that apply",
              "repeats": true,
              "answerOption": [
                {
                  "valueCoding": {
                    "system": "http://snomed.info/sct",
                    "code": "44054006",
                    "display": "Diabetes"
                  }
                },
                {
                  "valueCoding": {
                    "system": "http://snomed.info/sct",
                    "code": "38341003",
                    "display": "Hypertension"
                  }
                },
                {
                  "valueCoding": {
                    "system": "http://snomed.info/sct",
                    "code": "195967001",
                    "display": "Asthma"
                  }
                }
              ]
            },
            {
              "linkId": "conditions-detail",
              "type": "text",
              "text": "Please provide additional details about your conditions"
            }
          ]
        }
      ]
    },
    {
      "linkId": "medications",
      "type": "group",
      "text": "Current Medications",
      "item": [
        {
          "linkId": "has-medications",
          "type": "boolean",
          "text": "Are you currently taking any medications?",
          "required": true
        },
        {
          "linkId": "medication-entry",
          "type": "group",
          "text": "Medication",
          "repeats": true,
          "enableWhen": [
            {
              "question": "has-medications",
              "operator": "=",
              "answerBoolean": true
            }
          ],
          "item": [
            {
              "linkId": "med-name",
              "type": "string",
              "text": "Medication Name",
              "required": true
            },
            {
              "linkId": "med-dosage",
              "type": "string",
              "text": "Dosage",
              "required": true
            },
            {
              "linkId": "med-frequency",
              "type": "choice",
              "text": "Frequency",
              "answerOption": [
                {
                  "valueCoding": {
                    "code": "daily",
                    "display": "Daily"
                  }
                },
                {
                  "valueCoding": {
                    "code": "twice-daily",
                    "display": "Twice Daily"
                  }
                },
                {
                  "valueCoding": {
                    "code": "weekly",
                    "display": "Weekly"
                  }
                },
                {
                  "valueCoding": {
                    "code": "as-needed",
                    "display": "As Needed"
                  }
                }
              ]
            }
          ]
        }
      ]
    },
    {
      "linkId": "additional-info",
      "type": "group",
      "text": "Additional Information",
      "item": [
        {
          "linkId": "notes",
          "type": "text",
          "text": "Any additional information you would like to share?"
        }
      ]
    }
  ]
}


path: assets/templates/basic.json
content: {
  "resourceType": "Questionnaire",
  "id": "example-questionnaire",
  "url": "http://example.org/fhir/Questionnaire/example",
  "version": "1.0.0",
  "name": "ExampleQuestionnaire",
  "title": "Example Questionnaire",
  "status": "draft",
  "date": "2024-01-01",
  "publisher": "Example Organization",
  "description": "Brief description of the questionnaire",
  "item": [
    {
      "linkId": "section1",
      "type": "group",
      "text": "Section 1",
      "item": [
        {
          "linkId": "q1",
          "type": "string",
          "text": "Question 1",
          "required": true
        },
        {
          "linkId": "q2",
          "type": "choice",
          "text": "Question 2",
          "answerOption": [
            {
              "valueCoding": {
                "code": "option1",
                "display": "Option 1"
              }
            },
            {
              "valueCoding": {
                "code": "option2",
                "display": "Option 2"
              }
            }
          ]
        }
      ]
    }
  ]
}


path: assets/templates/minimal.json
content: {
  "resourceType": "Questionnaire",
  "id": "minimal-example",
  "status": "draft",
  "item": [
    {
      "linkId": "1",
      "type": "string",
      "text": "Question text here"
    }
  ]
}


path: references/best_practices.md
content: # Best Practices for FHIR Questionnaires

Guidelines for designing effective, maintainable, and interoperable FHIR Questionnaires.

## Table of Contents
- Design Principles
- Structure and Organization
- Coding and Terminology
- Conditional Logic
- Answer Options
- Validation and Testing
- Internationalization
- Performance

## Design Principles

### 1. Keep It Simple

Start with the minimal necessary structure. Add complexity only when needed.

**Good:**
```json
{
  "linkId": "name",
  "type": "string",
  "text": "Full Name",
  "required": true
}
```

**Avoid overengineering:**
```json
{
  "linkId": "name",
  "type": "string",
  "text": "Full Name",
  "required": true,
  "maxLength": 255,
  "readOnly": false,
  "repeats": false,
  "definition": "http://example.org/StructureDefinition/patient-name",
  "code": [...]
}
```

### 2. User-Centric Design

Write questions in clear, plain language. Consider the end user's perspective.

**Good:** "Have you experienced chest pain in the last 24 hours?"

**Avoid:** "Presence of thoracic discomfort within the preceding diurnal cycle?"

### 3. Progressive Disclosure

Use conditional logic to show questions only when relevant.

```json
{
  "linkId": "has-allergies",
  "type": "boolean",
  "text": "Do you have any allergies?"
},
{
  "linkId": "allergy-details",
  "type": "text",
  "text": "Please describe your allergies",
  "enableWhen": [{
    "question": "has-allergies",
    "operator": "=",
    "answerBoolean": true
  }]
}
```

## Structure and Organization

### Use Groups Effectively

Group related questions together:

```json
{
  "linkId": "demographics",
  "type": "group",
  "text": "Demographics",
  "item": [
    {
      "linkId": "name",
      "type": "string",
      "text": "Full Name"
    },
    {
      "linkId": "dob",
      "type": "date",
      "text": "Date of Birth"
    }
  ]
}
```

### Logical linkId Hierarchy

Use a consistent linkId naming scheme:

**Hierarchical:**
```
demographics
  demographics.name
  demographics.dob
  demographics.address
    demographics.address.street
    demographics.address.city
```

**Sequential:**
```
q1, q2, q3, ...
```

**Semantic:**
```
patient-name
patient-dob
patient-address-street
```

### Use Prefixes for Display

Add prefixes to improve readability:

```json
{
  "linkId": "q1",
  "prefix": "1.",
  "type": "string",
  "text": "What is your name?"
}
```

## Coding and Terminology

### Always Use Standard Codes When Available

Prioritize LOINC for clinical questions, SNOMED CT for clinical concepts:

```json
{
  "linkId": "blood-pressure",
  "type": "group",
  "code": [{
    "system": "http://loinc.org",
    "code": "85354-9",
    "display": "Blood pressure panel"
  }]
}
```

### Dual Coding Strategy

Combine standard and local codes:

```json
{
  "code": [
    {
      "system": "http://loinc.org",
      "code": "8867-4",
      "display": "Heart rate"
    },
    {
      "system": "http://example.org/local-codes",
      "code": "VITAL-HR"
    }
  ]
}
```

### Document Code System Versions

Include version information for reproducibility:

```json
{
  "code": [{
    "system": "http://loinc.org",
    "version": "2.78",
    "code": "8867-4",
    "display": "Heart rate"
  }]
}
```

## Conditional Logic

### Keep enableWhen Simple

Avoid complex nested conditions. If logic becomes too complex, reconsider the questionnaire structure.

**Simple (preferred):**
```json
{
  "enableWhen": [{
    "question": "q1",
    "operator": "=",
    "answerBoolean": true
  }]
}
```

**Complex (use sparingly):**
```json
{
  "enableWhen": [
    {
      "question": "q1",
      "operator": "=",
      "answerBoolean": true
    },
    {
      "question": "q2",
      "operator": ">",
      "answerInteger": 18
    }
  ],
  "enableBehavior": "all"
}
```

### Forward References Only

enableWhen should only reference questions that appear earlier in the questionnaire:

**Good:**
```json
[
  {"linkId": "q1", "type": "boolean", "text": "Question 1"},
  {"linkId": "q2", "type": "string", "text": "Question 2",
   "enableWhen": [{"question": "q1", "operator": "=", "answerBoolean": true}]}
]
```

**Avoid (backward reference):**
```json
[
  {"linkId": "q1", "type": "boolean", "text": "Question 1",
   "enableWhen": [{"question": "q2", "operator": "=", "answerBoolean": true}]},
  {"linkId": "q2", "type": "string", "text": "Question 2"}
]
```

### Test Edge Cases

Verify behavior when:
- enableWhen question has no answer
- Multiple enableWhen conditions interact
- Nested groups with enableWhen
- Repeating items with enableWhen

## Answer Options

### Priority Order for Answer Lists

Always follow this priority when defining answer options:

#### 1. LOINC Standardized Answer Lists (Best for Clinical)

Many LOINC codes come with standardized answer lists. Always check first:

```bash
# Discover answer options for a LOINC code
.venv/bin/python scripts/query_valueset.py --loinc-code "72166-2"
```

Then use the discovered ValueSet:

```json
{
  "linkId": "smoking-status",
  "type": "choice",
  "code": [{
    "system": "http://loinc.org",
    "code": "72166-2",
    "display": "Tobacco smoking status"
  }],
  "text": "Tobacco smoking status",
  "answerValueSet": "http://loinc.org/vs/LL2201-3"
}
```

#### 2. Standard FHIR ValueSets (For Common Administrative Data)

```json
{
  "linkId": "gender",
  "type": "choice",
  "text": "Gender",
  "answerValueSet": "http://hl7.org/fhir/ValueSet/administrative-gender"
}
```

#### 3. Inline Custom Answer Lists (Default for Non-Standardized)

When no LOINC or standard answer list exists, use inline `answerOption` with system-less `valueCoding`. This is the **default approach** for custom answer lists:

```json
{
  "linkId": "sleep-quality",
  "type": "choice",
  "text": "How would you rate your sleep quality?",
  "answerOption": [
    {"valueCoding": {"code": "good", "display": "Good"}},
    {"valueCoding": {"code": "fair", "display": "Fair"}},
    {"valueCoding": {"code": "poor", "display": "Poor"}}
  ]
}
```

Omitting `system` is valid FHIR and signals that these are questionnaire-scoped codes. This is appropriate for any number of custom options, not just short lists.

#### 4. Reusable Custom Coding System (Opt-in Only)

Only use a custom coding system (e.g., Welshare namespace `http://codes.welshare.app`) when the user **explicitly requests** reusable codes shared across multiple questionnaires. See `references/loinc_guide.md` for details.

**When to use each approach:**
- **LOINC answer lists**: For any LOINC-coded question (preferred for interoperability)
- **Standard FHIR ValueSets**: For common demographics and administrative data
- **Inline answerOption (no system)**: Default for all custom answer lists
- **Reusable coding system**: Only when the user explicitly requests cross-questionnaire code reuse

### Provide Display Values

Always include display text for codes:

```json
{
  "valueCoding": {
    "system": "http://snomed.info/sct",
    "code": "373066001",
    "display": "Yes"
  }
}
```

### Order Matters

List answer options in logical order:
- Chronological (e.g., dates, time periods)
- Severity (mild → moderate → severe)
- Frequency (never → sometimes → often → always)
- Alphabetical (for long lists)

## Validation and Testing

### Use the Validation Script

Always validate before publishing:

```bash
python scripts/validate_questionnaire.py questionnaire.json --verbose
```

### Test Scenarios

1. **Complete the questionnaire** as intended
2. **Skip optional questions** to test required field handling
3. **Trigger all conditional logic** paths
4. **Test with invalid data** (if possible)
5. **Test repeating groups** (add/remove items)

### Validate linkId Uniqueness

```bash
# Check for duplicate linkIds
cat questionnaire.json | jq '.. | .linkId? | select(. != null)' | sort | uniq -d
```

### Check enableWhen References

Ensure all enableWhen question references exist:

```bash
# Extract all enableWhen references
cat questionnaire.json | jq '.. | .enableWhen[]?.question' | sort | uniq
```

## Internationalization

### Support Multiple Languages

Use extensions for translations:

```json
{
  "linkId": "name",
  "type": "string",
  "text": "Full Name",
  "_text": {
    "extension": [{
      "url": "http://hl7.org/fhir/StructureDefinition/translation",
      "extension": [
        {
          "url": "lang",
          "valueCode": "es"
        },
        {
          "url": "content",
          "valueString": "Nombre Completo"
        }
      ]
    }]
  }
}
```

### Cultural Considerations

- Date formats (MM/DD/YYYY vs DD/MM/YYYY)
- Name ordering (given-family vs family-given)
- Gender and sex categories
- Measurement units (imperial vs metric)

## Performance

### Minimize Questionnaire Size

- Avoid deeply nested groups (limit to 3-4 levels)
- Use references to external resources rather than embedding
- Split very long questionnaires into multiple linked questionnaires

### Optimize ValueSet Usage

- Use answerValueSet references instead of expanding all codes inline
- Cache frequently used ValueSets
- Consider using small, focused ValueSets

### Lazy Loading Strategy

For large questionnaires:
1. Load basic structure first
2. Load conditional sections on demand
3. Retrieve ValueSets as needed

## Metadata Best Practices

### Published Questionnaires Must Have

```json
{
  "url": "http://example.org/fhir/Questionnaire/intake",
  "version": "1.0.0",
  "status": "active",
  "date": "2024-01-01",
  "publisher": "Example Healthcare"
}
```

### Use Semantic Versioning

- **Major** (1.0.0 → 2.0.0): Breaking changes (incompatible with previous responses)
- **Minor** (1.0.0 → 1.1.0): New questions, non-breaking changes
- **Patch** (1.0.0 → 1.0.1): Typos, display text improvements

### Include Contact Information

```json
{
  "contact": [{
    "name": "Clinical Informatics Team",
    "telecom": [{
      "system": "email",
      "value": "[REDACTED_SECRET]"
    }]
  }]
}
```

## Common Antipatterns to Avoid

### 1. Overly Complex Conditional Logic

**Problem:** Too many nested enableWhen conditions
**Solution:** Simplify structure, split into multiple questionnaires

### 2. Inconsistent linkId Naming

**Problem:** Mixing styles (q1, patient-name, demographics.dob)
**Solution:** Choose one scheme and stick with it

### 3. Missing Required Fields

**Problem:** Forgetting resourceType, status
**Solution:** Use validation tools early and often

### 4. Backward enableWhen References

**Problem:** Referencing questions that come later
**Solution:** Reorder questions or restructure logic

### 5. Inventing Fake Coding Systems for Custom Answers

**Problem:** Creating non-existent `system` URLs like `http://example.org/my-codes` for custom answers
**Solution:** Omit `system` for inline custom answers (valid FHIR), or use standard codes (LOINC, SNOMED). Only use a custom coding system if explicitly building a reusable code library

### 6. No Versioning for Published Questionnaires

**Problem:** Unable to track changes over time
**Solution:** Always version published questionnaires

### 7. Unclear Question Text

**Problem:** Medical jargon, ambiguous wording
**Solution:** Use plain language, test with users

## Testing Checklist

Before publishing a questionnaire:

- [ ] Validate JSON structure
- [ ] Run validation script
- [ ] Check linkId uniqueness
- [ ] Verify all enableWhen references exist
- [ ] Test all conditional paths
- [ ] Verify answerValueSet URLs are accessible
- [ ] Check LOINC codes are correct
- [ ] Test with real users (if possible)
- [ ] Review for accessibility
- [ ] Verify internationalization needs
- [ ] Document version changes
- [ ] Update metadata (date, version, status)

## Resources

- **FHIR Questionnaire Best Practices**: http://hl7.org/fhir/questionnaire.html
- **HL7 Structured Data Capture IG**: http://hl7.org/fhir/uv/sdc/
- **Form Design Best Practices**: Nielsen Norman Group research
- **Accessibility Guidelines**: WCAG 2.1 for forms


path: references/examples.md
content: # FHIR Questionnaire Examples

Complete examples of various questionnaire types and patterns.

## Table of Contents
- Simple Patient Intake
- PHQ-2 Depression Screening (Standardized)
- Conditional Logic Example
- Repeating Groups Example
- Choice Questions with ValueSets
- Complex Medical History

## Simple Patient Intake

A basic patient intake questionnaire with demographics and contact information.

```json
{
  "resourceType": "Questionnaire",
  "id": "patient-intake-simple",
  "url": "http://example.org/fhir/Questionnaire/patient-intake-simple",
  "version": "1.0.0",
  "name": "SimplePatientIntake",
  "title": "Simple Patient Intake Form",
  "status": "active",
  "date": "2024-01-01",
  "publisher": "Example Healthcare",
  "description": "Basic patient intake questionnaire for new patients",
  "item": [
    {
      "linkId": "demographics",
      "type": "group",
      "text": "Demographics",
      "item": [
        {
          "linkId": "name",
          "type": "string",
          "text": "Full Name",
          "required": true
        },
        {
          "linkId": "dob",
          "type": "date",
          "text": "Date of Birth",
          "required": true
        },
        {
          "linkId": "gender",
          "type": "choice",
          "text": "Gender",
          "answerValueSet": "http://hl7.org/fhir/ValueSet/administrative-gender",
          "required": true
        }
      ]
    },
    {
      "linkId": "contact",
      "type": "group",
      "text": "Contact Information",
      "item": [
        {
          "linkId": "phone",
          "type": "string",
          "text": "Phone Number",
          "required": true
        },
        {
          "linkId": "email",
          "type": "string",
          "text": "Email Address"
        },
        {
          "linkId": "address",
          "type": "text",
          "text": "Home Address",
          "required": true
        }
      ]
    }
  ]
}
```

## PHQ-2 Depression Screening

Standardized questionnaire using LOINC codes (shortened version of PHQ-9).

```json
{
  "resourceType": "Questionnaire",
  "id": "phq-2",
  "url": "http://example.org/fhir/Questionnaire/phq-2",
  "version": "1.0.0",
  "name": "PHQ2",
  "title": "Patient Health Questionnaire-2 (PHQ-2)",
  "status": "active",
  "code": [{
    "system": "http://loinc.org",
    "code": "69737-5",
    "display": "PHQ-2 quick depression assessment panel"
  }],
  "item": [
    {
      "linkId": "phq2-instructions",
      "type": "display",
      "text": "Over the last 2 weeks, how often have you been bothered by any of the following problems?"
    },
    {
      "linkId": "phq2-1",
      "prefix": "1.",
      "type": "choice",
      "text": "Little interest or pleasure in doing things",
      "required": true,
      "code": [{
        "system": "http://loinc.org",
        "code": "44250-9"
      }],
      "answerOption": [
        {
          "valueCoding": {
            "system": "http://loinc.org",
            "code": "LA6568-5",
            "display": "Not at all"
          }
        },
        {
          "valueCoding": {
            "system": "http://loinc.org",
            "code": "LA6569-3",
            "display": "Several days"
          }
        },
        {
          "valueCoding": {
            "system": "http://loinc.org",
            "code": "LA6570-1",
            "display": "More than half the days"
          }
        },
        {
          "valueCoding": {
            "system": "http://loinc.org",
            "code": "LA6571-9",
            "display": "Nearly every day"
          }
        }
      ]
    },
    {
      "linkId": "phq2-2",
      "prefix": "2.",
      "type": "choice",
      "text": "Feeling down, depressed, or hopeless",
      "required": true,
      "code": [{
        "system": "http://loinc.org",
        "code": "44255-8"
      }],
      "answerOption": [
        {
          "valueCoding": {
            "system": "http://loinc.org",
            "code": "LA6568-5",
            "display": "Not at all"
          }
        },
        {
          "valueCoding": {
            "system": "http://loinc.org",
            "code": "LA6569-3",
            "display": "Several days"
          }
        },
        {
          "valueCoding": {
            "system": "http://loinc.org",
            "code": "LA6570-1",
            "display": "More than half the days"
          }
        },
        {
          "valueCoding": {
            "system": "http://loinc.org",
            "code": "LA6571-9",
            "display": "Nearly every day"
          }
        }
      ]
    }
  ]
}
```

## Conditional Logic Example

Questionnaire demonstrating enableWhen conditions.

```json
{
  "resourceType": "Questionnaire",
  "id": "conditional-example",
  "url": "http://example.org/fhir/Questionnaire/conditional-example",
  "version": "1.0.0",
  "name": "ConditionalExample",
  "title": "Conditional Logic Example",
  "status": "draft",
  "item": [
    {
      "linkId": "has-symptoms",
      "type": "boolean",
      "text": "Have you experienced any symptoms in the past week?",
      "required": true
    },
    {
      "linkId": "symptom-details",
      "type": "group",
      "text": "Symptom Details",
      "enableWhen": [{
        "question": "has-symptoms",
        "operator": "=",
        "answerBoolean": true
      }],
      "item": [
        {
          "linkId": "symptom-type",
          "type": "choice",
          "text": "What type of symptoms?",
          "answerOption": [
            {"valueCoding": {"code": "fever", "display": "Fever"}},
            {"valueCoding": {"code": "cough", "display": "Cough"}},
            {"valueCoding": {"code": "fatigue", "display": "Fatigue"}},
            {"valueCoding": {"code": "other", "display": "Other"}}
          ]
        },
        {
          "linkId": "other-symptom",
          "type": "string",
          "text": "Please describe other symptoms",
          "enableWhen": [{
            "question": "symptom-type",
            "operator": "=",
            "answerCoding": {
              "code": "other"
            }
          }]
        },
        {
          "linkId": "symptom-severity",
          "type": "choice",
          "text": "How would you rate the severity?",
          "answerOption": [
            {"valueCoding": {"code": "mild", "display": "Mild"}},
            {"valueCoding": {"code": "moderate", "display": "Moderate"}},
            {"valueCoding": {"code": "severe", "display": "Severe"}}
          ]
        }
      ]
    },
    {
      "linkId": "seek-treatment",
      "type": "boolean",
      "text": "Have you sought medical treatment for severe symptoms?",
      "enableWhen": [
        {
          "question": "has-symptoms",
          "operator": "=",
          "answerBoolean": true
        },
        {
          "question": "symptom-severity",
          "operator": "=",
          "answerCoding": {
            "code": "severe"
          }
        }
      ],
      "enableBehavior": "all"
    }
  ]
}
```

## Repeating Groups Example

Questionnaire with repeating medication entries.

```json
{
  "resourceType": "Questionnaire",
  "id": "medications",
  "url": "http://example.org/fhir/Questionnaire/medications",
  "version": "1.0.0",
  "name": "MedicationList",
  "title": "Current Medications",
  "status": "active",
  "item": [
    {
      "linkId": "instructions",
      "type": "display",
      "text": "Please list all medications you are currently taking, including over-the-counter medications and supplements."
    },
    {
      "linkId": "has-medications",
      "type": "boolean",
      "text": "Are you currently taking any medications?",
      "required": true
    },
    {
      "linkId": "medication",
      "type": "group",
      "text": "Medication",
      "repeats": true,
      "enableWhen": [{
        "question": "has-medications",
        "operator": "=",
        "answerBoolean": true
      }],
      "item": [
        {
          "linkId": "medication.name",
          "type": "string",
          "text": "Medication Name",
          "required": true
        },
        {
          "linkId": "medication.dosage",
          "type": "string",
          "text": "Dosage (e.g., 10mg)",
          "required": true
        },
        {
          "linkId": "medication.frequency",
          "type": "choice",
          "text": "How often do you take it?",
          "answerOption": [
            {"valueCoding": {"code": "daily", "display": "Daily"}},
            {"valueCoding": {"code": "twice-daily", "display": "Twice Daily"}},
            {"valueCoding": {"code": "weekly", "display": "Weekly"}},
            {"valueCoding": {"code": "as-needed", "display": "As Needed"}}
          ]
        },
        {
          "linkId": "medication.reason",
          "type": "string",
          "text": "Reason for taking (optional)"
        }
      ]
    }
  ]
}
```

## Choice Questions with ValueSets

Examples using various answer patterns.

```json
{
  "resourceType": "Questionnaire",
  "id": "choice-examples",
  "url": "http://example.org/fhir/Questionnaire/choice-examples",
  "version": "1.0.0",
  "name": "ChoiceExamples",
  "title": "Choice Question Examples",
  "status": "draft",
  "item": [
    {
      "linkId": "gender",
      "type": "choice",
      "text": "Gender (using ValueSet reference)",
      "answerValueSet": "http://hl7.org/fhir/ValueSet/administrative-gender"
    },
    {
      "linkId": "yes-no",
      "type": "choice",
      "text": "Simple Yes/No (inline options)",
      "answerOption": [
        {
          "valueCoding": {
            "system": "http://terminology.hl7.org/CodeSystem/v2-0136",
            "code": "Y",
            "display": "Yes"
          }
        },
        {
          "valueCoding": {
            "system": "http://terminology.hl7.org/CodeSystem/v2-0136",
            "code": "N",
            "display": "No"
          }
        }
      ]
    },
    {
      "linkId": "pain-scale",
      "type": "choice",
      "text": "Pain level (0-10)",
      "answerOption": [
        {"valueInteger": 0},
        {"valueInteger": 1},
        {"valueInteger": 2},
        {"valueInteger": 3},
        {"valueInteger": 4},
        {"valueInteger": 5},
        {"valueInteger": 6},
        {"valueInteger": 7},
        {"valueInteger": 8},
        {"valueInteger": 9},
        {"valueInteger": 10}
      ]
    },
    {
      "linkId": "education",
      "type": "choice",
      "text": "Highest level of education",
      "answerOption": [
        {"valueCoding": {"code": "less-hs", "display": "Less than high school"}},
        {"valueCoding": {"code": "hs", "display": "High school graduate"}},
        {"valueCoding": {"code": "some-college", "display": "Some college"}},
        {"valueCoding": {"code": "bachelors", "display": "Bachelor's degree"}},
        {"valueCoding": {"code": "graduate", "display": "Graduate degree"}}
      ]
    },
    {
      "linkId": "languages",
      "type": "open-choice",
      "text": "Languages spoken (can specify other)",
      "repeats": true,
      "answerOption": [
        {"valueCoding": {"code": "en", "display": "English"}},
        {"valueCoding": {"code": "es", "display": "Spanish"}},
        {"valueCoding": {"code": "fr", "display": "French"}},
        {"valueCoding": {"code": "de", "display": "German"}},
        {"valueCoding": {"code": "zh", "display": "Chinese"}}
      ]
    }
  ]
}
```

## Complex Medical History

Comprehensive medical history with multiple sections.

```json
{
  "resourceType": "Questionnaire",
  "id": "medical-history",
  "url": "http://example.org/fhir/Questionnaire/medical-history",
  "version": "1.0.0",
  "name": "MedicalHistory",
  "title": "Comprehensive Medical History",
  "status": "active",
  "date": "2024-01-01",
  "publisher": "Example Healthcare",
  "item": [
    {
      "linkId": "chronic-conditions",
      "type": "group",
      "text": "Chronic Conditions",
      "item": [
        {
          "linkId": "has-chronic",
          "type": "boolean",
          "text": "Do you have any chronic medical conditions?",
          "required": true
        },
        {
          "linkId": "chronic-list",
          "type": "open-choice",
          "text": "Select all that apply",
          "repeats": true,
          "enableWhen": [{
            "question": "has-chronic",
            "operator": "=",
            "answerBoolean": true
          }],
          "answerOption": [
            {"valueCoding": {"system": "http://snomed.info/sct", "code": "44054006", "display": "Diabetes"}},
            {"valueCoding": {"system": "http://snomed.info/sct", "code": "38341003", "display": "Hypertension"}},
            {"valueCoding": {"system": "http://snomed.info/sct", "code": "195967001", "display": "Asthma"}},
            {"valueCoding": {"system": "http://snomed.info/sct", "code": "49601007", "display": "Cardiovascular disease"}}
          ]
        }
      ]
    },
    {
      "linkId": "allergies",
      "type": "group",
      "text": "Allergies",
      "code": [{
        "system": "http://loinc.org",
        "code": "48765-2",
        "display": "Allergies"
      }],
      "item": [
        {
          "linkId": "has-allergies",
          "type": "boolean",
          "text": "Do you have any known allergies?",
          "required": true
        },
        {
          "linkId": "allergy",
          "type": "group",
          "text": "Allergy",
          "repeats": true,
          "enableWhen": [{
            "question": "has-allergies",
            "operator": "=",
            "answerBoolean": true
          }],
          "item": [
            {
              "linkId": "allergy.substance",
              "type": "string",
              "text": "Allergen (e.g., peanuts, penicillin)"
            },
            {
              "linkId": "allergy.reaction",
              "type": "string",
              "text": "Reaction (e.g., rash, anaphylaxis)"
            },
            {
              "linkId": "allergy.severity",
              "type": "choice",
              "text": "Severity",
              "answerOption": [
                {"valueCoding": {"code": "mild", "display": "Mild"}},
                {"valueCoding": {"code": "moderate", "display": "Moderate"}},
                {"valueCoding": {"code": "severe", "display": "Severe"}}
              ]
            }
          ]
        }
      ]
    },
    {
      "linkId": "surgeries",
      "type": "group",
      "text": "Surgical History",
      "item": [
        {
          "linkId": "has-surgeries",
          "type": "boolean",
          "text": "Have you had any surgeries?",
          "required": true
        },
        {
          "linkId": "surgery",
          "type": "group",
          "text": "Surgery",
          "repeats": true,
          "enableWhen": [{
            "question": "has-surgeries",
            "operator": "=",
            "answerBoolean": true
          }],
          "item": [
            {
              "linkId": "surgery.type",
              "type": "string",
              "text": "Type of surgery"
            },
            {
              "linkId": "surgery.date",
              "type": "date",
              "text": "Approximate date"
            }
          ]
        }
      ]
    },
    {
      "linkId": "family-history",
      "type": "group",
      "text": "Family History",
      "item": [
        {
          "linkId": "family-conditions",
          "type": "open-choice",
          "text": "Has anyone in your immediate family had any of the following?",
          "repeats": true,
          "answerOption": [
            {"valueCoding": {"code": "heart-disease", "display": "Heart Disease"}},
            {"valueCoding": {"code": "cancer", "display": "Cancer"}},
            {"valueCoding": {"code": "diabetes", "display": "Diabetes"}},
            {"valueCoding": {"code": "mental-health", "display": "Mental Health Conditions"}}
          ]
        }
      ]
    },
    {
      "linkId": "social-history",
      "type": "group",
      "text": "Social History",
      "item": [
        {
          "linkId": "tobacco",
          "type": "choice",
          "text": "Tobacco use",
          "code": [{
            "system": "http://loinc.org",
            "code": "72166-2",
            "display": "Tobacco smoking status"
          }],
          "answerOption": [
            {"valueCoding": {"code": "never", "display": "Never smoked"}},
            {"valueCoding": {"code": "former", "display": "Former smoker"}},
            {"valueCoding": {"code": "current", "display": "Current smoker"}}
          ]
        },
        {
          "linkId": "alcohol",
          "type": "choice",
          "text": "Alcohol consumption",
          "answerOption": [
            {"valueCoding": {"code": "none", "display": "None"}},
            {"valueCoding": {"code": "occasional", "display": "Occasional (1-2 drinks/week)"}},
            {"valueCoding": {"code": "moderate", "display": "Moderate (3-7 drinks/week)"}},
            {"valueCoding": {"code": "heavy", "display": "Heavy (8+ drinks/week)"}}
          ]
        }
      ]
    }
  ]
}
```

## Usage Tips

### When to Use Each Pattern

1. **Simple Patient Intake**: Basic data collection with minimal logic
2. **PHQ-2**: Standardized clinical instruments with LOINC codes
3. **Conditional Logic**: Dynamic forms that adapt based on answers
4. **Repeating Groups**: Variable number of similar items (medications, allergies)
5. **Choice Questions**: Multiple selection patterns and answer formats
6. **Complex Medical History**: Comprehensive forms with multiple sections

### Adapting Examples

To adapt these examples:
1. Copy the relevant structure
2. Update linkId values to match your naming convention
3. Modify question text for your use case
4. Add appropriate codes (LOINC, SNOMED CT)
5. Adjust required/optional fields
6. Update ValueSets to match your terminology
7. Validate using the validation script

### Testing Approach

For each example:
1. Save as a .json file
2. Run validation: `python scripts/validate_questionnaire.py example.json`
3. Test in a FHIR server or rendering tool
4. Collect user feedback
5. Iterate and refine


path: references/fhir_questionnaire_spec.md
content: # FHIR Questionnaire Resource Specification (R4)

This reference provides essential information about the FHIR R4 Questionnaire resource structure.

**Source of Truth**: The definitive schema is located at `assets/schema/questionnaire.schema.json`. This document provides a human-readable interpretation of that schema.

## Table of Contents
- Resource Structure
- Root Elements
- Item Elements
- Item Types
- EnableWhen (Conditional Display)
- Answer Options
- Metadata Elements

## Resource Structure

Standard FHIR R4 Questionnaire resource structure:

```json
{
  "resourceType": "Questionnaire",
  "id": "example",
  "url": "http://example.org/fhir/Questionnaire/example",
  "version": "1.0.0",
  "status": "draft",
  "title": "Example Questionnaire",
  "item": [...]
}
```

## Root Elements

| Element | Cardinality | Type | Description |
|---------|------------|------|-------------|
| resourceType | 1..1 | code | Must be "Questionnaire" |
| id | 0..1 | string | Logical id (pattern: ^[A-Za-z0-9\-\.]{1,64}$) |
| url | 0..1 | uri | Canonical identifier (recommended for published) |
| version | 0..1 | string | Business version identifier |
| name | 0..1 | string | Computer-friendly name (no spaces) |
| title | 0..1 | string | Human-friendly name |
| status | 1..1 | code | draft \| active \| retired \| unknown |
| experimental | 0..1 | boolean | For testing purposes only |
| date | 0..1 | dateTime | Date last changed |
| publisher | 0..1 | string | Name of organization |
| description | 0..1 | markdown | Natural language description |
| purpose | 0..1 | markdown | Why this questionnaire is defined |
| code | 0..* | Coding | Concept that represents the overall questionnaire |
| item | 0..* | BackboneElement | Questions and sections |

## Item Elements

Items are the core building blocks of a questionnaire (questions and groups).

| Element | Cardinality | Type | Description |
|---------|------------|------|-------------|
| linkId | 1..1 | string | Unique id for item in questionnaire |
| definition | 0..1 | uri | ElementDefinition - details for the item |
| code | 0..* | Coding | Corresponding concept for this item |
| prefix | 0..1 | string | Display prefix (e.g., "1.", "a)") |
| text | 0..1 | string | Primary text for the item |
| type | 1..1 | code | Type of item (see Item Types below) |
| enableWhen | 0..* | BackboneElement | Conditional display rules |
| enableBehavior | 0..1 | code | all \| any (default: all) |
| required | 0..1 | boolean | Whether item must be answered |
| repeats | 0..1 | boolean | Whether item can repeat |
| readOnly | 0..1 | boolean | Don't allow input |
| maxLength | 0..1 | integer | Max characters for string types |
| answerValueSet | 0..1 | canonical | ValueSet containing permitted answers |
| answerOption | 0..* | BackboneElement | Permitted answers |
| initial | 0..* | BackboneElement | Initial value(s) |
| item | 0..* | BackboneElement | Nested items under this item |

## Item Types

| Type | Description | Answer Format |
|------|-------------|---------------|
| group | Container for items | (no answer) |
| display | Text for display (no answer) | (no answer) |
| boolean | True/False question | answerBoolean |
| decimal | Decimal number | answerDecimal |
| integer | Integer number | answerInteger |
| date | Date (no time) | answerDate |
| dateTime | Date and time | answerDateTime |
| time | Time of day | answerTime |
| string | Short text (< 255 chars) | answerString |
| text | Long text | answerString |
| url | URL/URI | answerString |
| choice | Single selection from list | answerCoding |
| open-choice | Single selection + "other" | answerCoding or answerString |
| attachment | File attachment | answerAttachment |
| reference | Reference to another resource | answerReference |
| quantity | Number with unit | answerQuantity |

## EnableWhen (Conditional Display)

Controls when an item is enabled/displayed based on answers to other questions.

```json
{
  "linkId": "2",
  "type": "string",
  "text": "If yes, please specify",
  "enableWhen": [
    {
      "question": "1",
      "operator": "=",
      "answerBoolean": true
    }
  ],
  "enableBehavior": "all"
}
```

### EnableWhen Elements

| Element | Cardinality | Type | Description |
|---------|------------|------|-------------|
| question | 1..1 | string | linkId of question to depend on |
| operator | 1..1 | code | exists \| = \| != \| > \| < \| >= \| <= |
| answer[x] | 0..1 | * | Answer value to compare against |

### Operators

- `exists`: True if question has any answer
- `=`: Answer equals the specified value
- `!=`: Answer not equal to specified value
- `>`, `<`, `>=`, `<=`: Comparison operators (for numbers, dates)

### EnableBehavior

- `all`: All enableWhen conditions must be true (AND logic)
- `any`: Any enableWhen condition must be true (OR logic)

## Answer Options

For `choice` and `open-choice` items, define permitted answers:

### Using answerOption

```json
{
  "linkId": "gender",
  "type": "choice",
  "text": "Gender",
  "answerOption": [
    {
      "valueCoding": {
        "system": "http://hl7.org/fhir/administrative-gender",
        "code": "male",
        "display": "Male"
      }
    },
    {
      "valueCoding": {
        "system": "http://hl7.org/fhir/administrative-gender",
        "code": "female",
        "display": "Female"
      }
    }
  ]
}
```

### Using answerValueSet

```json
{
  "linkId": "country",
  "type": "choice",
  "text": "Country",
  "answerValueSet": "http://hl7.org/fhir/ValueSet/iso3166-1-3"
}
```

## Metadata Elements

Recommended metadata for published questionnaires:

```json
{
  "resourceType": "Questionnaire",
  "id": "example",
  "meta": {
    "versionId": "1",
    "lastUpdated": "2024-01-01T00:00:00Z",
    "profile": ["http://hl7.org/fhir/StructureDefinition/Questionnaire"]
  },
  "url": "http://example.org/fhir/Questionnaire/example",
  "identifier": [{
    "system": "http://example.org/identifiers",
    "value": "Q12345"
  }],
  "version": "1.0.0",
  "name": "ExampleQuestionnaire",
  "title": "Example Patient Intake Questionnaire",
  "status": "active",
  "experimental": false,
  "date": "2024-01-01",
  "publisher": "Example Healthcare Organization",
  "contact": [{
    "name": "Clinical Informatics Team",
    "telecom": [{
      "system": "email",
      "value": "[REDACTED_SECRET]"
    }]
  }],
  "description": "Standard patient intake questionnaire",
  "useContext": [{
    "code": {
      "system": "http://terminology.hl7.org/CodeSystem/usage-context-type",
      "code": "focus"
    },
    "valueCodeableConcept": {
      "coding": [{
        "system": "http://snomed.info/sct",
        "code": "408443003",
        "display": "General medical practice"
      }]
    }
  }],
  "jurisdiction": [{
    "coding": [{
      "system": "urn:iso:std:iso:3166",
      "code": "US"
    }]
  }]
}
```

## Common Patterns

### Grouping Items

```json
{
  "linkId": "demographics",
  "type": "group",
  "text": "Demographics",
  "item": [
    {
      "linkId": "name",
      "type": "string",
      "text": "Full Name",
      "required": true
    },
    {
      "linkId": "dob",
      "type": "date",
      "text": "Date of Birth",
      "required": true
    }
  ]
}
```

### Repeating Items

```json
{
  "linkId": "medications",
  "type": "group",
  "text": "Current Medications",
  "repeats": true,
  "item": [
    {
      "linkId": "med-name",
      "type": "string",
      "text": "Medication Name"
    },
    {
      "linkId": "med-dose",
      "type": "string",
      "text": "Dosage"
    }
  ]
}
```

### Initial Values

```json
{
  "linkId": "country",
  "type": "choice",
  "text": "Country",
  "answerValueSet": "http://hl7.org/fhir/ValueSet/iso3166-1-3",
  "initial": [{
    "valueCoding": {
      "system": "urn:iso:std:iso:3166",
      "code": "US",
      "display": "United States"
    }
  }]
}
```

## Validation Rules

**Note**: The complete structural validation rules are defined in `assets/schema/questionnaire.schema.json`. The following are common validation checks:

1. **linkId uniqueness**: Each linkId must be unique across the entire questionnaire
2. **enableWhen references**: The `question` field must reference a valid linkId
3. **Answer types**: answerOption and answerValueSet should match the item type
4. **Required fields**: resourceType and status are mandatory per FHIR R4
5. **Status constraints**: Active/retired questionnaires should have url and version
6. **Nested groups**: Groups can contain groups, but watch for deep nesting
7. **enableWhen on first item**: Cannot reference questions that appear later
8. **ID pattern**: If present, the id field must match `^[A-Za-z0-9\-\.]{1,64}$`

## Common Coding Systems

| System | URL | Usage |
|--------|-----|-------|
| LOINC | http://loinc.org | Clinical questions and observations |
| SNOMED CT | http://snomed.info/sct | Clinical concepts |
| ICD-10 | http://hl7.org/fhir/sid/icd-10 | Diagnoses |
| RxNorm | http://www.nlm.nih.gov/research/umls/rxnorm | Medications |
| UCUM | http://unitsofmeasure.org | Units of measure |


path: references/loinc_guide.md
content: # LOINC Guide for FHIR Questionnaires

LOINC (Logical Observation Identifiers Names and Codes) provides standardized codes for clinical observations, including standardized questionnaires and assessment instruments.

## Table of Contents
- Using LOINC in Questionnaires
- Common LOINC Questionnaire Codes
- LOINC Code Structure
- Searching for LOINC Codes
- Best Practices

## Using LOINC in Questionnaires

### Questionnaire-Level Coding

Assign a LOINC code to the entire questionnaire using the `code` element:

```json
{
  "resourceType": "Questionnaire",
  "code": [{
    "system": "http://loinc.org",
    "code": "44249-1",
    "display": "PHQ-9 quick depression assessment panel"
  }]
}
```

### Item-Level Coding

Assign LOINC codes to individual questions:

```json
{
  "linkId": "q1",
  "type": "choice",
  "text": "Little interest or pleasure in doing things",
  "code": [{
    "system": "http://loinc.org",
    "code": "44250-9",
    "display": "Little interest or pleasure in doing things"
  }]
}
```

## Common LOINC Questionnaire Codes


### Vital Signs & Measurements

| LOINC Code | Display Name | Type |
|------------|--------------|------|
| 8867-4 | Heart rate | Observation |
| 85354-9 | Blood pressure panel | Panel |
| 8480-6 | Systolic blood pressure | Observation |
| 8462-4 | Diastolic blood pressure | Observation |
| 8310-5 | Body temperature | Observation |
| 29463-7 | Body weight | Observation |
| 8302-2 | Body height | Observation |
| 39156-5 | Body mass index (BMI) | Observation |
| 59408-5 | Oxygen saturation | Observation |

### Social History

| LOINC Code | Display Name |
|------------|--------------|
| 72166-2 | Tobacco smoking status |
| 11366-2 | History of tobacco use |
| 74013-4 | Alcoholic drinks per day |
| 68518-0 | Preferred language |
| 76689-9 | Sex assigned at birth |
| 54120-3 | Sexual orientation |
| 46098-0 | Biological sex |
| 71802-3 | Housing status |
| 82589-3 | Employment status |

## LOINC Code Structure

LOINC codes follow a structured format with multiple components:

**Format**: `Component : Property : Time : System : Scale : Method`

### Example: 8867-4 (Heart rate)
- **Component**: Heart rate
- **Property**: Rate (Numeric)
- **Time**: Point in time
- **System**: Heart
- **Scale**: Quantitative (Qn)
- **Method**: (not specified)

### Common Properties
- **Qn** (Quantitative): Numeric result
- **Ord** (Ordinal): Ordered categories (mild, moderate, severe)
- **Nom** (Nominal): Named categories (yes/no, blood types)
- **Nar** (Narrative): Free text

### Common Systems
- **Patient**: Questions about the patient as a whole
- **^Patient**: Self-reported by patient
- **Bld** (Blood): Blood-related observations
- **Ser/Plas**: Serum or plasma
- **{Setting}**: Clinical setting context

## Searching for LOINC Codes

### Using the search_loinc.py Script

```bash
# Search for depression screening tools
python scripts/search_loinc.py "depression screening" --limit 10

# Search for blood pressure codes
python scripts/search_loinc.py "blood pressure" --format fhir

# Search for anxiety assessments
python scripts/search_loinc.py "anxiety" --format table
```

### Online LOINC Resources

1. **LOINC Search**: https://loinc.org/search/
2. **FHIR LOINC ValueSets**: http://hl7.org/fhir/R4/loinc.html
3. **Clinical Tables API**: https://clinicaltables.nlm.nih.gov/
4. **RELMA (Regenstrief LOINC Mapping Assistant)**: Desktop tool for LOINC mapping

### Search Tips

- Search by clinical concept (e.g., "diabetes", "pain scale")
- Include measurement type (e.g., "blood glucose fasting")
- For panels, search with "panel" keyword (e.g., "lipid panel")
- For questionnaires, include scale name (e.g., "PHQ-9", "GAD-7")

## Best Practices

### 1. Use Standard Questionnaires When Available

When implementing well-known clinical scales (PHQ-9, GAD-7, etc.), use the official LOINC panel codes:

```json
{
  "resourceType": "Questionnaire",
  "url": "http://example.org/fhir/Questionnaire/phq9",
  "code": [{
    "system": "http://loinc.org",
    "code": "44249-1",
    "display": "PHQ-9 quick depression assessment panel"
  }],
  "item": [
    {
      "linkId": "phq9-1",
      "code": [{
        "system": "http://loinc.org",
        "code": "44250-9"
      }],
      "type": "choice",
      "text": "Little interest or pleasure in doing things"
    }
  ]
}
```

### 2. Combine with Custom Codes

For organization-specific questions, use both LOINC (if available) and local codes:

```json
{
  "linkId": "preferred-contact",
  "type": "choice",
  "text": "Preferred contact method",
  "code": [
    {
      "system": "http://loinc.org",
      "code": "68517-2",
      "display": "Preferred communication mode"
    },
    {
      "system": "http://example.org/codes",
      "code": "CONTACT-METHOD"
    }
  ]
}
```

### 3. Use Appropriate Scale Types

Match the LOINC scale type to your item type:

- **Qn (Quantitative)** → integer, decimal, quantity
- **Ord (Ordinal)** → choice with ordered options
- **Nom (Nominal)** → choice, open-choice
- **Nar (Narrative)** → text, string

### 4. Document LOINC Version

Include LOINC version in metadata for reproducibility:

```json
{
  "resourceType": "Questionnaire",
  "meta": {
    "tag": [{
      "system": "http://loinc.org",
      "code": "2.78",
      "display": "LOINC version 2.78"
    }]
  }
}
```

### 5. Use LOINC Answer Lists

For standardized questions, use LOINC answer lists (ValueSets):

```json
{
  "linkId": "tobacco-status",
  "type": "choice",
  "code": [{
    "system": "http://loinc.org",
    "code": "72166-2",
    "display": "Tobacco smoking status"
  }],
  "answerValueSet": "http://loinc.org/vs/LL2201-3"
}
```

#### Discovering Answer Lists for LOINC Codes

Many LOINC codes come with standardized answer lists. Use the `query_valueset.py` script to automatically discover them:

```bash
# Find answer options for a LOINC code
.venv/bin/python scripts/query_valueset.py --loinc-code "72166-2"

# Output:
# ValueSet: LOINCAnswerListLL2201_3
# URL: http://loinc.org/vs/LL2201-3
# Version: 2.77
#
# Answer Options (8 total):
#   LA18976-3       Current every day smoker
#   LA18977-1       Current some day smoker
#   LA15920-4       Former smoker
#   LA18978-9       Never smoker
#   ...

# Get as FHIR Coding objects for direct use
.venv/bin/python scripts/query_valueset.py --loinc-code "72166-2" --format fhir

# Get as complete Questionnaire item template
.venv/bin/python scripts/query_valueset.py --loinc-code "44250-9" --format questionnaire
```

The script uses the official FHIR terminology server (tx.fhir.org) which has comprehensive LOINC support including all answer lists. This ensures you use the correct, standardized answer options for each LOINC code.

**Benefits**:
- Automatically discovers associated answer lists
- Provides standardized, validated answer options
- Returns FHIR-compliant Coding objects
- Can generate complete Questionnaire item templates
- Ensures clinical interoperability

**Common LOINC codes with answer lists**:
- `72166-2` - Tobacco smoking status (8 options)
- `44250-9` - PHQ-9 depression items (4 frequency options)
- `54120-3` - Sexual orientation (7 options)
- Many standardized assessment instruments

### 6. Panel vs Individual Questions

- Use **panel codes** for the questionnaire as a whole
- Use **individual question codes** for each item
- Maintain the hierarchical structure

### 7. Validate Against Official Instruments

When implementing standardized instruments:
- Verify question wording matches the official instrument
- Use exact LOINC codes for each item
- Maintain original question order
- Use specified answer options

## Custom Answer Lists When LOINC Isn't Suitable

When LOINC search returns no suitable codes or answer lists, use **inline answerOption with system-less valueCoding** as the default approach. This is valid FHIR and the simplest way to define custom answer lists.

### Default: Inline Answer Options (No Coding System)

For custom questions where no standardized coding exists, define answer options directly on the questionnaire item. Omit the `system` property to signal these are questionnaire-scoped codes:

```json
{
  "linkId": "routine-preference",
  "type": "choice",
  "text": "How do you feel about routines?",
  "answerOption": [
    {"valueCoding": {"code": "prefer-routines", "display": "Prefer routines"}},
    {"valueCoding": {"code": "sometimes-new", "display": "Sometimes seek new challenges"}},
    {"valueCoding": {"code": "frequently-new", "display": "Frequently seek new challenges"}}
  ]
}
```

**When to use this approach:**
- No suitable LOINC codes exist for the question or its answers
- The answer list is specific to this questionnaire
- The options are unlikely to be reused across many questionnaires
- You want the simplest, most portable questionnaire structure

**Best practices for inline codes:**
1. **Search LOINC first**: Always try `search_loinc.py` and `query_valueset.py` before creating custom answers
2. **Use descriptive codes**: Make codes self-explanatory (e.g., `"prefer-routines"` not `"pr1"`)
3. **Consistent formatting**: Use lowercase with hyphens (kebab-case) for code values
4. **Clear display text**: Display text should be user-friendly and unambiguous
5. **Logical order**: List options in a natural order (severity, frequency, chronological, etc.)

### Hybrid: LOINC Question Code + Inline Custom Answers

You can assign a LOINC code to the question itself while using inline custom answer options:

```json
{
  "linkId": "exercise-frequency",
  "type": "choice",
  "code": [{
    "system": "http://loinc.org",
    "code": "68516-4",
    "display": "Exercise activity"
  }],
  "text": "How often do you exercise per week?",
  "answerOption": [
    {"valueCoding": {"code": "daily", "display": "Daily"}},
    {"valueCoding": {"code": "few-times-week", "display": "A few times a week"}},
    {"valueCoding": {"code": "weekly", "display": "About once a week"}},
    {"valueCoding": {"code": "rarely", "display": "Rarely"}},
    {"valueCoding": {"code": "never", "display": "Never"}}
  ]
}
```

This gives you standard interoperability for the question identity via LOINC, with custom granularity for the answers.

### Opt-in: Reusable Welshare Coding System

If the user **explicitly requests** reusable codes that can be shared across multiple questionnaires, use the Welshare namespace (`http://codes.welshare.app`). This is useful for organizations building a library of standardized custom codes.

#### Creating Reusable Codes

```bash
# Interactive mode
python scripts/create_custom_codesystem.py --interactive

# Command-line mode
python scripts/create_custom_codesystem.py \
  --id routine-preference \
  --category brainhealth \
  --title "Routine Preference Scale" \
  --description "Scale for assessing preference for routines" \
  --codes "prefer-routines:Prefer routines,sometimes-new:Sometimes seek new challenges"
```

The script generates:
- `CodeSystem-{id}.json` - Defines the codes and their meanings
- `ValueSet-vs-{id}.json` - References the CodeSystem for use in questionnaires

Both use the Welshare namespace `http://codes.welshare.app/` as URL base.

#### Converting Inline Answers to Reusable Codes

To upgrade an inline answer list to the reusable format, add the `system` property to each `valueCoding`:

```json
{
  "linkId": "routine-preference",
  "type": "choice",
  "text": "How do you feel about routines?",
  "answerOption": [
    {"valueCoding": {
      "system": "http://codes.welshare.app/CodeSystem/brainhealth/routine-preference.json",
      "code": "prefer-routines",
      "display": "Prefer routines"
    }},
    {"valueCoding": {
      "system": "http://codes.welshare.app/CodeSystem/brainhealth/routine-preference.json",
      "code": "sometimes-new",
      "display": "Sometimes seek new challenges"
    }}
  ]
}
```

Or reference a ValueSet instead of inline options:

```json
{
  "linkId": "routine-preference",
  "type": "choice",
  "text": "How do you feel about routines?",
  "answerValueSet": "http://codes.welshare.app/ValueSet/brainhealth/routine-preference.json"
}
```

## LOINC Copyright Notice

LOINC is copyright © 1995-2024, Regenstrief Institute, Inc. and the LOINC Committee.

Terms of use: https://loinc.org/license/

While LOINC codes are freely available, attribution is required when distributing questionnaires that use LOINC codes.


## Additional Resources

- **LOINC Official Site**: https://loinc.org
- **FHIR LOINC Implementation Guide**: http://hl7.org/fhir/loinc.html
- **LOINC Groups**: Pre-built panels and hierarchies



path: references/schema/questionnaire.schema.json
content: {
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "http://hl7.org/fhir/json-schema/Questionnaire",
  "title": "Questionnaire",
  "description": "A structured set of questions intended to guide the collection of answers from end-users. Questionnaires provide detailed control over order, presentation, phraseology and grouping to allow coherent, consistent data collection. See http://hl7.org/fhir/json.html#schema for information about the FHIR Json Schemas",
  "type": "object",
  "additionalProperties": true,
  "required": ["id", "resourceType"],
  "properties": {
    "resourceType": {
      "description": "This is a Questionnaire resource",
      "const": "Questionnaire"
    },
    "id": {
      "description": "The logical id of the resource, as used in the URL for the resource. Once assigned, this value never changes.",
      "type": "string",
      "pattern": "^[A-Za-z0-9\\-\\.]{1,64}$"
    },
    "meta": {
      "description": "The metadata about the resource. This is content that is maintained by the infrastructure. Changes to the content might not always be associated with version changes to the resource.",
      "type": "object",
      "properties": {
        "profile": {
          "type": "array",
          "items": {
            "type": "string"
          }
        },
        "tag": {
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "system": {
                "type": "string"
              },
              "code": {
                "type": "string"
              }
            }
          }
        }
      }
    },
    "language": {
      "description": "The base language in which the resource is written.",
      "type": "string",
      "pattern": "^[^\\s]+( [^\\s]+)*$"
    },
    "text": {
      "description": "A human-readable narrative that contains a summary of the resource and can be used to represent the content of the resource to a human. The narrative need not encode all the structured data, but is required to contain sufficient detail to make it \"clinically safe\" for a human to just read the narrative. Resource definitions may define what content should be represented in the narrative to ensure clinical safety.",
      "type": "string"
    },
    "extension": {
      "description": "May be used to represent additional information that is not part of the basic definition of the resource. To make the use of extensions safe and managable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer can define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension.",
      "type": "array",
      "items": {
        "type": "object",
        "additionalProperties": true
      }
    },
    "url": {
      "description": "An absolute URI that is used to identify this questionnaire when it is referenced in a specification, model, design or an instance; also called its canonical identifier. This SHOULD be globally unique and SHOULD be a literal address at which an authoritative instance of this questionnaire is (or will be) published. This URL can be the target of a canonical reference. It SHALL remain the same when the questionnaire is stored on different servers.",
      "type": "string"
    },
    "version": {
      "description": "The identifier that is used to identify this version of the questionnaire when it is referenced in a specification, model, design or instance. This is an arbitrary value managed by the questionnaire author and is not expected to be globally unique. For example, it might be a timestamp (e.g. yyyymmdd) if a managed version is not available. There is also no expectation that versions can be placed in a lexicographical sequence.",
      "type": "string"
    },
    "versionAlgorithmString": {
      "description": "Indicates the mechanism used to compare versions to determine which is more current.",
      "pattern": "^^[\\s\\S]+$$",
      "type": "string"
    },
    "versionAlgorithmCoding": {
      "description": "Indicates the mechanism used to compare versions to determine which is more current.",
      "type": "object",
      "properties": {
        "system": {
          "type": "string"
        },
        "version": {
          "type": "string"
        },
        "display": {
          "type": "string"
        },
        "code": {
          "type": "string"
        }
      }
    },
    "name": {
      "description": "A natural language name identifying the questionnaire. This name should be usable as an identifier for the module by machine processing applications such as code generation.",
      "type": "string"
    },
    "title": {
      "description": "A short, descriptive, user-friendly title for the questionnaire.",
      "type": "string"
    },
    "status": {
      "description": "The current state of this questionnaire.",
      "pattern": "^[^\\s]+( [^\\s]+)*$",
      "type": "string",
      "enum": ["draft", "active", "retired", "unknown"]
    },
    "experimental": {
      "description": "A Boolean value to indicate that this questionnaire is authored for testing purposes (or education/evaluation/marketing) and is not intended for genuine usage.",
      "pattern": "^true|false$",
      "type": "boolean"
    },
    "date": {
      "description": "The date  (and optionally time) when the questionnaire was last significantly changed. The date must change when the business version changes and it must change if the status code changes. In addition, it should change when the substantive content of the questionnaire changes.",
      "pattern": "^([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)(-(0[1-9]|1[0-2])(-(0[1-9]|[1-2][0-9]|3[0-1])(T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]{1,9})?)?)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00)?)?)?$",
      "type": "string"
    },
    "publisher": {
      "description": "The name of the organization or individual responsible for the release and ongoing maintenance of the questionnaire.",
      "type": "string"
    },
    "contact": {
      "description": "Contact details to assist a user in finding and communicating with the publisher.",
      "type": "array",
      "items": {
        "$ref": "#/definitions/ContactDetail"
      }
    },
    "description": {
      "description": "A free text natural language description of the questionnaire from a consumer\u0027s perspective.",
      "pattern": "^^[\\s\\S]+$$",
      "type": "string"
    },
    "useContext": {
      "description": "The content was developed with a focus and intent of supporting the contexts that are listed. These contexts may be general categories (gender, age, ...) or may be references to specific programs (insurance plans, studies, ...) and may be used to assist with indexing and searching for appropriate questionnaires.",
      "items": {
        "type": "object",
        "additionalProperties": true
      },
      "type": "array"
    },
    "jurisdiction": {
      "description": "A legal or geographic region in which the questionnaire is intended to be used.",
      "type": "array",
      "items": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "id": {
            "description": "Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces.",
            "type": "string"
          },
          "coding": {
            "description": "A reference to a code defined by a terminology system.",
            "type": "array",
            "items": {
              "$ref": "#/definitions/Coding"
            }
          },
          "text": {
            "description": "A human language representation of the concept as seen/selected/uttered by the user who entered the data and/or which represents the intended meaning of the user.",
            "type": "string"
          }
        }
      }
    },
    "purpose": {
      "description": "Explanation of why this questionnaire is needed and why it has been designed as it has.",
      "type": "string"
    },
    "copyright": {
      "description": "A copyright statement relating to the questionnaire and/or its contents. Copyright statements are generally legal restrictions on the use and publishing of the questionnaire.",
      "type": "string"
    },
    "copyrightLabel": {
      "description": "A short string (\u003c50 characters), suitable for inclusion in a page footer that identifies the copyright holder, effective period, and optionally whether rights are resctricted. (e.g. \u0027All rights reserved\u0027, \u0027Some rights reserved\u0027).",
      "type": "string"
    },
    "approvalDate": {
      "description": "The date on which the resource content was approved by the publisher. Approval happens once when the content is officially approved for usage.",
      "type": "string",
      "pattern": "^([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)(-(0[1-9]|1[0-2])(-(0[1-9]|[1-2][0-9]|3[0-1]))?)?$"
    },
    "lastReviewDate": {
      "description": "The date on which the resource content was last reviewed. Review happens periodically after approval but does not change the original approval date.",
      "type": "string",
      "pattern": "^([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)(-(0[1-9]|1[0-2])(-(0[1-9]|[1-2][0-9]|3[0-1]))?)?$"
    },
    "effectivePeriod": {
      "description": "The period during which the questionnaire content was or is planned to be in active use.",
      "type": "object",
      "properties": {
        "id": {
          "description": "Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces.",
          "type": "string"
        },
        "start": {
          "description": "The start of the period. The boundary is inclusive.",
          "type": "string",
          "pattern": "^([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)(-(0[1-9]|1[0-2])(-(0[1-9]|[1-2][0-9]|3[0-1])(T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]{1,9})?)?)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00)?)?)?$"
        },
        "end": {
          "description": "The end of the period. If the end of the period is missing, it means no end was known or planned at the time the instance was created. The start may be in the past, and the end date in the future, which means that period is expected/planned to end at that time.",
          "type": "string",
          "pattern": "^([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)(-(0[1-9]|1[0-2])(-(0[1-9]|[1-2][0-9]|3[0-1])(T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]{1,9})?)?)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00)?)?)?$"
        }
      }
    },
    "topic": {
      "description": "Descriptive topics related to the content of the {{title}}. Topics provide a high-level categorization as well as keywords for the {{title}} that can be useful for filtering and searching.",
      "items": {
        "$ref": "#/definitions/CodeableConcept"
      },
      "type": "array"
    },
    "author": {
      "description": "An individiual or organization primarily involved in the creation and maintenance of the {{title}}.",
      "items": {
        "$ref": "#/definitions/ContactDetail"
      },
      "type": "array"
    },
    "editor": {
      "description": "An individual or organization primarily responsible for internal coherence of the {{title}}.",
      "items": {
        "$ref": "#/definitions/ContactDetail"
      },
      "type": "array"
    },
    "reviewer": {
      "description": "An individual or organization asserted by the publisher to be primarily responsible for review of some aspect of the {{title}}.",
      "items": {
        "$ref": "#/definitions/ContactDetail"
      },
      "type": "array"
    },
    "endorser": {
      "description": "An individual or organization asserted by the publisher to be responsible for officially endorsing the {{title}} for use in some setting.",
      "items": {
        "$ref": "#/definitions/ContactDetail"
      },
      "type": "array"
    },
    "subjectType": {
      "description": "The types of subjects that can be the subject of responses created for the questionnaire.",
      "items": {
        "type": "string",
        "pattern": "^[^\\s]+( [^\\s]+)*$"
      },
      "type": "array"
    },
    "code": {
      "description": "An identifier for this collection of questions in a particular terminology such as LOINC.",
      "type": "array",
      "items": {
        "$ref": "#/definitions/Coding"
      }
    },
    "item": {
      "description": "A particular question, question grouping or display text that is part of the questionnaire.",
      "type": "array",
      "items": {
        "$ref": "#/definitions/Questionnaire_Item"
      }
    }
  },

  "definitions": {
    "Attachment": {
      "description": "For referring to data content defined in other formats.",
      "type": "object",
      "additionalProperties": true,
      "properties": {
        "id": {
          "description": "Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces.",
          "type": "string"
        },

        "contentType": {
          "description": "Identifies the type of the data in the attachment and allows a method to be chosen to interpret or render the data. Includes mime type parameters such as charset where appropriate.",
          "type": "string",
          "pattern": "^[^\\s]+( [^\\s]+)*$"
        },

        "language": {
          "description": "The human language of the content. The value can be any valid value according to BCP 47.",
          "type": "string",
          "pattern": "^[^\\s]+( [^\\s]+)*$"
        },

        "data": {
          "description": "The actual data of the attachment - a sequence of bytes, base64 encoded.",
          "type": "string"
        },

        "url": {
          "description": "A location where the data can be accessed.",
          "type": "string",
          "pattern": "^\\S*$"
        },

        "size": {
          "description": "The number of bytes of data that make up this attachment (before base64 encoding, if that is done).",
          "type": "string",
          "pattern": "^[0]|[-+]?[1-9][0-9]*$"
        },

        "hash": {
          "description": "The calculated hash of the data using SHA-1. Represented using base64.",
          "type": "string"
        },

        "title": {
          "description": "A label or set of text to display in place of the data.",
          "type": "string"
        },

        "creation": {
          "description": "The date that the attachment was first created.",
          "type": "string",
          "pattern": "^([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)(-(0[1-9]|1[0-2])(-(0[1-9]|[1-2][0-9]|3[0-1])(T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]{1,9})?)?)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00)?)?)?$"
        },

        "height": {
          "description": "Height of the image in pixels (photo/video).",
          "type": "number",
          "pattern": "^[1-9][0-9]*$"
        },

        "width": {
          "description": "Width of the image in pixels (photo/video).",
          "type": "number",
          "pattern": "^[1-9][0-9]*$"
        },

        "frames": {
          "description": "The number of frames in a photo. This is used with a multi-page fax, or an imaging acquisition context that takes multiple slices in a single image, or an animated gif. If there is more than one frame, this SHALL have a value in order to alert interface software that a multi-frame capable rendering widget is required.",
          "type": "number",
          "pattern": "^[1-9][0-9]*$"
        },

        "duration": {
          "description": "The duration of the recording in seconds - for audio and video.",
          "type": "number",
          "pattern": "^-?(0|[1-9][0-9]{0,17})(\\.[0-9]{1,17})?([eE][+-]?[0-9]{1,9})?$"
        },

        "pages": {
          "description": "The number of pages when printed.",
          "type": "number",
          "pattern": "^[1-9][0-9]*$"
        }
      }
    },
    "Coding": {
      "description": "A reference to a code defined by a terminology system.",
      "type": "object",
      "properties": {
        "id": {
          "description": "Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces.",
          "type": "string"
        },
        "system": {
          "description": "The identification of the code system that defines the meaning of the symbol in the code.",
          "type": "string",
          "pattern": "^\\S*$"
        },
        "version": {
          "description": "The version of the code system which was used when choosing this code. Note that a well-maintained code system does not need the version reported, because the meaning of codes is consistent across versions. However this cannot consistently be assured, and when the meaning is not guaranteed to be consistent, the version SHOULD be exchanged.",
          "type": "string"
        },
        "code": {
          "description": "A symbol in syntax defined by the system. The symbol may be a predefined code or an expression in a syntax defined by the coding system (e.g. post-coordination).",
          "type": "string",
          "pattern": "^[^\\s]+( [^\\s]+)*$"
        },
        "display": {
          "description": "A representation of the meaning of the code in the system, following the rules of the system.",
          "type": "string"
        },
        "userSelected": {
          "description": "Indicates that this coding was chosen by a user directly - e.g. off a pick list of available items (codes or displays).",
          "type": "boolean",
          "pattern": "^true|false$"
        }
      }
    },
    "CodeableConcept": {
      "description": "A concept that may be defined by a formal reference to a terminology or ontology or may be provided by text.",
      "type": "object",
      "properties": {
        "id": {
          "description": "Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces.",
          "type": "string"
        },
        "coding": {
          "description": "A reference to a code defined by a terminology system.",
          "type": "array",
          "items": {
            "$ref": "#/definitions/Coding"
          }
        },
        "text": {
          "description": "A human language representation of the concept as seen/selected/uttered by the user who entered the data and/or which represents the intended meaning of the user.",
          "type": "string"
        }
      }
    },
    "ContactDetail": {
      "description": "Specifies contact information for a person or organization.",
      "properties": {
        "id": {
          "description": "Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces.",
          "type": "string"
        },
        "name": {
          "description": "The name of an individual to contact.",
          "type": "string"
        },
        "telecom": {
          "description": "The contact details for the individual (if a name was provided) or the organization.",
          "items": {
            "type": "object",
            "additionalProperties": true
          },
          "type": "array"
        }
      },
      "type": "object",
      "additionalProperties": false
    },
    "Quantity": {
      "description": "A measured amount (or an amount that can potentially be measured). Note that measured amounts include amounts that are not precisely quantified, including amounts involving arbitrary units and floating currencies.",
      "type": "object",
      "properties": {
        "id": {
          "description": "Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces.",
          "type": "string"
        },
        "value": {
          "description": "The value of the measured amount. The value includes an implicit precision in the presentation of the value.",
          "type": "number",
          "pattern": "^-?(0|[1-9][0-9]{0,17})(\\.[0-9]{1,17})?([eE][+-]?[0-9]{1,9})?$"
        },
        "comparator": {
          "description": "How the value should be understood and represented - whether the actual value is greater or less than the stated value due to measurement issues; e.g. if the comparator is \"\u003c\" , then the real value is \u003c stated value.",
          "type": "string",
          "enum": ["\u003c", "\u003c\u003d", "\u003e\u003d", "\u003e", "ad"]
        },
        "unit": {
          "description": "A human-readable form of the unit.",
          "type": "string"
        },
        "system": {
          "description": "The identification of the system that provides the coded form of the unit.",
          "type": "string",
          "pattern": "^\\S*$"
        },
        "code": {
          "description": "A computer processable form of the unit in some unit representation system.",
          "type": "string",
          "pattern": "^[^\\s]+( [^\\s]+)*$"
        }
      }
    },
    "Questionnaire_Item": {
      "description": "A structured set of questions intended to guide the collection of answers from end-users. Questionnaires provide detailed control over order, presentation, phraseology and grouping to allow coherent, consistent data collection.",
      "properties": {
        "id": {
          "description": "Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces.",
          "type": "string"
        },
        "linkId": {
          "description": "An identifier that is unique within the Questionnaire allowing linkage to the equivalent item in a QuestionnaireResponse resource.",
          "type": "string"
        },
        "definition": {
          "description": "This element is a URI that refers to an [ElementDefinition](elementdefinition.html) or to an [ObservationDefinition](observationdefinition.html) that provides information about this item, including information that might otherwise be included in the instance of the Questionnaire resource. A detailed description of the construction of the URI is shown in [Comments](questionnaire.html#definition), below.",
          "type": "string",
          "pattern": "^\\S*$"
        },
        "code": {
          "description": "A terminology code that corresponds to this group or question (e.g. a code from LOINC, which defines many questions and answers).",
          "items": {
            "$ref": "#/definitions/Coding"
          },
          "type": "array"
        },
        "prefix": {
          "description": "A short label for a particular group, question or set of display text within the questionnaire used for reference by the individual completing the questionnaire.",
          "type": "string"
        },
        "text": {
          "description": "The name of a section, the text of a question or text content for a display item.",
          "type": "string"
        },
        "type": {
          "description": "The type of questionnaire item this is - whether text for display, a grouping of other items or a particular type of data to be captured (string, integer, Coding, etc.).",
          "type": "string",
          "pattern": "^[^\\s]+( [^\\s]+)*$"
        },
        "enableWhen": {
          "description": "A constraint indicating that this item should only be enabled (displayed/allow answers to be captured) when the specified condition is true.",
          "type": "array",
          "items": {
            "$ref": "#/definitions/Questionnaire_EnableWhen"
          }
        },
        "enableBehavior": {
          "description": "Controls how multiple enableWhen values are interpreted -  whether all or any must be true.",
          "type": "string",
          "pattern": "^[^\\s]+( [^\\s]+)*$"
        },
        "disabledDisplay": {
          "description": "Indicates if and how items that are disabled (because enableWhen evaluates to \u0027false\u0027) should be displayed.",
          "type": "string",
          "pattern": "^[^\\s]+( [^\\s]+)*$"
        },
        "required": {
          "description": "An indication, if true, that the item must be present in a \"completed\" QuestionnaireResponse.  If false, the item may be skipped when answering the questionnaire.",
          "type": "boolean",
          "pattern": "^true|false$"
        },
        "repeats": {
          "description": "An indication, if true, that a QuestionnaireResponse for this item may include multiple answers associated with a single instance of this item (for question-type items) or multiple repetitions of the item (for group-type items).",
          "type": "boolean",
          "pattern": "^true|false$"
        },
        "readOnly": {
          "description": "An indication, when true, that the value cannot be changed by a human respondent to the Questionnaire.",
          "type": "boolean",
          "pattern": "^true|false$"
        },
        "maxLength": {
          "description": "The maximum number of characters that are permitted in the answer to be considered a \"valid\" QuestionnaireResponse.",
          "type": "number",
          "pattern": "^[0]|[-+]?[1-9][0-9]*$"
        },
        "answerConstraint": {
          "description": "For items that have a defined set of allowed answers (via answerOption or answerValueSet), indicates whether values *other* than those specified can be selected.",
          "type": "string",
          "pattern": "^[^\\s]+( [^\\s]+)*$"
        },
        "answerValueSet": {
          "description": "A reference to a value set containing a list of values representing permitted answers for a question.",
          "type": "string",
          "pattern": "^\\S*$"
        },
        "answerOption": {
          "description": "One of the permitted answers for the question.",
          "type": "array",
          "items": {
            "$ref": "#/definitions/Questionnaire_AnswerOption"
          }
        },
        "initial": {
          "description": "One or more values that should be pre-populated in the answer when initially rendering the questionnaire for user input.",
          "type": "array",
          "items": {
            "$ref": "#/definitions/Questionnaire_Initial"
          }
        },
        "item": {
          "description": "Text, questions and other groups to be nested beneath a question or group.",
          "type": "array",
          "items": {
            "$ref": "#/definitions/Questionnaire_Item"
          }
        }
      },
      "type": "object",
      "additionalProperties": true
    },
    "Questionnaire_EnableWhen": {
      "description": "A structured set of questions intended to guide the collection of answers from end-users. Questionnaires provide detailed control over order, presentation, phraseology and grouping to allow coherent, consistent data collection.",
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "id": {
          "description": "Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces.",
          "type": "string"
        },
        "question": {
          "description": "The linkId for the question whose answer (or lack of answer) governs whether this item is enabled.",
          "type": "string"
        },
        "operator": {
          "description": "Specifies the criteria by which the question is enabled.",
          "type": "string",
          "pattern": "^[^\\s]+( [^\\s]+)*$"
        },
        "answerBoolean": {
          "description": "A value that the referenced question is tested using the specified operator in order for the item to be enabled.  If there are multiple answers, a match on any of the answers suffices.  If different behavior is desired (all must match, at least 2 must match, etc.), consider using the enableWhenExpression extension.",
          "type": "boolean",
          "pattern": "^true|false$"
        },
        "answerDecimal": {
          "description": "A value that the referenced question is tested using the specified operator in order for the item to be enabled.  If there are multiple answers, a match on any of the answers suffices.  If different behavior is desired (all must match, at least 2 must match, etc.), consider using the enableWhenExpression extension.",
          "type": "number",
          "pattern": "^-?(0|[1-9][0-9]{0,17})(\\.[0-9]{1,17})?([eE][+-]?[0-9]{1,9})?$"
        },
        "answerInteger": {
          "description": "A value that the referenced question is tested using the specified operator in order for the item to be enabled.  If there are multiple answers, a match on any of the answers suffices.  If different behavior is desired (all must match, at least 2 must match, etc.), consider using the enableWhenExpression extension.",
          "type": "number",
          "pattern": "^[0]|[-+]?[1-9][0-9]*$"
        },
        "answerDate": {
          "description": "A value that the referenced question is tested using the specified operator in order for the item to be enabled.  If there are multiple answers, a match on any of the answers suffices.  If different behavior is desired (all must match, at least 2 must match, etc.), consider using the enableWhenExpression extension.",
          "type": "string",
          "pattern": "^([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)(-(0[1-9]|1[0-2])(-(0[1-9]|[1-2][0-9]|3[0-1]))?)?$"
        },
        "answerDateTime": {
          "description": "A value that the referenced question is tested using the specified operator in order for the item to be enabled.  If there are multiple answers, a match on any of the answers suffices.  If different behavior is desired (all must match, at least 2 must match, etc.), consider using the enableWhenExpression extension.",
          "type": "string",
          "pattern": "^([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)(-(0[1-9]|1[0-2])(-(0[1-9]|[1-2][0-9]|3[0-1])(T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]{1,9})?)?)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00)?)?)?$"
        },
        "answerTime": {
          "description": "A value that the referenced question is tested using the specified operator in order for the item to be enabled.  If there are multiple answers, a match on any of the answers suffices.  If different behavior is desired (all must match, at least 2 must match, etc.), consider using the enableWhenExpression extension.",
          "type": "string",
          "pattern": "^([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]{1,9})?$"
        },
        "answerString": {
          "description": "A value that the referenced question is tested using the specified operator in order for the item to be enabled.  If there are multiple answers, a match on any of the answers suffices.  If different behavior is desired (all must match, at least 2 must match, etc.), consider using the enableWhenExpression extension.",
          "type": "string",
          "pattern": "^^[\\s\\S]+$$"
        },
        "answerCoding": {
          "description": "A value that the referenced question is tested using the specified operator in order for the item to be enabled.  If there are multiple answers, a match on any of the answers suffices.  If different behavior is desired (all must match, at least 2 must match, etc.), consider using the enableWhenExpression extension.",
          "$ref": "#/definitions/Coding"
        },
        "answerQuantity": {
          "description": "A value that the referenced question is tested using the specified operator in order for the item to be enabled.  If there are multiple answers, a match on any of the answers suffices.  If different behavior is desired (all must match, at least 2 must match, etc.), consider using the enableWhenExpression extension.",
          "$ref": "#/definitions/Quantity"
        }
      }
    },
    "Questionnaire_AnswerOption": {
      "description": "A structured set of questions intended to guide the collection of answers from end-users. Questionnaires provide detailed control over order, presentation, phraseology and grouping to allow coherent, consistent data collection.",
      "type": "object",
      "additionalProperties": true,
      "properties": {
        "id": {
          "description": "Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces.",
          "type": "string"
        },
        "valueInteger": {
          "description": "A potential answer that\u0027s allowed as the answer to this question.",
          "type": "number",
          "pattern": "^[0]|[-+]?[1-9][0-9]*$"
        },
        "valueDate": {
          "description": "A potential answer that\u0027s allowed as the answer to this question.",
          "type": "string",
          "pattern": "^([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)(-(0[1-9]|1[0-2])(-(0[1-9]|[1-2][0-9]|3[0-1]))?)?$"
        },
        "valueTime": {
          "description": "A potential answer that\u0027s allowed as the answer to this question.",
          "type": "string",
          "pattern": "^([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]{1,9})?$"
        },
        "valueString": {
          "description": "A potential answer that\u0027s allowed as the answer to this question.",
          "type": "string",
          "pattern": "^^[\\s\\S]+$$"
        },
        "valueCoding": {
          "description": "A potential answer that\u0027s allowed as the answer to this question.",
          "$ref": "#/definitions/Coding"
        },
        "initialSelected": {
          "description": "Indicates whether the answer value is selected when the list of possible answers is initially shown.",
          "type": "boolean",
          "pattern": "^true|false$"
        }
      }
    },
    "Questionnaire_Initial": {
      "description": "A structured set of questions intended to guide the collection of answers from end-users. Questionnaires provide detailed control over order, presentation, phraseology and grouping to allow coherent, consistent data collection.",
      "type": "object",
      "additionalProperties": true,
      "properties": {
        "id": {
          "description": "Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces.",
          "type": "string"
        },
        "valueBoolean": {
          "description": "The actual value to for an initial answer.",
          "type": "boolean",
          "pattern": "^true|false$"
        },
        "valueDecimal": {
          "description": "The actual value to for an initial answer.",
          "type": "number",
          "pattern": "^-?(0|[1-9][0-9]{0,17})(\\.[0-9]{1,17})?([eE][+-]?[0-9]{1,9})?$"
        },
        "valueInteger": {
          "description": "The actual value to for an initial answer.",
          "type": "number",
          "pattern": "^[0]|[-+]?[1-9][0-9]*$"
        },
        "valueDate": {
          "description": "The actual value to for an initial answer.",
          "type": "string",
          "pattern": "^([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)(-(0[1-9]|1[0-2])(-(0[1-9]|[1-2][0-9]|3[0-1]))?)?$"
        },
        "valueDateTime": {
          "description": "The actual value to for an initial answer.",
          "type": "string",
          "pattern": "^([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)(-(0[1-9]|1[0-2])(-(0[1-9]|[1-2][0-9]|3[0-1])(T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]{1,9})?)?)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00)?)?)?$"
        },
        "valueTime": {
          "description": "The actual value to for an initial answer.",
          "type": "string",
          "pattern": "^([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]{1,9})?$"
        },
        "valueString": {
          "description": "The actual value to for an initial answer.",
          "type": "string",
          "pattern": "^^[\\s\\S]+$$"
        },
        "valueUri": {
          "description": "The actual value to for an initial answer.",
          "type": "string",
          "pattern": "^\\S*$"
        },
        "valueAttachment": {
          "description": "The actual value to for an initial answer.",
          "$ref": "#/definitions/Attachment"
        },
        "valueCoding": {
          "description": "The actual value to for an initial answer.",
          "$ref": "#/definitions/Coding"
        },
        "valueQuantity": {
          "description": "The actual value to for an initial answer.",
          "$ref": "#/definitions/Quantity"
        }
      }
    }
  }
}


path: references/snomed_guide.md
content: 
## SNOMED CT for Clinical Concepts

While LOINC is ideal for clinical questions and observations, **SNOMED CT** is the preferred terminology for clinical concepts like conditions, findings, and procedures.

### When to Use SNOMED CT vs LOINC

**Use LOINC for:**
- Clinical questions (e.g., "Do you have diabetes?")
- Observations and measurements (e.g., blood pressure, body weight)
- Standardized assessment instruments (PHQ-9, GAD-7)
- Answer lists for coded responses

**Use SNOMED CT for:**
- Clinical conditions and diagnoses (e.g., Diabetes mellitus, Hypertension)
- Clinical findings (e.g., Fever, Cough)
- Procedures (e.g., Appendectomy, Blood draw)
- Body structures (e.g., Heart, Lung)
- Answer options representing clinical concepts

### Searching for SNOMED CT Codes

**ALWAYS use the search script - NEVER suggest SNOMED codes from memory:**

```bash
# Search for a condition
python scripts/search_snomed.py "diabetes"

# Search with semantic tag filter
python scripts/search_snomed.py "appendectomy" --semantic-tag "procedure"

# Get FHIR-formatted output
python scripts/search_snomed.py "hypertension" --format fhir
```

### Using SNOMED CT in Questionnaires

Example with SNOMED CT coded answer options:

```json
{
  "linkId": "chronic-conditions",
  "type": "choice",
  "repeats": true,
  "code": [{
    "system": "http://loinc.org",
    "code": "75618-9",
    "display": "Do you have any of these conditions?"
  }],
  "text": "Do you have any of these conditions?",
  "answerOption": [
    {
      "valueCoding": {
        "system": "http://snomed.info/sct",
        "code": "73211009",
        "display": "Diabetes mellitus"
      }
    },
    {
      "valueCoding": {
        "system": "http://snomed.info/sct",
        "code": "38341003",
        "display": "Hypertension"
      }
    },
    {
      "valueCoding": {
        "system": "http://snomed.info/sct",
        "code": "195967001",
        "display": "Asthma"
      }
    }
  ]
}
```

### SNOMED CT Semantic Tags

SNOMED CT concepts include semantic tags to clarify their meaning:

- **disorder** - Diseases and conditions (e.g., "Diabetes mellitus (disorder)")
- **finding** - Clinical findings (e.g., "Fever (finding)")
- **procedure** - Clinical procedures (e.g., "Appendectomy (procedure)")
- **body structure** - Anatomical structures (e.g., "Heart structure (body structure)")
- **substance** - Chemical substances (e.g., "Glucose (substance)")
- **organism** - Organisms (e.g., "Virus (organism)")

Use the `--semantic-tag` filter to narrow your search results.

### SNOMED CT System URI

Always use the official SNOMED CT system URI:
```
http://snomed.info/sct
```

### Best Practices

1. **Always search, never guess** - Use `search_snomed.py` for all SNOMED codes
2. **Choose the right granularity** - Use specific codes when appropriate (e.g., "Type 2 diabetes mellitus" vs generic "Diabetes mellitus")
3. **Consider semantic tags** - Filter by semantic tag to find the right concept type
4. **Combine with LOINC** - Use LOINC for the question code and SNOMED for answer options

### Links

- **SNOMED-LOINC Mapping**: For converting between terminologies
- **SNOMED CT Browser**: https://browser.ihtsdotools.org/
- **SNOMED International**: https://www.snomed.org/

path: requirements.txt
content: jsonschema>=4.0.0


path: scripts/create_custom_codesystem.py
content: #!/usr/bin/env python3
"""
Create reusable CodeSystem and ValueSet resources (opt-in).

This script generates custom CodeSystem and ValueSet resources in the Welshare
namespace for use cases where reusable codes across multiple questionnaires are
explicitly desired.

NOTE: For most custom answer lists, prefer inline answerOption with system-less
valueCoding directly in the questionnaire. Only use this script when the user
explicitly requests reusable codes that can be shared across questionnaires.

URL Base: http://codes.welshare.app
All resources created by this script use the Welshare URL base for consistency.

Usage:
    python create_custom_codesystem.py --id routine-preference --category brainhealth --title "Routine Preference Scale" --codes "prefer-routines:Prefer routines,sometimes-new:Sometimes seek new challenges"
    python create_custom_codesystem.py --interactive
"""

import argparse
import json
import re
import sys
from datetime import datetime
from pathlib import Path
from typing import Dict, List, Tuple


def sanitize_id(text: str) -> str:
    """Convert text to a valid FHIR id (lowercase, hyphens, alphanumeric)."""
    # Convert to lowercase
    text = text.lower()
    # Replace spaces and underscores with hyphens
    text = re.sub(r'[\s_]+', '-', text)
    # Remove any non-alphanumeric characters except hyphens and dots
    text = re.sub(r'[^a-z0-9\-.]', '', text)
    # Remove leading/trailing hyphens
    text = text.strip('-')
    # Limit to 64 characters
    return text[:64]


def sanitize_code(text: str) -> str:
    """Convert text to a valid code (lowercase, hyphens, alphanumeric)."""
    # Similar to id but more restrictive
    text = text.lower()
    text = re.sub(r'[\s_]+', '-', text)
    text = re.sub(r'[^a-z0-9\-]', '', text)
    text = text.strip('-')
    return text


def create_codesystem(
    id: str,
    category: str,
    title: str,
    description: str,
    concepts: List[Tuple[str, str]],
    publisher: str = "Welshare",
    experimental: bool = False
) -> Dict:
    """
    Create a CodeSystem resource.

    Uses the Welshare URL base: http://codes.welshare.app for consistency
    across reusable code systems.

    Args:
        id: CodeSystem identifier (e.g., "routine-preference")
        category: Category/domain (e.g., "brainhealth")
        title: Human-readable title
        description: Description of the code system
        concepts: List of (code, display) tuples
        publisher: Organization name
        experimental: Whether this is experimental

    Returns:
        CodeSystem resource as dict
    """
    today = datetime.now().strftime("%Y-%m-%d")

    # Create name from title (CamelCase, no spaces)
    name = ''.join(word.capitalize() for word in title.split())
    name = re.sub(r'[^A-Za-z0-9]', '', name)

    # URL base: http://codes.welshare.app
    codesystem = {
        "resourceType": "CodeSystem",
        "id": id,
        "url": f"http://codes.welshare.app/CodeSystem/{category}/{id}.json",
        "version": "1.0.0",
        "name": name,
        "title": title,
        "status": "active",
        "experimental": experimental,
        "date": today,
        "publisher": publisher,
        "description": description,
        "content": "complete",
        "concept": [
            {
                "code": code,
                "display": display
            }
            for code, display in concepts
        ]
    }

    return codesystem


def create_valueset(
    codesystem_id: str,
    category: str,
    title: str,
    description: str = None
) -> Dict:
    """
    Create a ValueSet resource that references a CodeSystem.

    Uses the Welshare URL base: http://codes.welshare.app for consistency
    across reusable value sets.

    Args:
        codesystem_id: ID of the CodeSystem to reference
        category: Category/domain (same as CodeSystem)
        title: Human-readable title (usually same as CodeSystem)
        description: Optional description

    Returns:
        ValueSet resource as dict
    """
    valueset_id = f"vs-{codesystem_id}"
    # URL base: http://codes.welshare.app
    codesystem_url = f"http://codes.welshare.app/CodeSystem/{category}/{codesystem_id}.json"

    valueset = {
        "resourceType": "ValueSet",
        "id": valueset_id,
        "url": f"http://codes.welshare.app/ValueSet/{category}/{codesystem_id}.json",
        "status": "active",
        "compose": {
            "include": [
                {
                    "system": codesystem_url
                }
            ]
        }
    }

    if title:
        valueset["title"] = f"ValueSet - {title}"

    if description:
        valueset["description"] = description

    return valueset


def parse_codes_string(codes_str: str) -> List[Tuple[str, str]]:
    """
    Parse codes from string format.

    Format: "code1:Display 1,code2:Display 2,code3:Display 3"

    Returns:
        List of (code, display) tuples
    """
    concepts = []
    for pair in codes_str.split(','):
        pair = pair.strip()
        if ':' not in pair:
            print(f"Warning: Invalid code format '{pair}'. Expected 'code:display'", file=sys.stderr)
            continue

        code, display = pair.split(':', 1)
        code = code.strip()
        display = display.strip()

        if not code or not display:
            print(f"Warning: Empty code or display in '{pair}'", file=sys.stderr)
            continue

        concepts.append((code, display))

    return concepts


def interactive_mode() -> Tuple[str, str, str, str, List[Tuple[str, str]]]:
    """
    Interactive mode to gather input from user.

    Returns:
        Tuple of (id, category, title, description, concepts)
    """
    print("\n=== Create Reusable CodeSystem & ValueSet ===")
    print("Note: For simple custom answer lists, consider using inline answerOption")
    print("      with system-less valueCoding instead (simpler, no external resources).\n")

    # Get title first (most natural)
    title = input("Title (e.g., 'Routine Preference Scale'): ").strip()
    if not title:
        print("Error: Title is required", file=sys.stderr)
        sys.exit(1)

    # Suggest ID based on title
    suggested_id = sanitize_id(title)
    id_input = input(f"ID [default: {suggested_id}]: ").strip()
    id = id_input if id_input else suggested_id

    # Category
    category = input("Category/domain (e.g., 'brainhealth', 'social'): ").strip()
    if not category:
        print("Error: Category is required", file=sys.stderr)
        sys.exit(1)
    category = sanitize_id(category)

    # Description
    description = input("Description: ").strip()
    if not description:
        description = f"Custom code system for {title.lower()}"

    # Concepts
    print("\nEnter concepts (codes and displays). Press Enter with empty code to finish.")
    concepts = []
    idx = 1

    while True:
        print(f"\nConcept {idx}:")
        code = input("  Code (or Enter to finish): ").strip()
        if not code:
            break

        # Suggest sanitized version
        suggested_code = sanitize_code(code)
        if code != suggested_code:
            confirm = input(f"  Use sanitized code '{suggested_code}'? [Y/n]: ").strip().lower()
            if confirm != 'n':
                code = suggested_code

        display = input("  Display text: ").strip()
        if not display:
            print("  Warning: Display text is required. Skipping this concept.")
            continue

        concepts.append((code, display))
        idx += 1

    if not concepts:
        print("\nError: At least one concept is required", file=sys.stderr)
        sys.exit(1)

    return id, category, title, description, concepts


def main():
    parser = argparse.ArgumentParser(
        description="Create reusable CodeSystem and ValueSet resources (opt-in for cross-questionnaire code sharing)",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
NOTE: For most custom answer lists, prefer inline answerOption with system-less
      valueCoding. Only use this script when reusable codes are explicitly needed.

Examples:
  # Interactive mode
  python create_custom_codesystem.py --interactive

  # Command-line mode
  python create_custom_codesystem.py \\
    --id routine-preference \\
    --category brainhealth \\
    --title "Routine Preference Scale" \\
    --description "Scale for assessing preference for routines" \\
    --codes "prefer-routines:Prefer routines,sometimes-new:Sometimes seek new challenges,frequently-new:Frequently seek new challenges"

  # Output to specific directory
  python create_custom_codesystem.py --interactive --output ./custom-codes/

  # Mark as experimental
  python create_custom_codesystem.py --interactive --experimental
        """
    )

    parser.add_argument("--interactive", "-i", action="store_true",
                       help="Interactive mode - prompt for all values")
    parser.add_argument("--id", help="CodeSystem ID (e.g., 'routine-preference')")
    parser.add_argument("--category", help="Category/domain (e.g., 'brainhealth')")
    parser.add_argument("--title", help="Human-readable title")
    parser.add_argument("--description", help="Description of the code system")
    parser.add_argument("--codes", help="Comma-separated codes in format 'code1:Display 1,code2:Display 2'")
    parser.add_argument("--publisher", default="Welshare", help="Publisher name (default: Welshare)")
    parser.add_argument("--experimental", action="store_true", help="Mark as experimental")
    parser.add_argument("--output", "-o", type=Path, default=".",
                       help="Output directory (default: current directory)")

    args = parser.parse_args()

    # Gather input
    if args.interactive:
        id, category, title, description, concepts = interactive_mode()
        experimental = args.experimental
        publisher = args.publisher
    else:
        # Validate required arguments
        if not all([args.id, args.category, args.title, args.codes]):
            parser.error("Non-interactive mode requires --id, --category, --title, and --codes")

        id = args.id
        category = args.category
        title = args.title
        description = args.description if args.description else f"Custom code system for {title.lower()}"
        concepts = parse_codes_string(args.codes)
        experimental = args.experimental
        publisher = args.publisher

        if not concepts:
            print("Error: No valid concepts provided", file=sys.stderr)
            sys.exit(1)

    # Create resources
    codesystem = create_codesystem(
        id=id,
        category=category,
        title=title,
        description=description,
        concepts=concepts,
        publisher=publisher,
        experimental=experimental
    )

    valueset = create_valueset(
        codesystem_id=id,
        category=category,
        title=title,
        description=f"ValueSet containing all codes from {title}"
    )

    # Ensure output directory exists
    output_dir = Path(args.output)
    output_dir.mkdir(parents=True, exist_ok=True)

    # Write files
    codesystem_file = output_dir / f"CodeSystem-{id}.json"
    valueset_file = output_dir / f"ValueSet-vs-{id}.json"

    with open(codesystem_file, 'w') as f:
        json.dump(codesystem, f, indent=2)

    with open(valueset_file, 'w') as f:
        json.dump(valueset, f, indent=2)

    # Success message
    print(f"\n✅ Successfully created reusable code system!\n")
    print(f"CodeSystem: {codesystem_file}")
    print(f"  URL: {codesystem['url']}")
    print(f"  Concepts: {len(concepts)}")
    print(f"\nValueSet: {valueset_file}")
    print(f"  URL: {valueset['url']}")
    print(f"\n📋 Usage in Questionnaire (with ValueSet reference):")
    print(f'   "answerValueSet": "{valueset["url"]}"')
    print(f"\n   Or with inline coding (add system to each valueCoding):")
    print(f'   "answerOption": [')
    print(f'     {{"valueCoding": {{')
    print(f'       "system": "{codesystem["url"]}",')
    print(f'       "code": "{concepts[0][0]}",')
    print(f'       "display": "{concepts[0][1]}"')
    print(f'     }}}}')
    print(f'   ]')
    print(f"\n💡 Tip: For simpler use cases, consider inline answerOption without system:"
          f'\n   {{"valueCoding": {{"code": "{concepts[0][0]}", "display": "{concepts[0][1]}"}}}}')
    print(f"   This avoids external resource dependencies.")


if __name__ == "__main__":
    main()


path: scripts/extract_loinc_codes.py
content: #!/usr/bin/env python3
"""
Extract and analyze all LOINC codes from a FHIR Questionnaire.

This script recursively extracts all LOINC codes from a FHIR Questionnaire JSON file,
categorizes them by type (question codes vs answer codes), and provides detailed
information about where each code is used. Optionally validates codes against the
LOINC database.

Usage:
    python extract_loinc_codes.py questionnaire.json
    python extract_loinc_codes.py questionnaire.json --output codes.json
    python extract_loinc_codes.py questionnaire.json --format table
    python extract_loinc_codes.py questionnaire.json --validate
"""

import argparse
import json
import sys
from pathlib import Path
from typing import Dict, List, Set, Optional
from collections import defaultdict


def extract_loinc_codes(item: dict, codes_dict: Dict, path: str = "") -> None:
    """
    Recursively extract LOINC codes from questionnaire items.

    Args:
        item: Questionnaire item to process
        codes_dict: Dictionary to store extracted codes
        path: Current path in the questionnaire hierarchy
    """
    link_id = item.get('linkId', 'unknown')
    current_path = f"{path}/{link_id}" if path else link_id

    # Extract question codes from the 'code' array
    if 'code' in item:
        for code in item['code']:
            if code.get('system') == 'http://loinc.org' and 'code' in code:
                loinc_code = code['code']
                display = code.get('display', 'No display text')

                if loinc_code not in codes_dict:
                    codes_dict[loinc_code] = {
                        'locations': [],
                        'display': display,
                        'type': 'question'
                    }
                codes_dict[loinc_code]['locations'].append(current_path)

    # Extract answer option codes from answerOption array
    if 'answerOption' in item:
        for answer in item['answerOption']:
            if 'valueCoding' in answer:
                coding = answer['valueCoding']
                if coding.get('system') == 'http://loinc.org' and 'code' in coding:
                    loinc_code = coding['code']
                    display = coding.get('display', 'No display text')

                    if loinc_code not in codes_dict:
                        codes_dict[loinc_code] = {
                            'locations': [],
                            'display': display,
                            'type': 'answer'
                        }
                    codes_dict[loinc_code]['locations'].append(f"{current_path} (answer)")

    # Extract codes from answerValueSet (if present)
    if 'answerValueSet' in item:
        valueset_url = item['answerValueSet']
        # Note: This doesn't extract actual codes, just notes the ValueSet reference
        # Could be enhanced to fetch and expand ValueSets

    # Recursively process nested items
    if 'item' in item:
        for child in item['item']:
            extract_loinc_codes(child, codes_dict, current_path)


def load_questionnaire(file_path: Path) -> Optional[dict]:
    """
    Load and parse a FHIR Questionnaire JSON file.

    Args:
        file_path: Path to the questionnaire file

    Returns:
        Parsed questionnaire dictionary or None on error
    """
    try:
        with open(file_path, 'r', encoding='utf-8') as f:
            questionnaire = json.load(f)

        # Basic validation
        if questionnaire.get('resourceType') != 'Questionnaire':
            print(f"Warning: Resource type is '{questionnaire.get('resourceType')}', expected 'Questionnaire'",
                  file=sys.stderr)

        return questionnaire

    except FileNotFoundError:
        print(f"Error: File not found: {file_path}", file=sys.stderr)
        return None
    except json.JSONDecodeError as e:
        print(f"Error: Invalid JSON in file: {e}", file=sys.stderr)
        return None
    except Exception as e:
        print(f"Error loading questionnaire: {e}", file=sys.stderr)
        return None


def extract_all_codes(questionnaire: dict) -> Dict[str, dict]:
    """
    Extract all LOINC codes from a questionnaire.

    Args:
        questionnaire: Parsed questionnaire dictionary

    Returns:
        Dictionary of LOINC codes with metadata
    """
    codes = {}

    if 'item' in questionnaire:
        for item in questionnaire['item']:
            extract_loinc_codes(item, codes)

    return codes


def format_table_output(codes: Dict[str, dict]) -> str:
    """Format codes as a readable table."""
    if not codes:
        return "No LOINC codes found in questionnaire."

    # Separate by type
    question_codes = {k: v for k, v in sorted(codes.items()) if v['type'] == 'question'}
    answer_codes = {k: v for k, v in sorted(codes.items()) if v['type'] == 'answer'}

    output = []
    output.append("=" * 80)
    output.append("LOINC CODES FOUND IN QUESTIONNAIRE")
    output.append("=" * 80)
    output.append(f"\nTotal unique LOINC codes: {len(codes)}")
    output.append(f"  Question codes: {len(question_codes)}")
    output.append(f"  Answer codes: {len(answer_codes)}")

    if question_codes:
        output.append("\n" + "=" * 80)
        output.append("QUESTION CODES")
        output.append("=" * 80)
        for code, info in question_codes.items():
            output.append(f"\n{code}: {info['display']}")
            output.append(f"  Used in: {', '.join(info['locations'])}")

    if answer_codes:
        output.append("\n" + "=" * 80)
        output.append("ANSWER CODES")
        output.append("=" * 80)

        # Group by unique code
        unique_answer_codes = {}
        for code, info in answer_codes.items():
            if code not in unique_answer_codes:
                unique_answer_codes[code] = {
                    'display': info['display'],
                    'locations': info['locations']
                }

        for code, info in sorted(unique_answer_codes.items()):
            output.append(f"\n{code}: {info['display']}")
            if len(info['locations']) > 1:
                output.append(f"  Used in {len(info['locations'])} locations")

    output.append("\n")
    return "\n".join(output)


def format_summary_output(codes: Dict[str, dict]) -> str:
    """Format a concise summary of codes."""
    question_codes = [k for k, v in codes.items() if v['type'] == 'question']
    answer_codes = [k for k, v in codes.items() if v['type'] == 'answer']

    output = []
    output.append(f"Total LOINC codes: {len(codes)}")
    output.append(f"Question codes ({len(question_codes)}): {', '.join(sorted(question_codes)) if question_codes else 'None'}")
    output.append(f"Answer codes ({len(answer_codes)}): {', '.join(sorted(set(answer_codes))) if answer_codes else 'None'}")

    return "\n".join(output)


def format_json_output(codes: Dict[str, dict]) -> str:
    """Format codes as JSON."""
    question_codes = {k: v for k, v in sorted(codes.items()) if v['type'] == 'question'}
    answer_codes = {k: v for k, v in sorted(codes.items()) if v['type'] == 'answer'}

    output = {
        'total': len(codes),
        'question_codes': question_codes,
        'answer_codes': answer_codes,
        'all_codes': sorted(codes.keys())
    }

    return json.dumps(output, indent=2)


def validate_codes(codes: Dict[str, dict]) -> None:
    """
    Validate LOINC codes using search_loinc.py.

    Note: This requires the search_loinc module to be available.
    """
    try:
        # Import the search_loinc module
        sys.path.insert(0, str(Path(__file__).parent))
        from search_loinc import search_loinc

        print("\n" + "=" * 80)
        print("VALIDATING LOINC CODES")
        print("=" * 80)

        for code in sorted(codes.keys()):
            # Search for exact code
            results = search_loinc(code, limit=1)

            if results and results[0]['code'] == code:
                status = "✓ VALID"
                official_display = results[0]['display']

                if codes[code]['display'] != official_display:
                    print(f"{code}: {status}")
                    print(f"  Current: {codes[code]['display']}")
                    print(f"  Official: {official_display}")
                else:
                    print(f"{code}: {status} - {official_display}")
            else:
                print(f"{code}: ✗ NOT FOUND")
                print(f"  Display: {codes[code]['display']}")

        print("\n")

    except ImportError:
        print("Warning: Could not import search_loinc module. Skipping validation.", file=sys.stderr)
    except Exception as e:
        print(f"Warning: Validation failed: {e}", file=sys.stderr)


def main():
    parser = argparse.ArgumentParser(
        description="Extract and analyze LOINC codes from FHIR Questionnaires",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Examples:
  # Extract and display as table
  python extract_loinc_codes.py questionnaire.json

  # Save to JSON file
  python extract_loinc_codes.py questionnaire.json --output codes.json

  # Show summary only
  python extract_loinc_codes.py questionnaire.json --format summary

  # Validate codes against LOINC database
  python extract_loinc_codes.py questionnaire.json --validate
        """
    )

    parser.add_argument(
        "questionnaire",
        type=Path,
        help="Path to FHIR Questionnaire JSON file"
    )

    parser.add_argument(
        "-o", "--output",
        type=Path,
        help="Output file path (default: print to stdout)"
    )

    parser.add_argument(
        "-f", "--format",
        choices=["table", "json", "summary"],
        default="table",
        help="Output format (default: table)"
    )

    parser.add_argument(
        "-v", "--validate",
        action="store_true",
        help="Validate codes against LOINC database (requires network access)"
    )

    args = parser.parse_args()

    # Load questionnaire
    questionnaire = load_questionnaire(args.questionnaire)
    if questionnaire is None:
        sys.exit(1)

    # Extract codes
    codes = extract_all_codes(questionnaire)

    if not codes:
        print("No LOINC codes found in questionnaire.", file=sys.stderr)
        sys.exit(0)

    # Format output
    if args.format == "table":
        output = format_table_output(codes)
    elif args.format == "json":
        output = format_json_output(codes)
    elif args.format == "summary":
        output = format_summary_output(codes)
    else:
        output = format_json_output(codes)

    # Write or print output
    if args.output:
        try:
            args.output.parent.mkdir(parents=True, exist_ok=True)
            with open(args.output, 'w', encoding='utf-8') as f:
                f.write(output)
            print(f"Extracted codes saved to: {args.output}")
        except Exception as e:
            print(f"Error writing output file: {e}", file=sys.stderr)
            sys.exit(1)
    else:
        print(output)

    # Validate if requested
    if args.validate:
        validate_codes(codes)


if __name__ == '__main__':
    main()


path: scripts/query_valueset.py
content: #!/usr/bin/env python3
"""
Query LOINC answer lists and FHIR ValueSets.

This script helps discover answer options for LOINC codes and search for ValueSets.
It uses the tx.fhir.org terminology server which has comprehensive LOINC support.

Usage:
    # Find answer options for a LOINC code
    python query_valueset.py --loinc-code "72166-2"

    # Search for answer lists by text
    python query_valueset.py --search "smoking"

    # Expand a specific ValueSet
    python query_valueset.py --expand "http://loinc.org/vs/LL2201-3"
"""

import argparse
import json
import sys
from typing import Any, Dict, List, Optional
from urllib import request, parse, error


# Default FHIR terminology server with comprehensive LOINC support
DEFAULT_SERVER = "https://tx.fhir.org/r4"


def lookup_loinc_answerlist(loinc_code: str, server_url: str = DEFAULT_SERVER) -> Optional[Dict[str, str]]:
    """
    Look up a LOINC code to find its associated answer list ID and display name.

    Args:
        loinc_code: The LOINC code (e.g., "72166-2")
        server_url: FHIR terminology server base URL

    Returns:
        Dictionary with 'answerlist_id' and 'display', or None if not found
    """
    params = {
        "system": "http://loinc.org",
        "code": loinc_code
    }

    url = f"{server_url}/CodeSystem/$lookup?{parse.urlencode(params)}"

    try:
        print(f"Accessing: {url}", file=sys.stderr)
        req = request.Request(url)
        req.add_header("Accept", "application/fhir+json")

        with request.urlopen(req, timeout=30) as response:
            data = json.loads(response.read().decode())

        if data.get("resourceType") != "Parameters":
            print(f"Error: Expected Parameters resource, got {data.get('resourceType')}", file=sys.stderr)
            return None

        # Extract display name and answer list
        display = None
        answerlist_id = None

        for param in data.get("parameter", []):
            # Get display name
            if param.get("name") == "display":
                display = param.get("valueString")

            # Find the AnswerList property
            if param.get("name") == "property":
                parts = param.get("part", [])
                code_part = next((p for p in parts if p.get("name") == "code" and p.get("valueCode") == "AnswerList"), None)
                if code_part:
                    value_part = next((p for p in parts if p.get("name") == "value"), None)
                    if value_part:
                        answerlist_id = value_part.get("valueCode")

        if answerlist_id:
            return {
                "answerlist_id": answerlist_id,
                "display": display or f"LOINC {loinc_code}"
            }

        return None

    except error.HTTPError as e:
        print(f"HTTP Error {e.code} when accessing {url}", file=sys.stderr)
        print(f"Reason: {e.reason}", file=sys.stderr)
        if e.code == 404:
            print(f"LOINC code {loinc_code} not found on this server", file=sys.stderr)
        elif e.code == 403:
            print("Access forbidden - server may not allow this operation", file=sys.stderr)
        return None
    except error.URLError as e:
        print(f"Network error when accessing {url}", file=sys.stderr)
        print(f"Error details: {e.reason}", file=sys.stderr)
        if "SSL" in str(e.reason) or "ssl" in str(e.reason):
            print("SSL/TLS handshake failed - this server may not be accessible from your environment", file=sys.stderr)
            print("Try using an alternative server with --server flag:", file=sys.stderr)
            print("  --server https://hapi.fhir.org/baseR4", file=sys.stderr)
            print("  --server https://r4.ontoserver.csiro.au/fhir", file=sys.stderr)
        return None
    except json.JSONDecodeError as e:
        print(f"Error parsing JSON response from {url}", file=sys.stderr)
        print(f"Parse error: {e}", file=sys.stderr)
        return None
    except Exception as e:
        print(f"Unexpected error when accessing {url}", file=sys.stderr)
        print(f"Error: {type(e).__name__}: {e}", file=sys.stderr)
        return None


def expand_valueset(valueset_url: str, server_url: str = DEFAULT_SERVER) -> Optional[Dict]:
    """
    Expand a ValueSet to show all contained codes.

    Args:
        valueset_url: ValueSet canonical URL (e.g., "http://loinc.org/vs/LL2201-3")
        server_url: FHIR terminology server base URL

    Returns:
        Expanded ValueSet resource or None if not found
    """
    params = {
        "url": valueset_url
    }

    url = f"{server_url}/ValueSet/$expand?{parse.urlencode(params)}"

    try:
        print(f"Accessing: {url}", file=sys.stderr)
        req = request.Request(url)
        req.add_header("Accept", "application/fhir+json")

        with request.urlopen(req, timeout=30) as response:
            data = json.loads(response.read().decode())

        if data.get("resourceType") == "OperationOutcome":
            print(f"Server returned OperationOutcome for {valueset_url}", file=sys.stderr)
            issues = data.get("issue", [])
            for issue in issues:
                print(f"  Issue: {issue.get('details', {}).get('text', 'Unknown error')}", file=sys.stderr)
            return None

        return data

    except error.HTTPError as e:
        print(f"HTTP Error {e.code} when accessing {url}", file=sys.stderr)
        print(f"Reason: {e.reason}", file=sys.stderr)
        if e.code == 404:
            print(f"ValueSet not found: {valueset_url}", file=sys.stderr)
        return None
    except error.URLError as e:
        print(f"Network error when accessing {url}", file=sys.stderr)
        print(f"Error details: {e.reason}", file=sys.stderr)
        if "SSL" in str(e.reason) or "ssl" in str(e.reason):
            print("SSL/TLS handshake failed - this server may not be accessible from your environment", file=sys.stderr)
            print("Try using an alternative server with --server flag:", file=sys.stderr)
            print("  --server https://hapi.fhir.org/baseR4", file=sys.stderr)
            print("  --server https://r4.ontoserver.csiro.au/fhir", file=sys.stderr)
        return None
    except json.JSONDecodeError as e:
        print(f"Error parsing JSON response from {url}", file=sys.stderr)
        print(f"Parse error: {e}", file=sys.stderr)
        return None
    except Exception as e:
        print(f"Unexpected error when accessing {url}", file=sys.stderr)
        print(f"Error: {type(e).__name__}: {e}", file=sys.stderr)
        return None


def search_valuesets(search_term: str, server_url: str = DEFAULT_SERVER, limit: int = 10) -> List[Dict]:
    """
    Search for ValueSets by title or name.

    Args:
        search_term: Search term for ValueSet title/name
        server_url: FHIR terminology server base URL
        limit: Maximum number of results

    Returns:
        List of ValueSet resources
    """
    params = {
        "title:contains": search_term,
        "_count": str(limit),
        "_summary": "true"
    }

    url = f"{server_url}/ValueSet?{parse.urlencode(params)}"

    try:
        print(f"Accessing: {url}", file=sys.stderr)
        req = request.Request(url)
        req.add_header("Accept", "application/fhir+json")

        with request.urlopen(req, timeout=30) as response:
            bundle = json.loads(response.read().decode())

        if bundle.get("resourceType") != "Bundle":
            print(f"Error: Expected Bundle, got {bundle.get('resourceType')}", file=sys.stderr)
            return []

        entries = bundle.get("entry", [])
        if not entries:
            print(f"No ValueSets found matching '{search_term}'", file=sys.stderr)

        return entries

    except error.HTTPError as e:
        print(f"HTTP Error {e.code} when accessing {url}", file=sys.stderr)
        print(f"Reason: {e.reason}", file=sys.stderr)
        return []
    except error.URLError as e:
        print(f"Network error when accessing {url}", file=sys.stderr)
        print(f"Error details: {e.reason}", file=sys.stderr)
        if "SSL" in str(e.reason) or "ssl" in str(e.reason):
            print("SSL/TLS handshake failed - this server may not be accessible from your environment", file=sys.stderr)
            print("Try using an alternative server with --server flag:", file=sys.stderr)
            print("  --server https://hapi.fhir.org/baseR4", file=sys.stderr)
            print("  --server https://r4.ontoserver.csiro.au/fhir", file=sys.stderr)
        return []
    except json.JSONDecodeError as e:
        print(f"Error parsing JSON response from {url}", file=sys.stderr)
        print(f"Parse error: {e}", file=sys.stderr)
        return []
    except Exception as e:
        print(f"Unexpected error when accessing {url}", file=sys.stderr)
        print(f"Error: {type(e).__name__}: {e}", file=sys.stderr)
        return []


def format_expansion(valueset: Dict, show_full: bool = False) -> str:
    """Format an expanded ValueSet showing answer options."""
    output = []

    # Header information
    output.append(f"ValueSet: {valueset.get('name', 'N/A')}")
    output.append(f"URL: {valueset.get('url', 'N/A')}")
    output.append(f"Version: {valueset.get('version', 'N/A')}")
    output.append("")

    # Get expansion
    expansion = valueset.get("expansion", {})
    contains = expansion.get("contains", [])

    if not contains:
        output.append("No codes found in expansion.")
        return "\n".join(output)

    output.append(f"Answer Options ({len(contains)} total):")
    output.append("-" * 80)

    # Format codes
    for item in contains:
        code = item.get("code", "")
        display = item.get("display", "")
        system = item.get("system", "")

        if show_full:
            output.append(f"Code:    {code}")
            output.append(f"Display: {display}")
            output.append(f"System:  {system}")
            output.append("-" * 80)
        else:
            output.append(f"  {code:<15} {display}")

    return "\n".join(output)


def format_fhir_codings(valueset: Dict) -> str:
    """Format expansion as FHIR Coding objects for use in Questionnaires."""
    expansion = valueset.get("expansion", {})
    contains = expansion.get("contains", [])

    codings = []
    for item in contains:
        codings.append({
            "system": item.get("system"),
            "code": item.get("code"),
            "display": item.get("display")
        })

    return json.dumps(codings, indent=2)


def format_questionnaire_item(loinc_code: str, loinc_display: str, valueset_url: str, valueset: Dict) -> str:
    """Format as a FHIR Questionnaire item with answerValueSet."""
    expansion = valueset.get("expansion", {})
    contains = expansion.get("contains", [])

    item = {
        "linkId": loinc_code.replace("-", "_"),
        "type": "choice",
        "code": [{
            "system": "http://loinc.org",
            "code": loinc_code,
            "display": loinc_display
        }],
        "text": loinc_display,
        "answerValueSet": valueset_url,
        "_answerValueSet_preview": {
            "description": f"This ValueSet contains {len(contains)} answer options",
            "options": [{"code": c.get("code"), "display": c.get("display")} for c in contains[:5]]
        }
    }

    return json.dumps(item, indent=2)


def main():
    parser = argparse.ArgumentParser(
        description="Query LOINC answer lists and FHIR ValueSets",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Examples:
  # Find answer options for a LOINC code
  python query_valueset.py --loinc-code "72166-2"

  # Get FHIR Coding format for use in Questionnaires
  python query_valueset.py --loinc-code "72166-2" --format fhir

  # Get as a Questionnaire item template
  python query_valueset.py --loinc-code "72166-2" --format questionnaire

  # Search for ValueSets by keyword
  python query_valueset.py --search "smoking"

  # Expand a specific ValueSet URL
  python query_valueset.py --expand "http://loinc.org/vs/LL2201-3"

  # Use a different FHIR server
  python query_valueset.py --loinc-code "72166-2" --server https://hapi.fhir.org/baseR4
        """
    )

    parser.add_argument("--loinc-code", help="LOINC code to look up (finds associated answer list)")
    parser.add_argument("--search", help="Search term for ValueSets")
    parser.add_argument("--expand", help="ValueSet URL to expand")
    parser.add_argument("--server", default=DEFAULT_SERVER, help=f"FHIR server URL (default: {DEFAULT_SERVER})")
    parser.add_argument("--format", choices=["table", "json", "fhir", "questionnaire"], default="table",
                       help="Output format (default: table)")
    parser.add_argument("--limit", type=int, default=10, help="Maximum search results (default: 10)")
    parser.add_argument("--full", action="store_true", help="Show full details in table format")

    args = parser.parse_args()

    # Validate arguments
    modes = [args.loinc_code, args.search, args.expand]
    if sum(bool(m) for m in modes) != 1:
        parser.error("Exactly one of --loinc-code, --search, or --expand must be specified")

    # Mode 1: Look up LOINC code and find its answer list
    if args.loinc_code:
        print(f"Looking up LOINC code: {args.loinc_code}", file=sys.stderr)
        print(f"Using server: {args.server}", file=sys.stderr)
        print("", file=sys.stderr)

        lookup_result = lookup_loinc_answerlist(args.loinc_code, args.server)

        if not lookup_result:
            print("", file=sys.stderr)
            print("=" * 80, file=sys.stderr)
            print(f"RESULT: No answer list found for LOINC code {args.loinc_code}", file=sys.stderr)
            print("", file=sys.stderr)
            print("This could mean:", file=sys.stderr)
            print("  1. This LOINC code has no standardized answer options", file=sys.stderr)
            print("  2. Network/server error prevented lookup (see error messages above)", file=sys.stderr)
            print("  3. The server doesn't have this LOINC code in its database", file=sys.stderr)
            print("=" * 80, file=sys.stderr)
            sys.exit(0)  # Exit successfully to not break parallel commands

        answerlist_id = lookup_result["answerlist_id"]
        loinc_display = lookup_result["display"]

        print(f"✓ Found answer list: {answerlist_id}", file=sys.stderr)
        print(f"✓ LOINC display: {loinc_display}", file=sys.stderr)
        print("", file=sys.stderr)

        # Expand the answer list
        valueset_url = f"http://loinc.org/vs/{answerlist_id}"
        print(f"Expanding ValueSet: {valueset_url}", file=sys.stderr)
        print("", file=sys.stderr)

        valueset = expand_valueset(valueset_url, args.server)

        if not valueset:
            print("", file=sys.stderr)
            print("=" * 80, file=sys.stderr)
            print(f"RESULT: Failed to expand ValueSet {valueset_url}", file=sys.stderr)
            print("Check error messages above for details", file=sys.stderr)
            print("=" * 80, file=sys.stderr)
            sys.exit(0)  # Exit successfully to not break parallel commands

        # Output in requested format
        if args.format == "json":
            print(json.dumps(valueset, indent=2))
        elif args.format == "fhir":
            print(format_fhir_codings(valueset))
        elif args.format == "questionnaire":
            print(format_questionnaire_item(args.loinc_code, loinc_display, valueset_url, valueset))
        else:  # table
            print(format_expansion(valueset, args.full))

    # Mode 2: Search for ValueSets
    elif args.search:
        results = search_valuesets(args.search, args.server, args.limit)

        if not results:
            print("No results found.", file=sys.stderr)
            sys.exit(0)  # Exit successfully to not break parallel commands

        if args.format == "json":
            print(json.dumps(results, indent=2))
        else:
            print(f"{'Title':<50} {'URL':<70}")
            print("-" * 120)
            for entry in results:
                resource = entry.get("resource", {})
                title = resource.get("title", resource.get("name", "N/A"))[:47]
                if len(resource.get("title", resource.get("name", "N/A"))) > 50:
                    title += "..."
                url = resource.get("url", "N/A")[:67]
                if len(resource.get("url", "N/A")) > 70:
                    url += "..."
                print(f"{title:<50} {url:<70}")

    # Mode 3: Expand a specific ValueSet
    elif args.expand:
        valueset = expand_valueset(args.expand, args.server)

        if not valueset:
            sys.exit(0)  # Exit successfully to not break parallel commands

        if args.format == "json":
            print(json.dumps(valueset, indent=2))
        elif args.format == "fhir":
            print(format_fhir_codings(valueset))
        else:  # table
            print(format_expansion(valueset, args.full))


if __name__ == "__main__":
    main()


path: scripts/search_loinc.py
content: #!/usr/bin/env python3
"""
Search LOINC codes using the SearchLOINC API.

This script provides a simple interface to search for LOINC codes that can be used
in FHIR Questionnaire items to make questions conform to clinical standards.

Usage:
    python search_loinc.py "blood pressure"
    python search_loinc.py "depression screening" --limit 10
"""

import argparse
import json
import sys
from typing import Dict, List, Optional
from urllib import request, parse, error


def search_loinc(query: str, limit: int = 5) -> List[Dict]:
    """
    Search LOINC codes using the SearchLOINC API.

    Args:
        query: Search term for LOINC codes
        limit: Maximum number of results to return (default: 5)

    Returns:
        List of LOINC code dictionaries with code, display, and additional info
    """
    base_url = "https://clinicaltables.nlm.nih.gov/api/loinc_items/v3/search"

    params = {
        "terms": query,
        "maxList": str(limit),
        "df": "LOINC_NUM,COMPONENT,SYSTEM,SCALE_TYP,METHOD_TYP,CLASS"
    }

    url = f"{base_url}?{parse.urlencode(params)}"

    try:
        with request.urlopen(url, timeout=10) as response:
            data = json.loads(response.read().decode())

        # API returns: [count, [codes], null, [details]]
        if len(data) < 4:
            return []

        count = data[0]
        codes = data[1]
        details = data[3]

        results = []
        for i, code in enumerate(codes):
            if i < len(details):
                detail = details[i]
                results.append({
                    "code": code,
                    "system": "http://loinc.org",
                    "display": detail[1] if len(detail) > 1 else "",  # COMPONENT
                    "property": {
                        "component": detail[1] if len(detail) > 1 else "",
                        "system_measured": detail[2] if len(detail) > 2 else "",
                        "scale": detail[3] if len(detail) > 3 else "",
                        "method": detail[4] if len(detail) > 4 else "",
                        "class": detail[5] if len(detail) > 5 else ""
                    }
                })

        return results

    except error.URLError as e:
        print(f"Error accessing SearchLOINC API: {e}", file=sys.stderr)
        return []
    except json.JSONDecodeError as e:
        print(f"Error parsing API response: {e}", file=sys.stderr)
        return []
    except Exception as e:
        print(f"Unexpected error: {e}", file=sys.stderr)
        return []


def format_output(results: List[Dict], output_format: str = "json") -> str:
    """Format search results for output."""
    if output_format == "json":
        return json.dumps(results, indent=2)

    elif output_format == "table":
        if not results:
            return "No results found."

        output = []
        output.append(f"{'Code':<15} {'Display':<50} {'System/Scale':<20}")
        output.append("-" * 85)

        for item in results:
            code = item["code"]
            display = item["display"][:47] + "..." if len(item["display"]) > 50 else item["display"]
            sys_scale = f"{item['property']['system_measured']}/{item['property']['scale']}"[:17] + "..." if len(f"{item['property']['system_measured']}/{item['property']['scale']}") > 20 else f"{item['property']['system_measured']}/{item['property']['scale']}"
            output.append(f"{code:<15} {display:<50} {sys_scale:<20}")

        return "\n".join(output)

    elif output_format == "fhir":
        # Format as FHIR Coding objects
        codings = []
        for item in results:
            codings.append({
                "system": item["system"],
                "code": item["code"],
                "display": item["display"]
            })
        return json.dumps(codings, indent=2)

    return ""


def main():
    parser = argparse.ArgumentParser(
        description="Search LOINC codes for FHIR Questionnaires",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Examples:
  python search_loinc.py "blood pressure"
  python search_loinc.py "depression screening" --limit 10
  python search_loinc.py "body weight" --format fhir
        """
    )

    parser.add_argument("query", help="Search term for LOINC codes")
    parser.add_argument("--limit", type=int, default=5, help="Maximum number of results (default: 5)")
    parser.add_argument("--format", choices=["json", "table", "fhir"], default="json",
                       help="Output format (default: json)")

    args = parser.parse_args()

    results = search_loinc(args.query, args.limit)

    if not results:
        print("No results found or an error occurred.", file=sys.stderr)
        sys.exit(0)  # Exit successfully to not break parallel commands

    print(format_output(results, args.format))


if __name__ == "__main__":
    main()


path: scripts/search_snomed.py
content: #!/usr/bin/env python3
"""
Search SNOMED CT codes using the SNOMED CT Browser API.

This script provides a simple interface to search for SNOMED CT codes that can be used
in FHIR Questionnaire items for clinical concepts, conditions, procedures, etc.

Usage:
    python search_snomed.py "diabetes"
    python search_snomed.py "hypertension" --limit 10
"""

import argparse
import json
import sys
from typing import Dict, List, Optional
from urllib import request, parse, error


def search_snomed(query: str, limit: int = 5, semantic_tag: Optional[str] = None) -> List[Dict]:
    """
    Search SNOMED CT codes using the FHIR terminology server.

    Args:
        query: Search term for SNOMED CT codes
        limit: Maximum number of results to return (default: 5)
        semantic_tag: Optional semantic tag to filter results (e.g., "disorder", "procedure")

    Returns:
        List of SNOMED CT code dictionaries with code, display, and additional info
    """
    # Using tx.fhir.org terminology server which supports SNOMED CT
    base_url = "https://tx.fhir.org/r4/CodeSystem/$lookup"

    # For searching, we'll use the ValueSet expand operation with a filter
    search_url = "https://tx.fhir.org/r4/ValueSet/$expand"

    # If filtering by semantic tag, request more results to account for filtering
    request_count = limit * 5 if semantic_tag else limit

    params = {
        "url": "http://snomed.info/sct?fhir_vs",
        "filter": query,
        "count": str(request_count)
    }

    url = f"{search_url}?{parse.urlencode(params)}"

    try:
        req = request.Request(url)
        req.add_header("Accept", "application/fhir+json")

        with request.urlopen(req, timeout=30) as response:
            data = json.loads(response.read().decode())

        # Check for errors
        if data.get("resourceType") == "OperationOutcome":
            print(f"FHIR server returned an error:", file=sys.stderr)
            for issue in data.get("issue", []):
                print(f"  {issue.get('severity')}: {issue.get('diagnostics', 'Unknown error')}", file=sys.stderr)
            return []

        # Extract codes from expansion
        expansion = data.get("expansion", {})
        contains = expansion.get("contains", [])

        results = []
        for item in contains:
            code = item.get("code")
            display = item.get("display", "")

            # Extract semantic tag from display if present (appears in parentheses at end)
            semantic = ""
            clean_display = display
            if "(" in display and display.rstrip().endswith(")"):
                parts = display.rsplit("(", 1)
                if len(parts) == 2:
                    clean_display = parts[0].strip()
                    semantic = parts[1].rstrip(")")

            # Filter by semantic tag if specified (must match before limiting results)
            if semantic_tag and semantic_tag.lower() not in semantic.lower():
                continue

            results.append({
                "code": code,
                "system": "http://snomed.info/sct",
                "display": display,
                "property": {
                    "semanticTag": semantic,
                    "active": True,
                    "moduleId": ""
                }
            })

            # Stop when we have enough results after filtering
            if len(results) >= limit:
                break

        return results

    except error.HTTPError as e:
        print(f"HTTP Error {e.code} when accessing FHIR terminology server", file=sys.stderr)
        print(f"Reason: {e.reason}", file=sys.stderr)
        if e.code == 429:
            print("Rate limit exceeded. Please wait a moment and try again.", file=sys.stderr)
        return []
    except error.URLError as e:
        print(f"Error accessing FHIR terminology server: {e}", file=sys.stderr)
        return []
    except json.JSONDecodeError as e:
        print(f"Error parsing API response: {e}", file=sys.stderr)
        return []
    except Exception as e:
        print(f"Unexpected error: {e}", file=sys.stderr)
        return []


def format_output(results: List[Dict], output_format: str = "json") -> str:
    """Format search results for output."""
    if output_format == "json":
        return json.dumps(results, indent=2)

    elif output_format == "table":
        if not results:
            return "No results found."

        output = []
        output.append(f"{'Code':<20} {'Display':<50} {'Semantic Tag':<15}")
        output.append("-" * 85)

        for item in results:
            code = item["code"]
            display = item["display"][:47] + "..." if len(item["display"]) > 50 else item["display"]
            semantic = item["property"]["semanticTag"][:12] + "..." if len(item["property"]["semanticTag"]) > 15 else item["property"]["semanticTag"]
            output.append(f"{code:<20} {display:<50} {semantic:<15}")

        return "\n".join(output)

    elif output_format == "fhir":
        # Format as FHIR Coding objects
        codings = []
        for item in results:
            codings.append({
                "system": item["system"],
                "code": item["code"],
                "display": item["display"]
            })
        return json.dumps(codings, indent=2)

    return ""


def main():
    parser = argparse.ArgumentParser(
        description="Search SNOMED CT codes for FHIR Questionnaires",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Examples:
  python search_snomed.py "diabetes"
  python search_snomed.py "hypertension" --limit 10
  python search_snomed.py "appendectomy" --format fhir
  python search_snomed.py "asthma" --semantic-tag "disorder"

Common semantic tags:
  disorder, finding, procedure, body structure, substance, organism
        """
    )

    parser.add_argument("query", help="Search term for SNOMED CT codes")
    parser.add_argument("--limit", type=int, default=5, help="Maximum number of results (default: 5)")
    parser.add_argument("--format", choices=["json", "table", "fhir"], default="json",
                       help="Output format (default: json)")
    parser.add_argument("--semantic-tag", help="Filter by semantic tag (e.g., disorder, procedure)")

    args = parser.parse_args()

    results = search_snomed(args.query, args.limit, args.semantic_tag)

    if not results:
        print("No results found or an error occurred.", file=sys.stderr)
        sys.exit(0)  # Exit successfully to not break parallel commands

    print(format_output(results, args.format))


if __name__ == "__main__":
    main()


path: scripts/validate_questionnaire.py
content: #!/usr/bin/env python3
"""
Validate FHIR Questionnaire resources against JSON Schema.

This script validates FHIR Questionnaire JSON files against the schema defined in
references/schema/questionnaire.schema.json. It performs both schema validation and
additional semantic checks.

Source of Truth: The definitive schema is located at references/schema/questionnaire.schema.json

Usage:
    python validate_questionnaire.py questionnaire.json
    python validate_questionnaire.py questionnaire.json --verbose
    python validate_questionnaire.py questionnaire.json --schema-only
"""

# Standard imports
import argparse
import json
import sys
from pathlib import Path
from typing import Any, Dict, List, Set, Tuple

try:
    import jsonschema
    from jsonschema import Draft7Validator, ValidationError
except ImportError:
    print("Error: jsonschema library not found. Install it with: pip install jsonschema", file=sys.stderr)
    sys.exit(1)


class QuestionnaireValidator:
    """Validates FHIR Questionnaire resources against JSON Schema and semantic rules."""

    def __init__(self, schema: Dict[str, Any], verbose: bool = False):
        self.schema = schema
        self.verbose = verbose
        self.errors: List[str] = []
        self.warnings: List[str] = []
        self.validator = Draft7Validator(schema)

    def validate(self, data: Dict[str, Any], schema_only: bool = False) -> Tuple[bool, List[str], List[str]]:
        """
        Validate a FHIR Questionnaire resource.

        Args:
            data: Questionnaire data to validate
            schema_only: If True, only perform schema validation (skip semantic checks)

        Returns:
            Tuple of (is_valid, errors, warnings)
        """
        self.errors = []
        self.warnings = []

        # Perform JSON Schema validation
        schema_errors = list(self.validator.iter_errors(data))
        for error in schema_errors:
            # Format error message with path
            path = ".".join(str(p) for p in error.path) if error.path else "root"
            self.errors.append(f"{path}: {error.message}")

        # If schema_only mode or schema validation failed, return early
        if schema_only or schema_errors:
            return len(self.errors) == 0, self.errors, self.warnings

        # Additional semantic validations
        self._check_linkid_uniqueness(data)
        self._check_enable_when_references(data)
        self._check_common_issues(data)

        return len(self.errors) == 0, self.errors, self.warnings

    def _check_linkid_uniqueness(self, data: Dict[str, Any]):
        """Check that all linkIds are unique across the questionnaire."""
        link_ids: Set[str] = set()
        duplicates: Set[str] = set()

        def collect_link_ids(items: List[Dict], path: str = ""):
            for idx, item in enumerate(items):
                if "linkId" in item:
                    link_id = item["linkId"]
                    if link_id in link_ids:
                        duplicates.add(link_id)
                    link_ids.add(link_id)

                # Recurse into nested items
                if "item" in item:
                    collect_link_ids(item["item"], f"{path}.item[{idx}]")

        if "item" in data:
            collect_link_ids(data["item"])

        for dup in duplicates:
            self.errors.append(f"Duplicate linkId: '{dup}' (linkIds must be unique across the entire questionnaire)")

    def _check_enable_when_references(self, data: Dict[str, Any]):
        """Check that enableWhen references point to valid linkIds."""
        link_ids: Set[str] = set()

        # First, collect all linkIds
        def collect_link_ids(items: List[Dict]):
            for item in items:
                if "linkId" in item:
                    link_ids.add(item["linkId"])
                if "item" in item:
                    collect_link_ids(item["item"])

        if "item" in data:
            collect_link_ids(data["item"])

        # Then check enableWhen references
        def check_references(items: List[Dict], path: str = "item"):
            for idx, item in enumerate(items):
                item_path = f"{path}[{idx}]"

                if "enableWhen" in item:
                    for ew_idx, enable_when in enumerate(item["enableWhen"]):
                        if "question" in enable_when:
                            ref_link_id = enable_when["question"]
                            if ref_link_id not in link_ids:
                                self.errors.append(
                                    f"{item_path}.enableWhen[{ew_idx}]: "
                                    f"References non-existent linkId '{ref_link_id}'"
                                )

                if "item" in item:
                    check_references(item["item"], f"{item_path}.item")

        if "item" in data:
            check_references(data["item"])

    def _check_common_issues(self, data: Dict[str, Any]):
        """Check for common issues and best practices."""
        # Check for url (recommended for published questionnaires)
        if data.get("status") in ["active", "retired"] and "url" not in data:
            self.warnings.append("Published questionnaires should have a 'url' field")

        # Check for version
        if "url" in data and "version" not in data:
            self.warnings.append("Questionnaires with a URL should have a 'version' field")

        # Check for title or name
        if "title" not in data and "name" not in data:
            self.warnings.append("Questionnaire should have a 'title' or 'name' field")

        # Check for text in items (for user display)
        def check_items_text(items: List[Dict], path: str = "item"):
            for idx, item in enumerate(items):
                item_path = f"{path}[{idx}]"
                if "text" not in item and item.get("type") != "display":
                    self.warnings.append(
                        f"{item_path}: Missing 'text' field (recommended for user display)"
                    )

                # Check choice/open-choice items have answer options
                if item.get("type") in ["choice", "open-choice"]:
                    if "answerOption" not in item and "answerValueSet" not in item:
                        self.warnings.append(
                            f"{item_path}: Choice/open-choice items should have 'answerOption' or 'answerValueSet'"
                        )

                if "item" in item:
                    check_items_text(item["item"], f"{item_path}.item")

        if "item" in data:
            check_items_text(data["item"])


def load_questionnaire(file_path: Path) -> Dict[str, Any]:
    """Load questionnaire from JSON file."""
    try:
        with open(file_path, 'r') as f:
            return json.load(f)
    except json.JSONDecodeError as e:
        print(f"Error: Invalid JSON in {file_path}: {e}", file=sys.stderr)
        sys.exit(1)
    except FileNotFoundError:
        print(f"Error: File not found: {file_path}", file=sys.stderr)
        sys.exit(1)
    except Exception as e:
        print(f"Error reading file: {e}", file=sys.stderr)
        sys.exit(1)


def load_schema(schema_path: Path = None) -> Dict[str, Any]:
    """Load the JSON Schema for validation."""
    if schema_path is None:
        # Try to find schema relative to script location
        script_dir = Path(__file__).parent
        schema_path = script_dir.parent / "references" / "schema" / "questionnaire.schema.json"

    try:
        with open(schema_path, 'r') as f:
            return json.load(f)
    except FileNotFoundError:
        print(f"Error: Schema file not found at {schema_path}", file=sys.stderr)
        print("Please ensure references/schema/questionnaire.schema.json exists", file=sys.stderr)
        sys.exit(1)
    except json.JSONDecodeError as e:
        print(f"Error: Invalid JSON in schema file: {e}", file=sys.stderr)
        sys.exit(1)


def main():
    parser = argparse.ArgumentParser(
        description="Validate FHIR Questionnaire resources against JSON Schema",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Examples:
  # Full validation (schema + semantic checks)
  python validate_questionnaire.py questionnaire.json

  # Verbose output
  python validate_questionnaire.py questionnaire.json --verbose

  # Schema validation only (skip semantic checks)
  python validate_questionnaire.py questionnaire.json --schema-only

  # Use custom schema file
  python validate_questionnaire.py questionnaire.json --schema custom-schema.json
        """
    )

    parser.add_argument("file", type=Path, help="Path to FHIR Questionnaire JSON file")
    parser.add_argument("--verbose", "-v", action="store_true",
                       help="Show detailed validation output")
    parser.add_argument("--schema-only", action="store_true",
                       help="Only perform JSON Schema validation (skip semantic checks)")
    parser.add_argument("--schema", type=Path,
                       help="Path to custom schema file (default: references/schema/questionnaire.schema.json)")

    args = parser.parse_args()

    # Load schema
    schema = load_schema(args.schema)

    # Load questionnaire
    data = load_questionnaire(args.file)

    # Validate
    validator = QuestionnaireValidator(schema, verbose=args.verbose)
    is_valid, errors, warnings = validator.validate(data, schema_only=args.schema_only)

    # Output results
    if args.verbose or not is_valid:
        print(f"\nValidating: {args.file}")
        print("=" * 80)

    if errors:
        print("\nERRORS:")
        for error in errors:
            print(f"  ❌ {error}")

    if warnings:
        print("\nWARNINGS:")
        for warning in warnings:
            print(f"  ⚠️  {warning}")

    if is_valid:
        print(f"\n✅ Questionnaire is valid!")
        if warnings:
            print(f"   ({len(warnings)} warning(s))")
        sys.exit(0)
    else:
        print(f"\n❌ Questionnaire is invalid ({len(errors)} error(s))")
        sys.exit(1)


if __name__ == "__main__":
    main()


path: setup.sh
content: #!/bin/bash
# Setup script for FHIR Questionnaire Skill
# Creates virtual environment and installs dependencies

set -e

SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

echo "Setting up FHIR Questionnaire Skill..."

if command -v uv &> /dev/null; then
    echo "Using uv..."
    cd "$SKILL_DIR"

    if [ ! -d ".venv" ]; then
        uv venv
    fi

    uv pip install -r requirements.txt
    echo "✅ Setup complete! Use: .venv/bin/python scripts/validate_questionnaire.py ..."

elif command -v python3 &> /dev/null; then
    echo "Using python3 venv..."
    cd "$SKILL_DIR"

    if [ ! -d ".venv" ]; then
        python3 -m venv .venv
    fi

    .venv/bin/pip install -r requirements.txt
    echo "✅ Setup complete! Use: .venv/bin/python scripts/validate_questionnaire.py ..."

else
    echo "❌ Error: Neither uv nor python3 found."
    exit 1
fi


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (HIGH)