@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix health: <https://ns.cascadeprotocol.org/health/v1#> .
@prefix cascade: <https://ns.cascadeprotocol.org/core/v1#> .
@prefix clinical: <https://ns.cascadeprotocol.org/clinical/v1#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix fhir: <http://hl7.org/fhir/> .

# ============================================================================
# Cascade Protocol — Health & Wellness Vocabulary SHACL Validation Shapes
# ============================================================================
# Version: 1.4 (2026-08-14)
# Validates: health:SelfReport, health:VO2MaxStatistics, health:HRVStatistics,
#            health:BPStatistics, health:MetricTrend, health:ActivitySnapshot,
#            health:SleepSnapshot, health:HealthProfile,
#            health:SocialHistoryRecord,
#            health:LabResultRecord, health:ConditionRecord,
#            health:AllergyRecord, health:ImmunizationRecord,
#            health:FamilyHistoryRecord,
#            health:DailyVitalReading, health:DailyActivitySnapshot,
#            health:DailySleepSnapshot
#
# SEVERITY LEVELS:
# - sh:Violation = Must fix (data invalid without) - e.g., missing required stats
# - sh:Warning = Should address (important for completeness) - e.g., missing period
# - sh:Info = Nice to have (suggested enrichment) - e.g., optional context fields
#
# Validates against: health.ttl v2.6 (2026-08-08)
# ============================================================================

# ============================================================================
# Shape 1: Self Report
# ============================================================================

health:SelfReportShape a sh:NodeShape ;
    sh:targetClass health:SelfReport ;
    rdfs:label "Self Report Shape"@en ;
    rdfs:comment "Validation constraints for patient-reported daily check-ins and self-assessments. Ensures required timestamping, type classification, and completion tracking."@en ;

    # REQUIRED: reportDate (when the self-report was recorded)
    sh:property [
        sh:path health:reportDate ;
        sh:datatype xsd:dateTime ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:name "Report Date"@en ;
        sh:message "Self-report must have exactly one reportDate timestamp"@en ;
        sh:severity sh:Violation
    ] ;

    # REQUIRED: reportType (category of self-report)
    sh:property [
        sh:path health:reportType ;
        sh:datatype xsd:string ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:minLength 1 ;
        sh:name "Report Type"@en ;
        sh:message "Self-report must specify a reportType (e.g., medication_adherence, symptom_tracking, mood, sleep)"@en ;
        sh:severity sh:Violation
    ] ;

    # REQUIRED: completionStatus (whether report is complete)
    sh:property [
        sh:path health:completionStatus ;
        sh:datatype xsd:string ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:in ("complete" "partial" "skipped") ;
        sh:name "Completion Status"@en ;
        sh:message "Self-report must have a completionStatus of complete, partial, or skipped"@en ;
        sh:severity sh:Violation
    ] ;

    # INFO: reportContext (what tracking plan this relates to)
    sh:property [
        sh:path health:reportContext ;
        sh:datatype xsd:string ;
        sh:maxCount 1 ;
        sh:name "Report Context"@en ;
        sh:message "Consider adding reportContext to link this report to a tracking plan"@en ;
        sh:severity sh:Info
    ] ;

    # INFO: reportNotes (free-text patient notes)
    sh:property [
        sh:path health:reportNotes ;
        sh:datatype xsd:string ;
        sh:maxCount 1 ;
        sh:name "Report Notes"@en ;
        sh:message "Consider adding reportNotes for additional patient context"@en ;
        sh:severity sh:Info
    ] .

# ============================================================================
# Shape 2: VO2 Max Statistics
# ============================================================================

health:VO2MaxStatisticsShape a sh:NodeShape ;
    sh:targetClass health:VO2MaxStatistics ;
    rdfs:label "VO2 Max Statistics Shape"@en ;
    rdfs:comment "Validation constraints for VO2 Max statistical summaries. Ensures core statistics, fitness classification, and trend analysis fields are present. Supports the dual-path UI pattern where rich statistics are preferred when available."@en ;

    # REQUIRED: vo2Mean (average VO2 Max over period)
    sh:property [
        sh:path health:vo2Mean ;
        sh:datatype xsd:double ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:minInclusive 0 ;
        sh:name "Mean VO2 Max"@en ;
        sh:message "VO2 Max statistics must have a mean value (mL/kg/min, >= 0)"@en ;
        sh:severity sh:Violation
    ] ;

    # REQUIRED: vo2SampleCount (number of measurements)
    sh:property [
        sh:path health:vo2SampleCount ;
        sh:datatype xsd:integer ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:minInclusive 1 ;
        sh:name "VO2 Sample Count"@en ;
        sh:message "VO2 Max statistics must have at least 1 sample"@en ;
        sh:severity sh:Violation
    ] ;

    # REQUIRED: fitnessClassification (cardiorespiratory fitness level)
    sh:property [
        sh:path health:fitnessClassification ;
        sh:datatype xsd:string ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:in ("veryPoor" "poor" "fair" "good" "excellent" "superior") ;
        sh:name "Fitness Classification"@en ;
        sh:message "VO2 Max statistics must have a fitnessClassification (veryPoor, poor, fair, good, excellent, or superior)"@en ;
        sh:severity sh:Violation
    ] ;

    # WARNING: periodStart (measurement window start)
    sh:property [
        sh:path health:periodStart ;
        sh:datatype xsd:dateTime ;
        sh:maxCount 1 ;
        sh:name "Period Start"@en ;
        sh:message "VO2 Max statistics should specify the measurement period start date"@en ;
        sh:severity sh:Warning
    ] ;

    # WARNING: periodEnd (measurement window end)
    sh:property [
        sh:path health:periodEnd ;
        sh:datatype xsd:dateTime ;
        sh:maxCount 1 ;
        sh:name "Period End"@en ;
        sh:message "VO2 Max statistics should specify the measurement period end date"@en ;
        sh:severity sh:Warning
    ] ;

    # INFO: vo2Min (lowest reading)
    sh:property [
        sh:path health:vo2Min ;
        sh:datatype xsd:double ;
        sh:maxCount 1 ;
        sh:name "Minimum VO2 Max"@en ;
        sh:message "Consider including minimum VO2 Max for range analysis"@en ;
        sh:severity sh:Info
    ] ;

    # INFO: vo2MaxValue (highest reading)
    sh:property [
        sh:path health:vo2MaxValue ;
        sh:datatype xsd:double ;
        sh:maxCount 1 ;
        sh:name "Maximum VO2 Max"@en ;
        sh:message "Consider including maximum VO2 Max for range analysis"@en ;
        sh:severity sh:Info
    ] ;

    # INFO: vo2TrendDirection (trend over period)
    sh:property [
        sh:path health:vo2TrendDirection ;
        sh:datatype xsd:string ;
        sh:maxCount 1 ;
        sh:in ("improving" "declining" "stable" "unknown") ;
        sh:name "VO2 Trend Direction"@en ;
        sh:message "Consider including vo2TrendDirection for fitness trend analysis"@en ;
        sh:severity sh:Info
    ] ;

    # INFO: isSparseData (data quality indicator)
    sh:property [
        sh:path health:isSparseData ;
        sh:datatype xsd:boolean ;
        sh:maxCount 1 ;
        sh:name "Sparse Data Indicator"@en ;
        sh:message "Consider setting isSparseData to flag limited measurement periods"@en ;
        sh:severity sh:Info
    ] .

# ============================================================================
# Shape 3: HRV Statistics
# ============================================================================

health:HRVStatisticsShape a sh:NodeShape ;
    sh:targetClass health:HRVStatistics ;
    rdfs:label "HRV Statistics Shape"@en ;
    rdfs:comment "Validation constraints for Heart Rate Variability (SDNN) statistical summaries. Ensures core statistics and measurement period are present for trend analysis."@en ;

    # REQUIRED: hrvMean (average HRV over period)
    sh:property [
        sh:path health:hrvMean ;
        sh:datatype xsd:double ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:minInclusive 0 ;
        sh:name "Mean HRV"@en ;
        sh:message "HRV statistics must have a mean value (ms, >= 0)"@en ;
        sh:severity sh:Violation
    ] ;

    # REQUIRED: hrvSampleCount (number of readings)
    sh:property [
        sh:path health:hrvSampleCount ;
        sh:datatype xsd:integer ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:minInclusive 1 ;
        sh:name "HRV Sample Count"@en ;
        sh:message "HRV statistics must have at least 1 sample"@en ;
        sh:severity sh:Violation
    ] ;

    # REQUIRED: periodStart (measurement window start)
    sh:property [
        sh:path health:periodStart ;
        sh:datatype xsd:dateTime ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:name "Period Start"@en ;
        sh:message "HRV statistics must specify the measurement period start date"@en ;
        sh:severity sh:Violation
    ] ;

    # REQUIRED: periodEnd (measurement window end)
    sh:property [
        sh:path health:periodEnd ;
        sh:datatype xsd:dateTime ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:name "Period End"@en ;
        sh:message "HRV statistics must specify the measurement period end date"@en ;
        sh:severity sh:Violation
    ] ;

    # INFO: hrvMedian (median HRV)
    sh:property [
        sh:path health:hrvMedian ;
        sh:datatype xsd:double ;
        sh:maxCount 1 ;
        sh:name "Median HRV"@en ;
        sh:message "Consider including median HRV for distribution analysis"@en ;
        sh:severity sh:Info
    ] ;

    # INFO: hrvStdDev (standard deviation)
    sh:property [
        sh:path health:hrvStdDev ;
        sh:datatype xsd:double ;
        sh:maxCount 1 ;
        sh:name "HRV Standard Deviation"@en ;
        sh:message "Consider including HRV standard deviation for variability analysis"@en ;
        sh:severity sh:Info
    ] ;

    # INFO: hrvMin (lowest reading)
    sh:property [
        sh:path health:hrvMin ;
        sh:datatype xsd:double ;
        sh:maxCount 1 ;
        sh:name "Minimum HRV"@en ;
        sh:message "Consider including minimum HRV for range analysis"@en ;
        sh:severity sh:Info
    ] ;

    # INFO: hrvMax (highest reading)
    sh:property [
        sh:path health:hrvMax ;
        sh:datatype xsd:double ;
        sh:maxCount 1 ;
        sh:name "Maximum HRV"@en ;
        sh:message "Consider including maximum HRV for range analysis"@en ;
        sh:severity sh:Info
    ] ;

    # INFO: hrvTrendDirection (trend over period)
    sh:property [
        sh:path health:hrvTrendDirection ;
        sh:datatype xsd:string ;
        sh:maxCount 1 ;
        sh:in ("improving" "declining" "stable" "unknown") ;
        sh:name "HRV Trend Direction"@en ;
        sh:message "Consider including hrvTrendDirection for autonomic nervous system trend analysis"@en ;
        sh:severity sh:Info
    ] .

# ============================================================================
# Shape 4: Blood Pressure Statistics
# ============================================================================

health:BPStatisticsShape a sh:NodeShape ;
    sh:targetClass health:BPStatistics ;
    rdfs:label "Blood Pressure Statistics Shape"@en ;
    rdfs:comment "Validation constraints for blood pressure statistical summaries. Ensures mean systolic/diastolic values and AHA category classification are present."@en ;

    # REQUIRED: bpMeanSystolic (average systolic BP)
    sh:property [
        sh:path health:bpMeanSystolic ;
        sh:datatype xsd:double ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:minInclusive 40 ;
        sh:maxInclusive 300 ;
        sh:name "Mean Systolic BP"@en ;
        sh:message "BP statistics must have a mean systolic value (mmHg, 40-300)"@en ;
        sh:severity sh:Violation
    ] ;

    # REQUIRED: bpMeanDiastolic (average diastolic BP)
    sh:property [
        sh:path health:bpMeanDiastolic ;
        sh:datatype xsd:double ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:minInclusive 20 ;
        sh:maxInclusive 200 ;
        sh:name "Mean Diastolic BP"@en ;
        sh:message "BP statistics must have a mean diastolic value (mmHg, 20-200)"@en ;
        sh:severity sh:Violation
    ] ;

    # REQUIRED: bpCategory (AHA classification)
    sh:property [
        sh:path health:bpCategory ;
        sh:datatype xsd:string ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:in ("normal" "elevated" "hypertension_stage1" "hypertension_stage2" "hypertensive_crisis") ;
        sh:name "BP Category"@en ;
        sh:message "BP statistics must have an AHA category (normal, elevated, hypertension_stage1, hypertension_stage2, or hypertensive_crisis)"@en ;
        sh:severity sh:Violation
    ] ;

    # WARNING: bpSampleCount (number of readings)
    sh:property [
        sh:path health:bpSampleCount ;
        sh:datatype xsd:integer ;
        sh:maxCount 1 ;
        sh:minInclusive 1 ;
        sh:name "BP Sample Count"@en ;
        sh:message "BP statistics should include a sample count for data quality assessment"@en ;
        sh:severity sh:Warning
    ] ;

    # INFO: bpMinSystolic (lowest systolic reading)
    sh:property [
        sh:path health:bpMinSystolic ;
        sh:datatype xsd:double ;
        sh:maxCount 1 ;
        sh:name "Minimum Systolic BP"@en ;
        sh:message "Consider including minimum systolic BP for range analysis"@en ;
        sh:severity sh:Info
    ] ;

    # INFO: bpMaxSystolic (highest systolic reading)
    sh:property [
        sh:path health:bpMaxSystolic ;
        sh:datatype xsd:double ;
        sh:maxCount 1 ;
        sh:name "Maximum Systolic BP"@en ;
        sh:message "Consider including maximum systolic BP for range analysis"@en ;
        sh:severity sh:Info
    ] ;

    # INFO: bpMinDiastolic (lowest diastolic reading)
    sh:property [
        sh:path health:bpMinDiastolic ;
        sh:datatype xsd:double ;
        sh:maxCount 1 ;
        sh:name "Minimum Diastolic BP"@en ;
        sh:message "Consider including minimum diastolic BP for range analysis"@en ;
        sh:severity sh:Info
    ] ;

    # INFO: bpMaxDiastolic (highest diastolic reading)
    sh:property [
        sh:path health:bpMaxDiastolic ;
        sh:datatype xsd:double ;
        sh:maxCount 1 ;
        sh:name "Maximum Diastolic BP"@en ;
        sh:message "Consider including maximum diastolic BP for range analysis"@en ;
        sh:severity sh:Info
    ] .

# ============================================================================
# Shape 5: Metric Trend
# ============================================================================

health:MetricTrendShape a sh:NodeShape ;
    sh:targetClass health:MetricTrend ;
    rdfs:label "Metric Trend Shape"@en ;
    rdfs:comment "Validation constraints for time-bounded wellness metric trends. Ensures trend direction is present and encourages confidence and period metadata for proper interpretation."@en ;

    # REQUIRED: trendDirection (direction of change)
    sh:property [
        sh:path health:trendDirection ;
        sh:datatype xsd:string ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:in ("increasing" "decreasing" "stable" "insufficient_data") ;
        sh:name "Trend Direction"@en ;
        sh:message "Metric trend must have a direction (increasing, decreasing, stable, or insufficient_data)"@en ;
        sh:severity sh:Violation
    ] ;

    # WARNING: trendConfidence (data quality indicator)
    sh:property [
        sh:path health:trendConfidence ;
        sh:datatype xsd:string ;
        sh:maxCount 1 ;
        sh:in ("high" "medium" "low") ;
        sh:name "Trend Confidence"@en ;
        sh:message "Metric trend should include confidence level (high >30d, medium 14-30d, low <14d)"@en ;
        sh:severity sh:Warning
    ] ;

    # WARNING: trendPeriodStart (observation window start)
    sh:property [
        sh:path health:trendPeriodStart ;
        sh:datatype xsd:dateTime ;
        sh:maxCount 1 ;
        sh:name "Trend Period Start"@en ;
        sh:message "Metric trend should specify the observation period start date"@en ;
        sh:severity sh:Warning
    ] ;

    # WARNING: trendPeriodEnd (observation window end)
    sh:property [
        sh:path health:trendPeriodEnd ;
        sh:datatype xsd:dateTime ;
        sh:maxCount 1 ;
        sh:name "Trend Period End"@en ;
        sh:message "Metric trend should specify the observation period end date"@en ;
        sh:severity sh:Warning
    ] ;

    # INFO: trendMagnitude (percentage change)
    sh:property [
        sh:path health:trendMagnitude ;
        sh:datatype xsd:decimal ;
        sh:maxCount 1 ;
        sh:name "Trend Magnitude"@en ;
        sh:message "Consider including trendMagnitude (percentage change) for quantitative trend analysis"@en ;
        sh:severity sh:Info
    ] ;

    # INFO: trendBaselineValue (reference value)
    sh:property [
        sh:path health:trendBaselineValue ;
        sh:datatype xsd:decimal ;
        sh:maxCount 1 ;
        sh:name "Baseline Value"@en ;
        sh:message "Consider including trendBaselineValue for comparative analysis"@en ;
        sh:severity sh:Info
    ] ;

    # INFO: trendCurrentValue (current period average)
    sh:property [
        sh:path health:trendCurrentValue ;
        sh:datatype xsd:decimal ;
        sh:maxCount 1 ;
        sh:name "Current Value"@en ;
        sh:message "Consider including trendCurrentValue for comparative analysis"@en ;
        sh:severity sh:Info
    ] .

# ============================================================================
# Shape 6: Activity Snapshot
# ============================================================================

health:ActivitySnapshotShape a sh:NodeShape ;
    sh:targetClass health:ActivitySnapshot ;
    rdfs:label "Activity Snapshot Shape"@en ;
    rdfs:comment "Validation constraints for aggregated activity metrics over a measurement period (typically 7 days). All fields are optional since activity data availability depends on device capabilities."@en ;

    # WARNING: averageDailySteps (primary activity metric)
    sh:property [
        sh:path health:averageDailySteps ;
        sh:datatype xsd:integer ;
        sh:maxCount 1 ;
        sh:minInclusive 0 ;
        sh:name "Average Daily Steps"@en ;
        sh:message "Activity snapshot should include average daily steps as a primary activity metric"@en ;
        sh:severity sh:Warning
    ] ;

    # INFO: exerciseMinutesWeekly (exercise activity)
    sh:property [
        sh:path health:exerciseMinutesWeekly ;
        sh:datatype xsd:integer ;
        sh:maxCount 1 ;
        sh:minInclusive 0 ;
        sh:name "Exercise Minutes (Weekly)"@en ;
        sh:message "Consider including weekly exercise minutes for fitness context"@en ;
        sh:severity sh:Info
    ] ;

    # INFO: standHoursDaily (standing activity)
    sh:property [
        sh:path health:standHoursDaily ;
        sh:datatype xsd:integer ;
        sh:maxCount 1 ;
        sh:minInclusive 0 ;
        sh:maxInclusive 24 ;
        sh:name "Stand Hours (Daily)"@en ;
        sh:message "Consider including daily stand hours for sedentary behavior tracking"@en ;
        sh:severity sh:Info
    ] ;

    # INFO: activeEnergyBurnedKcal (energy expenditure)
    sh:property [
        sh:path health:activeEnergyBurnedKcal ;
        sh:datatype xsd:decimal ;
        sh:maxCount 1 ;
        sh:minInclusive 0 ;
        sh:name "Active Energy Burned (kcal)"@en ;
        sh:message "Consider including active energy burned for caloric expenditure tracking"@en ;
        sh:severity sh:Info
    ] .

# ============================================================================
# Shape 7: Sleep Snapshot
# ============================================================================

health:SleepSnapshotShape a sh:NodeShape ;
    sh:targetClass health:SleepSnapshot ;
    rdfs:label "Sleep Snapshot Shape"@en ;
    rdfs:comment "Validation constraints for aggregated sleep metrics over a measurement period (typically 7 days). Sleep data availability depends on device capabilities and user behavior."@en ;

    # WARNING: averageDurationHours (primary sleep metric)
    sh:property [
        sh:path health:averageDurationHours ;
        sh:datatype xsd:decimal ;
        sh:maxCount 1 ;
        sh:minInclusive 0 ;
        sh:maxInclusive 24 ;
        sh:name "Average Sleep Duration (hours)"@en ;
        sh:message "Sleep snapshot should include average duration as a primary sleep metric"@en ;
        sh:severity sh:Warning
    ] ;

    # INFO: sleepQuality (qualitative assessment)
    sh:property [
        sh:path health:sleepQuality ;
        sh:datatype xsd:string ;
        sh:maxCount 1 ;
        sh:name "Sleep Quality"@en ;
        sh:message "Consider including sleep quality assessment for comprehensive sleep analysis"@en ;
        sh:severity sh:Info
    ] .

# ============================================================================
# Shape 8: Health Profile (Top-level Container)
# ============================================================================

health:HealthProfileShape a sh:NodeShape ;
    sh:targetClass health:HealthProfile ;
    # The six wellness containers are declared rdfs:subClassOf health:HealthProfile
    # in health.ttl v2.5, and SHACL sh:targetClass is subclass-aware. But that
    # aspect of targeting is evaluated against the DATA graph (SHACL 2.1.3.1
    # defines "SHACL instance of" as rdf:type/rdfs:subClassOf* in the data
    # graph), so it only fires for a validator that loads the vocabulary
    # alongside the data. A validator that loads shapes only -- which is the
    # common case, and is what the reference implementation does -- would see
    # no target and check nothing, reproducing the vacuous PASS this release
    # exists to remove.
    #
    # These six are therefore named explicitly as well. The subclass axioms
    # remain the semantic statement; these make it effective today.
    sh:targetClass health:ActivityData ;
    sh:targetClass health:SleepData ;
    sh:targetClass health:HeartRateData ;
    sh:targetClass health:BloodPressureData ;
    sh:targetClass health:HRVData ;
    sh:targetClass health:BodyMeasurements ;
    rdfs:label "Health Profile Shape"@en ;
    rdfs:comment "Validation constraints for the top-level wellness profile aggregating vital signs, body measurements, activity, and sleep data from consumer devices. Stored at /wellness/ in the Cascade Pod."@en ;

    # WARNING: bloodType (ABO blood group characteristic)
    sh:property [
        sh:path health:bloodType ;
        sh:datatype xsd:string ;
        sh:maxCount 1 ;
        sh:in ("aPositive" "aNegative" "bPositive" "bNegative" "abPositive" "abNegative" "oPositive" "oNegative") ;
        sh:name "Blood Type"@en ;
        sh:message "Blood type should be a valid ABO/Rh value if provided"@en ;
        sh:severity sh:Warning
    ] ;

    # INFO: restingHeartRateHistory (90-day vital trend)
    sh:property [
        sh:path health:restingHeartRateHistory ;
        sh:class health:DailyVitalReading ;
        sh:name "Resting Heart Rate History"@en ;
        sh:message "Consider populating resting heart rate history for cardiac trend analysis"@en ;
        sh:severity sh:Info
    ] ;

    # INFO: walkingHeartRateHistory (90-day vital trend)
    sh:property [
        sh:path health:walkingHeartRateHistory ;
        sh:class health:DailyVitalReading ;
        sh:name "Walking Heart Rate History"@en ;
        sh:message "Consider populating walking heart rate history for activity-related cardiac trends"@en ;
        sh:severity sh:Info
    ] ;

    # INFO: hrvHistory (7-day HRV readings)
    sh:property [
        sh:path health:hrvHistory ;
        sh:class health:HRVReading ;
        sh:name "HRV History"@en ;
        sh:message "Consider populating HRV history for autonomic nervous system analysis"@en ;
        sh:severity sh:Info
    ] ;

    # INFO: bloodPressureHistory (30-day BP readings)
    sh:property [
        sh:path health:bloodPressureHistory ;
        sh:class health:BloodPressureReading ;
        sh:name "Blood Pressure History"@en ;
        sh:message "Consider populating blood pressure history for cardiovascular trend analysis"@en ;
        sh:severity sh:Info
    ] ;

    # INFO: vo2MaxHistory (180-day VO2 Max readings)
    sh:property [
        sh:path health:vo2MaxHistory ;
        sh:class health:VitalSignReading ;
        sh:name "VO2 Max History"@en ;
        sh:message "Consider populating VO2 Max history for fitness trend analysis"@en ;
        sh:severity sh:Info
    ] ;

    # INFO: bodyMassHistory (30-day weight readings)
    sh:property [
        sh:path health:bodyMassHistory ;
        sh:class health:DailyVitalReading ;
        sh:name "Body Mass History"@en ;
        sh:message "Consider populating body mass history for weight trend analysis"@en ;
        sh:severity sh:Info
    ] ;

    # INFO: dailyActivityHistory (90-day activity snapshots)
    sh:property [
        sh:path health:dailyActivityHistory ;
        sh:class health:DailyActivitySnapshot ;
        sh:name "Daily Activity History"@en ;
        sh:message "Consider populating daily activity history for activity trend analysis"@en ;
        sh:severity sh:Info
    ] ;

    # INFO: dailySleepHistory (90-day sleep snapshots)
    sh:property [
        sh:path health:dailySleepHistory ;
        sh:class health:DailySleepSnapshot ;
        sh:name "Daily Sleep History"@en ;
        sh:message "Consider populating daily sleep history for sleep pattern analysis"@en ;
        sh:severity sh:Info
    ] ;

    # INFO: hasHRVStatistics (link to HRV statistical summary)
    sh:property [
        sh:path health:hasHRVStatistics ;
        sh:class health:HRVStatistics ;
        sh:maxCount 1 ;
        sh:node health:HRVStatisticsShape ;
        sh:name "HRV Statistics"@en ;
        sh:message "Consider linking HRV statistics for heart rate variability summary"@en ;
        sh:severity sh:Info
    ] ;

    # INFO: hasBPStatistics (link to BP statistical summary)
    sh:property [
        sh:path health:hasBPStatistics ;
        sh:class health:BPStatistics ;
        sh:maxCount 1 ;
        sh:node health:BPStatisticsShape ;
        sh:name "BP Statistics"@en ;
        sh:message "Consider linking BP statistics for blood pressure summary"@en ;
        sh:severity sh:Info
    ] .

# ============================================================================
# Shape: SocialHistoryRecord (v2.4)
# ============================================================================

health:SocialHistoryRecordShape a sh:NodeShape ;
    sh:targetClass health:SocialHistoryRecord ;
    rdfs:label "Social History Record Shape"@en ;
    sh:property [
        sh:path cascade:sourceSystem ;
        sh:datatype xsd:string ;
        sh:maxCount 1 ;
        sh:severity sh:Info ;
        sh:name "Source System"@en ;
        sh:message "Source system attribution is recommended for social history records"@en
    ] ;
    sh:property [
        sh:path health:smokingStatus ;
        sh:datatype xsd:string ;
        sh:maxCount 1 ;
        sh:severity sh:Info ;
        sh:name "Smoking Status"@en ;
        sh:message "Smoking status is a key social history field"@en
    ] .

# ============================================================================
# Changelog
# ============================================================================
#
# Version 1.4 (2026-08-14) — health v2.7
# - health:interpretation: the sh:in list gains the fourteen data-absent-reason
#   codes it did not already carry, so the list is now the 49 selectable HL7 v3
#   ObservationInterpretation codes, ALL FIFTEEN codes of
#   http://terminology.hl7.org/CodeSystem/data-absent-reason, and the ten
#   retained v2.5 words: 74 values. v2.6 accepted "unknown" alone, which made
#   every reason for an absent interpretation identical.
# - NEW: a property shape for health:interpretationSourceCode. Single string,
#   no value set and no pattern, by design.
# - No interpretation constraint was added to DailyVitalReadingShape. See the
#   v2.7 changelog note in health.ttl: health:interpretation's domain is
#   health:LabResultRecord and health:DailyVitalReading carries no
#   interpretation predicate, so the constraint would be vacuous. The vital
#   binding is clinical:VitalSignShape in clinical v1.15.
# - Strictly widening: every graph valid under v1.3 is valid under v1.4.
#
# Version 1.3 (2026-08-08) — health v2.6
# - health:interpretation: the sh:in list is now the 49 selectable codes of the
#   HL7 v3 ObservationInterpretation code system,
#   http://terminology.hl7.org/CodeSystem/v3-ObservationInterpretation (3.0.0),
#   plus the data-absent-reason code "unknown" and the ten retained v2.5 words.
#   The previous list was a five-member set with no standards basis, so a lab
#   reporting a susceptibility, detection, reactivity or change result — all
#   conformant FHIR — failed validation.
# - health:labCategory: sh:maxCount 1 removed. FHIR R4 Observation.category is
#   0..*.
# - health:testCode, health:icd10Code, health:snomedCode: sh:maxCount 1 removed.
#   FHIR R4 CodeableConcept.coding is 0..*, and dual-coded problem-list entries
#   and multi-coding lab observations are ordinary EHR output.
# - No sh:pattern added to any code property; see the note at
#   health:icd10Code for why a lexical pattern would reject valid codes.
# - Strictly widening: every graph valid under v1.2 is valid under v1.3.
#
# Version 1.2 (2026-08-03)
# - Added 5 record shapes for the classes defined in health v2.5:
#   LabResultRecordShape, ConditionRecordShape, AllergyRecordShape,
#   ImmunizationRecordShape, FamilyHistoryRecordShape. Before this version
#   these classes had no sh:targetClass anywhere, so validating a lab result,
#   condition, allergy, immunization or family history record checked nothing
#   and returned PASS.
# - Added 3 shapes for classes that have existed since health v2.0 but were
#   never targeted: DailyVitalReadingShape, DailyActivitySnapshotShape,
#   DailySleepSnapshotShape.
# - Constraint sets lifted from the corresponding clinical:* shapes and
#   checked against FHIR R4. Where FHIR names a required value set the sh:in
#   list is that value set verbatim.
# - sh:Violation severity on all required fields.
# - 29 conformance fixtures (7 lab, 7 condition, 6 allergy, 3 immunization,
#   3 family history) become executable against real constraints.
#
# Version 1.1 (2026-03-27)
# - Added SocialHistoryRecordShape for health:SocialHistoryRecord (health v2.4)
#
# Version 1.0 (2026-02-18)
# - Initial release of Health & Wellness SHACL shapes (Phase 4)
# - 8 shapes covering all major health.ttl v2.2 classes:
#   - SelfReportShape: Patient-reported check-in validation (3 required, 2 info)
#   - VO2MaxStatisticsShape: Fitness statistics validation (3 required, 2 warning, 4 info)
#   - HRVStatisticsShape: HRV statistics validation (4 required, 5 info)
#   - BPStatisticsShape: Blood pressure statistics validation (3 required, 1 warning, 4 info)
#   - MetricTrendShape: Wellness trend validation (1 required, 3 warning, 3 info)
#   - ActivitySnapshotShape: Activity metrics validation (1 warning, 3 info)
#   - SleepSnapshotShape: Sleep metrics validation (1 warning, 1 info)
#   - HealthProfileShape: Top-level container validation (1 warning, 10 info)
# - Severity levels follow established pattern: Violation/Warning/Info
# - sh:in constraints match enumerated values from health.ttl ontology
# - Range constraints (minInclusive/maxInclusive) reflect physiological bounds
#

# ============================================================================
# Clinical Record Shapes (v1.2 — health v2.5)
# ============================================================================
#
# Constraint sets are LIFTED from the corresponding clinical:* shapes in
# clinical.shapes.ttl (AllergyShape :526, LabResultShape :607, ConditionShape
# :685, ImmunizationShape :764) so that the two spellings of the same record
# are held to the same standard, then checked against the FHIR R4 resource
# definitions. Where FHIR names a required value set, the sh:in list below is
# that value set verbatim:
#
#   health:allergyCategory   AllergyIntoleranceCategory     food | medication | environment | biologic
#   health:allergySeverity   AllergyIntoleranceSeverity     mild | moderate | severe
#   health:status (condition) ConditionClinicalStatusCodes  active | recurrence | relapse | inactive | remission | resolved
#   health:status (immunization) ImmunizationStatusCodes    completed | entered-in-error | not-done
#
# health:status is deliberately constrained per-shape rather than on the
# property: the permitted values differ by record class and a single global
# sh:in would be wrong for both.
#
# Severity is sh:Violation on every required field. A Warning-severity
# constraint on a required field reproduces the problem these shapes exist to
# fix (a PASS that means nothing) with extra steps.
#
# CONSEQUENCE, STATED PLAINLY: before this release, validating a lab result,
# condition, allergy, immunization or family-history record returned PASS
# because no shape targeted it and nothing was checked. Those records are now
# actually checked. A validation failure appearing after upgrade means the
# validator improved, not that the data got worse.

# ============================================================================
# Shape 9: Lab Result Record
# ============================================================================

health:LabResultRecordShape a sh:NodeShape ;
    sh:targetClass health:LabResultRecord ;
    rdfs:label "Lab Result Record Shape"@en ;
    rdfs:comment "Validation constraints for laboratory result records. Lifted from clinical:LabResultShape and checked against FHIR R4 Observation."@en ;

    # REQUIRED: testName
    sh:property [
        sh:path health:testName ;
        sh:datatype xsd:string ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:minLength 1 ;
        sh:name "Test Name"@en ;
        sh:message "Lab result must have exactly one non-empty testName"@en ;
        sh:severity sh:Violation
    ] ;

    # REQUIRED cardinality: resultValue is at most one.
    # A single subject asserting two resultValues is the failure mode this
    # constraint exists for: two same-day readings of the same analyte merged
    # onto one record produce a subject with both values, which a consumer
    # then reads as the single string "95, 310". Unconstrained, that passes.
    sh:property [
        sh:path health:resultValue ;
        sh:maxCount 1 ;
        sh:name "Result Value"@en ;
        sh:message "Lab result must not carry more than one resultValue; two readings of the same analyte are two records"@en ;
        sh:severity sh:Violation
    ] ;

    sh:property [
        sh:path health:resultUnit ;
        sh:datatype xsd:string ;
        sh:maxCount 1 ;
        sh:name "Result Unit"@en ;
        sh:message "Lab result must not carry more than one resultUnit"@en ;
        sh:severity sh:Violation
    ] ;

    sh:property [
        sh:path health:referenceRange ;
        sh:datatype xsd:string ;
        sh:maxCount 1 ;
        sh:name "Reference Range"@en ;
        sh:severity sh:Violation
    ] ;

    # Interpretation is bound to the HL7 v3 ObservationInterpretation code
    # system, which is the code system FHIR R4 binds Observation.interpretation
    # to. The previous list was a five-member set Cascade invented; laboratories
    # report susceptibility (S/I/R), detection (POS/NEG/DET/ND/IND), reactivity
    # (RR/WR/NR) and change (B/D/U/W) results that have no member of that set,
    # so a conformant export failed validation for being conformant.
    #
    # The list below is the 49 SELECTABLE codes of
    # http://terminology.hl7.org/CodeSystem/v3-ObservationInterpretation
    # (version 3.0.0), verbatim and in the code system's own order. The eight
    # abstract concepts (_GeneticObservationInterpretation,
    # _ObservationInterpretationChange, _ObservationInterpretationExceptions,
    # _ObservationInterpretationNormality,
    # _ObservationInterpretationSusceptibility, ObservationInterpretationDetection,
    # ObservationInterpretationExpectation, ReactivityObservationInterpretation)
    # are marked notSelectable there and are deliberately absent here: they are
    # hierarchy nodes, not values. Codes the code system marks deprecated
    # (Carrier, AC, QCF, TOX, MS, VS, HM, OBX, H>, L<) ARE accepted, because a
    # deprecated code is still a defined code and historical results carry them.
    #
    # Then two additions that are not from that code system and are named here
    # rather than left to be inferred:
    #   - ALL FIFTEEN codes of
    #     http://terminology.hl7.org/CodeSystem/data-absent-reason (value set
    #     http://hl7.org/fhir/ValueSet/data-absent-reason). v2.6 admitted only
    #     "unknown" here, for the very common source Observation that carried no
    #     interpretation at all. health v2.7 admits the rest, because a source
    #     that says WHY the element is absent is saying something, and one
    #     accepted code made every reason identical: a C-CDA
    #     <interpretationCode nullFlavor="NASK"/> (nobody asked) became
    #     indistinguishable from nullFlavor="ASKU" (asked, did not know) and
    #     from nullFlavor="NAV" (expected later). Putting a data-absent-reason
    #     coding INTO the CodeableConcept that is missing is FHIR's own idiom,
    #     not a Cascade convention, so this is deference and not invention. The
    #     nullFlavor-to-data-absent-reason mapping an importer must implement is
    #     stated once, on cascade:dataAbsentReason in core v3.6.
    #   - the ten lower- and title-case English words of the previous enum,
    #     retained so that data already written against health v2.5 keeps
    #     validating. They are NOT recommended for new writes.
    #
    # A source code in NEITHER value set is not forced in here and is not
    # dropped: it goes verbatim on health:interpretationSourceCode (v2.7), and
    # the producer writes its nearest ratified equivalent here.
    sh:property [
        sh:path health:interpretation ;
        sh:datatype xsd:string ;
        sh:maxCount 1 ;
        sh:in ("EX" "HM" "OBX" "CAR" "Carrier" "B" "D" "U" "W"
               "<" ">" "AC" "IE" "QCF" "TOX"
               "A" "N" "I" "MS" "NCL" "NS" "R" "S" "VS"
               "AA" "H" "L" "HH" "LL" "HX" "LX" "H>" "HU" "E" "L<" "LU"
               "ND" "IND" "NEG" "POS" "EXP" "UNE" "DET"
               "SYN-R" "NR" "RR" "WR" "SDD" "SYN-S"
               "unknown" "asked-unknown" "temp-unknown" "not-asked"
               "asked-declined" "masked" "not-applicable" "unsupported"
               "as-text" "error" "not-a-number" "negative-infinity"
               "positive-infinity" "not-performed" "not-permitted"
               "normal" "high" "low" "abnormal" "critical"
               "Normal" "High" "Low" "Abnormal" "Critical") ;
        sh:name "Interpretation"@en ;
        sh:message "Interpretation must be a code from the HL7 v3 ObservationInterpretation code system (http://terminology.hl7.org/CodeSystem/v3-ObservationInterpretation), a code from http://terminology.hl7.org/CodeSystem/data-absent-reason, or one of the retained health v2.5 words. A source code in none of those is carried verbatim on health:interpretationSourceCode."@en ;
        sh:severity sh:Violation
    ] ;

    # health:interpretationSourceCode (v2.7) is deliberately unconstrained apart
    # from being a single string. It carries a source code that is a member of
    # no bound value set, verbatim; a value set or a pattern here would recreate
    # exactly the loss the property exists to prevent.
    sh:property [
        sh:path health:interpretationSourceCode ;
        sh:datatype xsd:string ;
        sh:maxCount 1 ;
        sh:name "Interpretation Source Code"@en ;
        sh:message "Interpretation source code must be a single string: the source's verbatim code, unmodified"@en ;
        sh:severity sh:Violation
    ] ;

    sh:property [
        sh:path health:performedDate ;
        sh:or ( [ sh:datatype xsd:date ] [ sh:datatype xsd:dateTime ] ) ;
        sh:message "Performed Date must be an xsd:date or an xsd:dateTime. FHIR's dateTime primitive permits date precision (YYYY, YYYY-MM, YYYY-MM-DD), so a source that stated only a calendar day must not be given an invented time; an untyped or xsd:string literal is not either type."@en ;
        sh:maxCount 1 ;
        sh:name "Performed Date"@en ;
        sh:severity sh:Violation
    ] ;

    sh:property [
        sh:path health:reportedDate ;
        sh:or ( [ sh:datatype xsd:date ] [ sh:datatype xsd:dateTime ] ) ;
        sh:message "Reported Date must be an xsd:date or an xsd:dateTime. FHIR's dateTime primitive permits date precision (YYYY, YYYY-MM, YYYY-MM-DD), so a source that stated only a calendar day must not be given an invented time; an untyped or xsd:string literal is not either type."@en ;
        sh:maxCount 1 ;
        sh:name "Reported Date"@en ;
        sh:severity sh:Violation
    ] ;

    # Multi-valued: FHIR R4 CodeableConcept.coding is 0..*
    # (https://hl7.org/fhir/R4/datatypes.html#CodeableConcept), and an
    # Observation.code routinely carries more than one LOINC coding for the same
    # test. Importers emit one health:testCode per coding, so sh:maxCount 1
    # rejected the record for preserving what the source sent. No cardinality
    # ceiling; each value is still a single code.
    sh:property [
        sh:path health:testCode ;
        sh:name "Test Code (LOINC)"@en ;
        sh:severity sh:Violation
    ] ;

    # Multi-valued: FHIR R4 Observation.category is 0..*
    # (https://hl7.org/fhir/R4/observation-definitions.html#Observation.category).
    # Real exports categorise one result several ways at once (for example a
    # department grouping alongside "laboratory"), so a single-valued constraint
    # here contradicted the resource definition the data comes from.
    sh:property [
        sh:path health:labCategory ;
        sh:datatype xsd:string ;
        sh:name "Lab Category"@en ;
        sh:severity sh:Violation
    ] ;

    sh:property [
        sh:path health:specimenType ;
        sh:datatype xsd:string ;
        sh:maxCount 1 ;
        sh:name "Specimen Type"@en ;
        sh:severity sh:Violation
    ] ;

    sh:property [
        sh:path health:orderingProvider ;
        sh:datatype xsd:string ;
        sh:maxCount 1 ;
        sh:name "Ordering Provider"@en ;
        sh:severity sh:Violation
    ] ;

    sh:property [
        sh:path health:performingLab ;
        sh:datatype xsd:string ;
        sh:maxCount 1 ;
        sh:name "Performing Laboratory"@en ;
        sh:severity sh:Violation
    ] ;

    sh:property [
        sh:path health:sourceRecordId ;
        sh:datatype xsd:string ;
        sh:maxCount 1 ;
        sh:name "Source Record Identifier"@en ;
        sh:severity sh:Violation
    ] ;

    # REQUIRED: provenance
    sh:property [
        sh:path cascade:dataProvenance ;
        sh:in (cascade:ClinicalGenerated cascade:EHRVerified cascade:DeviceGenerated
               cascade:PatientReported cascade:SelfReported cascade:AIExtracted) ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:name "Data Provenance"@en ;
        sh:message "Lab result must declare exactly one valid cascade:dataProvenance"@en ;
        sh:severity sh:Violation
    ] ;

    # REQUIRED: schemaVersion
    sh:property [
        sh:path cascade:schemaVersion ;
        sh:datatype xsd:string ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:pattern "^[0-9]+\\.[0-9]+$" ;
        sh:name "Schema Version"@en ;
        sh:message "Lab result must declare exactly one schemaVersion in major.minor form"@en ;
        sh:severity sh:Violation
    ] .

# ============================================================================
# Shape 10: Condition Record
# ============================================================================

health:ConditionRecordShape a sh:NodeShape ;
    sh:targetClass health:ConditionRecord ;
    rdfs:label "Condition Record Shape"@en ;
    rdfs:comment "Validation constraints for condition and problem-list records. Lifted from clinical:ConditionShape and checked against FHIR R4 Condition."@en ;

    # REQUIRED: conditionName
    sh:property [
        sh:path health:conditionName ;
        sh:datatype xsd:string ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:minLength 1 ;
        sh:name "Condition Name"@en ;
        sh:message "Condition must have exactly one non-empty conditionName"@en ;
        sh:severity sh:Violation
    ] ;

    # status: FHIR R4 ConditionClinicalStatusCodes
    sh:property [
        sh:path health:status ;
        sh:datatype xsd:string ;
        sh:maxCount 1 ;
        sh:in ("active" "recurrence" "relapse" "inactive" "remission" "resolved") ;
        sh:name "Clinical Status"@en ;
        sh:message "Condition status must be one of the FHIR R4 ConditionClinicalStatusCodes: active, recurrence, relapse, inactive, remission, resolved"@en ;
        sh:severity sh:Violation
    ] ;

    sh:property [
        sh:path health:onsetDate ;
        sh:or ( [ sh:datatype xsd:date ] [ sh:datatype xsd:dateTime ] ) ;
        sh:message "Onset Date must be an xsd:date or an xsd:dateTime. FHIR's dateTime primitive permits date precision (YYYY, YYYY-MM, YYYY-MM-DD), so a source that stated only a calendar day must not be given an invented time; an untyped or xsd:string literal is not either type."@en ;
        sh:maxCount 1 ;
        sh:name "Onset Date"@en ;
        sh:severity sh:Violation
    ] ;

    # Multi-valued for the same reason as health:testCode above: FHIR R4
    # CodeableConcept.coding is 0..*
    # (https://hl7.org/fhir/R4/datatypes.html#CodeableConcept). A Condition.code
    # commonly carries two ICD-10-CM codings, or an ICD-10-CM coding beside a
    # SNOMED CT one, and dual coding is the normal shape of EHR problem lists
    # rather than an anomaly.
    #
    # NOTE ON FORMAT: neither of these carries an sh:pattern, and that is
    # deliberate. ICD-10-CM permits a letter in any character position
    # (categories such as C4A and M1A are ordinary codes) and SNOMED CT
    # identifiers are 6-18 digit integers, so a naive pattern here would reject
    # valid codes. Format is left to the code system.
    sh:property [
        sh:path health:icd10Code ;
        sh:name "ICD-10-CM Code"@en ;
        sh:severity sh:Violation
    ] ;

    sh:property [
        sh:path health:snomedCode ;
        sh:name "SNOMED CT Code"@en ;
        sh:severity sh:Violation
    ] ;

    sh:property [
        sh:path health:conditionClass ;
        sh:datatype xsd:string ;
        sh:maxCount 1 ;
        sh:name "Condition Class"@en ;
        sh:severity sh:Violation
    ] ;

    sh:property [
        sh:path health:monitoredVitalSigns ;
        sh:maxCount 1 ;
        sh:name "Monitored Vital Signs"@en ;
        sh:message "Monitored vital signs must be a single rdf:List, not repeated"@en ;
        sh:severity sh:Violation
    ] ;

    sh:property [
        sh:path health:sourceRecordId ;
        sh:datatype xsd:string ;
        sh:maxCount 1 ;
        sh:name "Source Record Identifier"@en ;
        sh:severity sh:Violation
    ] ;

    sh:property [
        sh:path cascade:dataProvenance ;
        sh:in (cascade:ClinicalGenerated cascade:EHRVerified cascade:DeviceGenerated
               cascade:PatientReported cascade:SelfReported cascade:AIExtracted) ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:name "Data Provenance"@en ;
        sh:message "Condition must declare exactly one valid cascade:dataProvenance"@en ;
        sh:severity sh:Violation
    ] ;

    sh:property [
        sh:path cascade:schemaVersion ;
        sh:datatype xsd:string ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:pattern "^[0-9]+\\.[0-9]+$" ;
        sh:name "Schema Version"@en ;
        sh:message "Condition must declare exactly one schemaVersion in major.minor form"@en ;
        sh:severity sh:Violation
    ] .

# ============================================================================
# Shape 11: Allergy Record
# ============================================================================

health:AllergyRecordShape a sh:NodeShape ;
    sh:targetClass health:AllergyRecord ;
    rdfs:label "Allergy Record Shape"@en ;
    rdfs:comment "Validation constraints for allergy and intolerance records. Lifted from clinical:AllergyShape and checked against FHIR R4 AllergyIntolerance."@en ;

    # REQUIRED: allergen. An allergy record without an allergen carries no
    # safety information at all, which is worse than having no record.
    sh:property [
        sh:path health:allergen ;
        sh:datatype xsd:string ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:minLength 1 ;
        sh:name "Allergen"@en ;
        sh:message "Allergy must specify exactly one non-empty allergen"@en ;
        sh:severity sh:Violation
    ] ;

    sh:property [
        sh:path health:reaction ;
        sh:datatype xsd:string ;
        sh:name "Reaction"@en ;
        sh:severity sh:Violation
    ] ;

    # FHIR R4 AllergyIntoleranceSeverity
    sh:property [
        sh:path health:allergySeverity ;
        sh:datatype xsd:string ;
        sh:maxCount 1 ;
        sh:in ("mild" "moderate" "severe") ;
        sh:name "Allergy Severity"@en ;
        sh:message "Allergy severity must be one of the FHIR R4 AllergyIntoleranceSeverity codes: mild, moderate, severe"@en ;
        sh:severity sh:Violation
    ] ;

    # FHIR R4 AllergyIntoleranceCategory
    sh:property [
        sh:path health:allergyCategory ;
        sh:datatype xsd:string ;
        sh:maxCount 1 ;
        sh:in ("food" "medication" "environment" "biologic") ;
        sh:name "Allergy Category"@en ;
        sh:message "Allergy category must be one of the FHIR R4 AllergyIntoleranceCategory codes: food, medication, environment, biologic"@en ;
        sh:severity sh:Violation
    ] ;

    sh:property [
        sh:path health:onsetDate ;
        sh:or ( [ sh:datatype xsd:date ] [ sh:datatype xsd:dateTime ] ) ;
        sh:message "Onset Date must be an xsd:date or an xsd:dateTime. FHIR's dateTime primitive permits date precision (YYYY, YYYY-MM, YYYY-MM-DD), so a source that stated only a calendar day must not be given an invented time; an untyped or xsd:string literal is not either type."@en ;
        sh:maxCount 1 ;
        sh:name "Onset Date"@en ;
        sh:severity sh:Violation
    ] ;

    sh:property [
        sh:path health:sourceRecordId ;
        sh:datatype xsd:string ;
        sh:maxCount 1 ;
        sh:name "Source Record Identifier"@en ;
        sh:severity sh:Violation
    ] ;

    sh:property [
        sh:path cascade:dataProvenance ;
        sh:in (cascade:ClinicalGenerated cascade:EHRVerified cascade:DeviceGenerated
               cascade:PatientReported cascade:SelfReported cascade:AIExtracted) ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:name "Data Provenance"@en ;
        sh:message "Allergy must declare exactly one valid cascade:dataProvenance"@en ;
        sh:severity sh:Violation
    ] ;

    sh:property [
        sh:path cascade:schemaVersion ;
        sh:datatype xsd:string ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:pattern "^[0-9]+\\.[0-9]+$" ;
        sh:name "Schema Version"@en ;
        sh:message "Allergy must declare exactly one schemaVersion in major.minor form"@en ;
        sh:severity sh:Violation
    ] .

# ============================================================================
# Shape 12: Immunization Record
# ============================================================================

health:ImmunizationRecordShape a sh:NodeShape ;
    sh:targetClass health:ImmunizationRecord ;
    rdfs:label "Immunization Record Shape"@en ;
    rdfs:comment "Validation constraints for immunization records. Lifted from clinical:ImmunizationShape and checked against FHIR R4 Immunization."@en ;

    # REQUIRED: vaccineName
    sh:property [
        sh:path health:vaccineName ;
        sh:datatype xsd:string ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:minLength 1 ;
        sh:name "Vaccine Name"@en ;
        sh:message "Immunization must have exactly one non-empty vaccineName"@en ;
        sh:severity sh:Violation
    ] ;

    # NOT required here although FHIR R4 Immunization.occurrence is 1..1.
    # clinical:ImmunizationShape constrains no date at all, so requiring one
    # would be a new constraint rather than a lifted one, and would newly fail
    # existing records. Promoting this to sh:minCount 1 is a candidate for a
    # release that is explicitly flagged as tightening.
    sh:property [
        sh:path health:administrationDate ;
        sh:or ( [ sh:datatype xsd:date ] [ sh:datatype xsd:dateTime ] ) ;
        sh:message "Administration Date must be an xsd:date or an xsd:dateTime. FHIR's dateTime primitive permits date precision (YYYY, YYYY-MM, YYYY-MM-DD), so a source that stated only a calendar day must not be given an invented time; an untyped or xsd:string literal is not either type."@en ;
        sh:maxCount 1 ;
        sh:name "Administration Date"@en ;
        sh:severity sh:Violation
    ] ;

    # FHIR R4 ImmunizationStatusCodes
    sh:property [
        sh:path health:status ;
        sh:datatype xsd:string ;
        sh:maxCount 1 ;
        sh:in ("completed" "entered-in-error" "not-done") ;
        sh:name "Status"@en ;
        sh:message "Immunization status must be one of the FHIR R4 ImmunizationStatusCodes: completed, entered-in-error, not-done"@en ;
        sh:severity sh:Violation
    ] ;

    sh:property [
        sh:path health:vaccineCode ;
        sh:datatype xsd:string ;
        sh:maxCount 1 ;
        sh:name "Vaccine Code"@en ;
        sh:severity sh:Violation
    ] ;

    sh:property [
        sh:path health:manufacturer ;
        sh:datatype xsd:string ;
        sh:maxCount 1 ;
        sh:name "Manufacturer"@en ;
        sh:severity sh:Violation
    ] ;

    sh:property [
        sh:path health:lotNumber ;
        sh:datatype xsd:string ;
        sh:maxCount 1 ;
        sh:name "Lot Number"@en ;
        sh:message "Immunization must not carry more than one lotNumber; a lot number identifies a single administered dose"@en ;
        sh:severity sh:Violation
    ] ;

    sh:property [
        sh:path health:doseQuantity ;
        sh:datatype xsd:string ;
        sh:maxCount 1 ;
        sh:name "Dose Quantity"@en ;
        sh:severity sh:Violation
    ] ;

    sh:property [
        sh:path health:route ;
        sh:datatype xsd:string ;
        sh:maxCount 1 ;
        sh:name "Route"@en ;
        sh:severity sh:Violation
    ] ;

    sh:property [
        sh:path health:site ;
        sh:datatype xsd:string ;
        sh:maxCount 1 ;
        sh:name "Administration Site"@en ;
        sh:severity sh:Violation
    ] ;

    sh:property [
        sh:path health:administeringProvider ;
        sh:datatype xsd:string ;
        sh:maxCount 1 ;
        sh:name "Administering Provider"@en ;
        sh:severity sh:Violation
    ] ;

    sh:property [
        sh:path health:administeringLocation ;
        sh:datatype xsd:string ;
        sh:maxCount 1 ;
        sh:name "Administering Location"@en ;
        sh:severity sh:Violation
    ] ;

    sh:property [
        sh:path health:sourceRecordId ;
        sh:datatype xsd:string ;
        sh:maxCount 1 ;
        sh:name "Source Record Identifier"@en ;
        sh:severity sh:Violation
    ] ;

    sh:property [
        sh:path cascade:dataProvenance ;
        sh:in (cascade:ClinicalGenerated cascade:EHRVerified cascade:DeviceGenerated
               cascade:PatientReported cascade:SelfReported cascade:AIExtracted) ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:name "Data Provenance"@en ;
        sh:message "Immunization must declare exactly one valid cascade:dataProvenance"@en ;
        sh:severity sh:Violation
    ] ;

    sh:property [
        sh:path cascade:schemaVersion ;
        sh:datatype xsd:string ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:pattern "^[0-9]+\\.[0-9]+$" ;
        sh:name "Schema Version"@en ;
        sh:message "Immunization must declare exactly one schemaVersion in major.minor form"@en ;
        sh:severity sh:Violation
    ] .

# ============================================================================
# Shape 13: Family History Record
# ============================================================================
#
# There is no clinical:FamilyHistory class and therefore no shape to lift from.
# Constraints here are derived directly from FHIR R4 FamilyMemberHistory, whose
# relationship element is 1..1 and whose condition.code is required within each
# condition entry. The provenance and schemaVersion blocks follow the same
# pattern as every other record shape.

health:FamilyHistoryRecordShape a sh:NodeShape ;
    sh:targetClass health:FamilyHistoryRecord ;
    rdfs:label "Family History Record Shape"@en ;
    rdfs:comment "Validation constraints for family history records. Derived from FHIR R4 FamilyMemberHistory."@en ;

    # REQUIRED: conditionName (FHIR condition.code is required per entry)
    sh:property [
        sh:path health:conditionName ;
        sh:datatype xsd:string ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:minLength 1 ;
        sh:name "Condition Name"@en ;
        sh:message "Family history record must name exactly one condition"@en ;
        sh:severity sh:Violation
    ] ;

    # REQUIRED: relationship (FHIR FamilyMemberHistory.relationship is 1..1).
    # A condition with no stated relative is not family history.
    sh:property [
        sh:path clinical:relationship ;
        sh:datatype xsd:string ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:minLength 1 ;
        sh:name "Relationship"@en ;
        sh:message "Family history record must state exactly one relationship to the patient"@en ;
        sh:severity sh:Violation
    ] ;

    sh:property [
        sh:path health:onsetAge ;
        sh:datatype xsd:integer ;
        sh:maxCount 1 ;
        sh:minInclusive 0 ;
        sh:maxInclusive 130 ;
        sh:name "Onset Age"@en ;
        sh:message "Onset age must be a single integer between 0 and 130"@en ;
        sh:severity sh:Violation
    ] ;

    sh:property [
        sh:path health:sourceRecordId ;
        sh:datatype xsd:string ;
        sh:maxCount 1 ;
        sh:name "Source Record Identifier"@en ;
        sh:severity sh:Violation
    ] ;

    sh:property [
        sh:path cascade:dataProvenance ;
        sh:in (cascade:ClinicalGenerated cascade:EHRVerified cascade:DeviceGenerated
               cascade:PatientReported cascade:SelfReported cascade:AIExtracted) ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:name "Data Provenance"@en ;
        sh:message "Family history record must declare exactly one valid cascade:dataProvenance"@en ;
        sh:severity sh:Violation
    ] ;

    sh:property [
        sh:path cascade:schemaVersion ;
        sh:datatype xsd:string ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:pattern "^[0-9]+\\.[0-9]+$" ;
        sh:name "Schema Version"@en ;
        sh:message "Family history record must declare exactly one schemaVersion in major.minor form"@en ;
        sh:severity sh:Violation
    ] .

# ============================================================================
# Daily Snapshot Shapes (v1.2 — health v2.5)
# ============================================================================
#
# These three classes were defined in health v2.0 and have been emitted ever
# since, but no shape targeted them, so every reading in every history
# container validated vacuously.
#
# health:DailyVitalReading is emitted in TWO forms that do not share their
# value and date predicates:
#
#   Pattern A (HealthProfileSerializer): health:value, health:unit, health:date
#   Pattern B (reference pod):           fhir:valueQuantity, cascade:date
#
# Both are live. The shape therefore requires a date and a value through
# sh:or rather than naming one spelling and failing the other. Reconciling the
# two serializations is a separate change; constraining one of them here would
# have silently invalidated the other.

health:DailyVitalReadingShape a sh:NodeShape ;
    sh:targetClass health:DailyVitalReading ;
    rdfs:label "Daily Vital Reading Shape"@en ;
    rdfs:comment "Validation constraints for a single day's aggregated vital sign reading inside a history container."@en ;

    # REQUIRED: a timestamp, in either spelling. A reading with no date cannot
    # be placed in the time series it lives in.
    sh:or (
        [ sh:property [ sh:path cascade:date ; sh:minCount 1 ] ]
        [ sh:property [ sh:path health:date  ; sh:minCount 1 ] ]
    ) ;
    sh:message "Daily vital reading must carry a timestamp as either cascade:date or health:date"@en ;
    sh:severity sh:Violation ;

    sh:property [
        sh:path cascade:date ;
        sh:datatype xsd:dateTime ;
        sh:maxCount 1 ;
        sh:name "Date"@en ;
        sh:severity sh:Violation
    ] ;

    sh:property [
        sh:path health:date ;
        sh:datatype xsd:dateTime ;
        sh:maxCount 1 ;
        sh:name "Date"@en ;
        sh:severity sh:Violation
    ] ;

    sh:property [
        sh:path cascade:sampleCount ;
        sh:datatype xsd:integer ;
        sh:maxCount 1 ;
        sh:minInclusive 0 ;
        sh:name "Sample Count"@en ;
        sh:message "Sample count must be a single non-negative integer"@en ;
        sh:severity sh:Violation
    ] ;

    sh:property [
        sh:path health:value ;
        sh:maxCount 1 ;
        sh:name "Value"@en ;
        sh:message "Daily vital reading must not carry more than one value"@en ;
        sh:severity sh:Violation
    ] ;

    sh:property [
        sh:path health:unit ;
        sh:datatype xsd:string ;
        sh:maxCount 1 ;
        sh:name "Unit"@en ;
        sh:severity sh:Violation
    ] ;

    sh:property [
        sh:path cascade:loincCode ;
        sh:maxCount 1 ;
        sh:name "LOINC Code"@en ;
        sh:severity sh:Violation
    ] .

health:DailyActivitySnapshotShape a sh:NodeShape ;
    sh:targetClass health:DailyActivitySnapshot ;
    rdfs:label "Daily Activity Snapshot Shape"@en ;
    rdfs:comment "Validation constraints for a single day's activity metrics."@en ;

    sh:property [
        sh:path cascade:date ;
        sh:datatype xsd:dateTime ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:name "Date"@en ;
        sh:message "Daily activity snapshot must carry exactly one cascade:date"@en ;
        sh:severity sh:Violation
    ] ;

    sh:property [
        sh:path health:steps ;
        sh:datatype xsd:integer ;
        sh:maxCount 1 ;
        sh:minInclusive 0 ;
        sh:name "Steps"@en ;
        sh:message "Steps must be a single non-negative integer"@en ;
        sh:severity sh:Violation
    ] ;

    sh:property [
        sh:path health:activeEnergyKcal ;
        sh:datatype xsd:decimal ;
        sh:maxCount 1 ;
        sh:minInclusive 0 ;
        sh:name "Active Energy (kcal)"@en ;
        sh:message "Active energy must be a single non-negative decimal"@en ;
        sh:severity sh:Violation
    ] ;

    sh:property [
        sh:path health:exerciseMinutes ;
        sh:datatype xsd:integer ;
        sh:maxCount 1 ;
        sh:minInclusive 0 ;
        sh:maxInclusive 1440 ;
        sh:name "Exercise Minutes"@en ;
        sh:message "Exercise minutes must be a single integer between 0 and 1440"@en ;
        sh:severity sh:Violation
    ] ;

    sh:property [
        sh:path health:standHours ;
        sh:datatype xsd:integer ;
        sh:maxCount 1 ;
        sh:minInclusive 0 ;
        sh:maxInclusive 24 ;
        sh:name "Stand Hours"@en ;
        sh:message "Stand hours must be a single integer between 0 and 24"@en ;
        sh:severity sh:Violation
    ] .

health:DailySleepSnapshotShape a sh:NodeShape ;
    sh:targetClass health:DailySleepSnapshot ;
    rdfs:label "Daily Sleep Snapshot Shape"@en ;
    rdfs:comment "Validation constraints for a single night's sleep metrics."@en ;

    sh:property [
        sh:path cascade:date ;
        sh:datatype xsd:dateTime ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:name "Date"@en ;
        sh:message "Daily sleep snapshot must carry exactly one cascade:date"@en ;
        sh:severity sh:Violation
    ] ;

    sh:property [
        sh:path health:durationHours ;
        sh:datatype xsd:decimal ;
        sh:maxCount 1 ;
        sh:minInclusive 0 ;
        sh:maxInclusive 24 ;
        sh:name "Sleep Duration (hours)"@en ;
        sh:message "Sleep duration must be a single decimal between 0 and 24 hours"@en ;
        sh:severity sh:Violation
    ] ;

    # No sh:datatype: health:sleepQuality is declared owl:DatatypeProperty with
    # rdfs:range xsd:string in health.ttl, but every known emitter writes an
    # IRI. Asserting either form here would contradict something real, so the
    # shape constrains cardinality and the permitted individuals only.
    sh:property [
        sh:path health:sleepQuality ;
        sh:maxCount 1 ;
        sh:in (health:Excellent health:Good health:Fair health:Poor) ;
        sh:name "Sleep Quality"@en ;
        sh:message "Sleep quality must be one of health:Excellent, health:Good, health:Fair, health:Poor"@en ;
        sh:severity sh:Violation
    ] .
