# SHACL shapes for the Sentinels event vocabulary.
# Author in Turtle; publish alongside the JSON-LD context.
# Target: SHACL Core (1.1). Validated with tggo/goRDFlib or any SHACL Core processor.

@prefix sh:       <http://www.w3.org/ns/shacl#> .
@prefix rdf:      <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs:     <http://www.w3.org/2000/01/rdf-schema#> .
@prefix xsd:      <http://www.w3.org/2001/XMLSchema#> .
@prefix prov:     <http://www.w3.org/ns/prov#> .
@prefix sosa:     <http://www.w3.org/ns/sosa/> .
@prefix ssn:      <http://www.w3.org/ns/ssn/> .
@prefix schema:   <https://schema.org/> .
@prefix otel:     <https://opentelemetry.io/schemas/resource/> .
@prefix sentinel: <https://sentinels.simon.services/ns/v1#> .

# -----------------------------------------------------------------------------
# Common regex patterns for IRIs
# -----------------------------------------------------------------------------
# Host IRI:     urn:sentinel:host:<host_id>
# Boot IRI:     urn:sentinel:boot:<host_id>:<boot_epoch>
# Sensor IRI:   urn:sentinel:sensor:<host_id>:<sentinel_name>:<instance>
# Run IRI:      urn:sentinel:run:<host_id>:<name>:<instance>:<uuid>
# Event IRI:    urn:sentinel:event:<host_id>:<name>:<instance>:<seq8>
# Feature IRI:  urn:sentinel:feature:<kind>:<host_id>:<key>

# -----------------------------------------------------------------------------
# Base shape: universal fields every sentinel event must carry.
# Other event shapes extend this via sh:node sentinel:EventShape.
# -----------------------------------------------------------------------------

sentinel:EventShape
    a sh:NodeShape ;
    rdfs:label "Base sentinel event shape" ;
    rdfs:comment "Required fields on every event emitted by any sentinel." ;

    # @id must be an event IRI
    sh:nodeKind sh:IRI ;
    sh:pattern "^urn:sentinel:event:[a-zA-Z0-9_-]+:[a-z0-9_-]+:[a-zA-Z0-9_-]+:[0-9]+$" ;

    # Every event must be typed as at least a sosa:Observation and prov:Entity.
    sh:property [
        sh:path rdf:type ;
        sh:minCount 2 ;
        sh:hasValue sosa:Observation ;
        sh:message "Event must be typed as sosa:Observation."
    ] ;
    sh:property [
        sh:path rdf:type ;
        sh:hasValue prov:Entity ;
        sh:message "Event must be typed as prov:Entity."
    ] ;

    # Sequence number
    sh:property [
        sh:path sentinel:seq ;
        sh:datatype xsd:long ;
        sh:minCount 1 ; sh:maxCount 1 ;
        sh:minInclusive 1 ;
        sh:message "seq is required and must be a positive long."
    ] ;

    # Generation time
    sh:property [
        sh:path prov:generatedAtTime ;
        sh:datatype xsd:dateTime ;
        sh:minCount 1 ; sh:maxCount 1 ;
        sh:message "generatedAt is required and must be xsd:dateTime in UTC."
    ] ;

    # Run (Activity) reference
    sh:property [
        sh:path prov:wasGeneratedBy ;
        sh:nodeKind sh:IRI ;
        sh:minCount 1 ; sh:maxCount 1 ;
        sh:pattern "^urn:sentinel:run:" ;
        sh:message "wasGeneratedBy must reference a sentinel run IRI."
    ] ;

    # Boot epoch reference
    sh:property [
        sh:path sentinel:inBootEpoch ;
        sh:nodeKind sh:IRI ;
        sh:minCount 1 ; sh:maxCount 1 ;
        sh:pattern "^urn:sentinel:boot:" ;
        sh:message "bootEpoch must reference a boot epoch IRI."
    ] ;

    # Sensor reference
    sh:property [
        sh:path sosa:madeBySensor ;
        sh:nodeKind sh:IRI ;
        sh:minCount 1 ; sh:maxCount 1 ;
        sh:pattern "^urn:sentinel:sensor:" ;
        sh:message "sensor must reference a sensor IRI."
    ] ;

    # Feature of interest
    sh:property [
        sh:path sosa:hasFeatureOfInterest ;
        sh:nodeKind sh:IRI ;
        sh:minCount 1 ; sh:maxCount 1 ;
        sh:pattern "^urn:sentinel:feature:" ;
        sh:message "featureOfInterest must reference a feature IRI."
    ] ;

    # Host reference
    sh:property [
        sh:path sentinel:onHost ;
        sh:nodeKind sh:IRI ;
        sh:minCount 1 ; sh:maxCount 1 ;
        sh:pattern "^urn:sentinel:host:" ;
        sh:message "host must reference a host IRI."
    ] .

# -----------------------------------------------------------------------------
# Run (prov:Activity) shape
# -----------------------------------------------------------------------------

sentinel:RunShape
    a sh:NodeShape ;
    rdfs:label "Sentinel run shape" ;
    sh:targetClass prov:Activity ;
    sh:nodeKind sh:IRI ;
    sh:pattern "^urn:sentinel:run:" ;

    sh:property [
        sh:path prov:startedAtTime ;
        sh:datatype xsd:dateTime ;
        sh:minCount 1 ; sh:maxCount 1 ;
    ] ;
    sh:property [
        sh:path prov:endedAtTime ;
        sh:datatype xsd:dateTime ;
        sh:maxCount 1 ;
    ] ;
    sh:property [
        sh:path prov:wasAssociatedWith ;
        sh:nodeKind sh:IRI ;
        sh:minCount 1 ;
        sh:message "Run must be associated with at least one agent (the sentinel process)."
    ] ;
    sh:property [
        sh:path otel:service.name ;
        sh:datatype xsd:string ;
        sh:minCount 1 ; sh:maxCount 1 ;
    ] ;
    sh:property [
        sh:path otel:service.version ;
        sh:datatype xsd:string ;
        sh:maxCount 1 ;
    ] ;
    sh:property [
        sh:path otel:process.pid ;
        sh:datatype xsd:int ;
        sh:maxCount 1 ;
    ] .

# -----------------------------------------------------------------------------
# Start / Stop / Heartbeat — common to all sentinels
# -----------------------------------------------------------------------------

sentinel:StartShape
    a sh:NodeShape ;
    sh:targetClass sentinel:Start ;
    sh:node sentinel:EventShape ;
    sh:property [
        sh:path sentinel:reason ;
        sh:datatype xsd:string ;
        sh:minCount 1 ; sh:maxCount 1 ;
        sh:in ( "clean_previous" "unclean_previous" "first_ever" ) ;
        sh:message "Start reason must be one of: clean_previous, unclean_previous, first_ever."
    ] .

sentinel:StopShape
    a sh:NodeShape ;
    sh:targetClass sentinel:Stop ;
    sh:node sentinel:EventShape ;
    sh:property [
        sh:path sentinel:reason ;
        sh:datatype xsd:string ;
        sh:minCount 1 ; sh:maxCount 1 ;
        sh:pattern "^(signal:(TERM|INT|HUP)|panic|config_error|internal_error)$" ;
        sh:message "Stop reason must match the known stop reasons."
    ] ;
    sh:property [
        sh:path sentinel:uptimeMs ;
        sh:datatype xsd:long ;
        sh:minCount 1 ; sh:maxCount 1 ;
        sh:minInclusive 0 ;
    ] .

sentinel:HeartbeatShape
    a sh:NodeShape ;
    sh:targetClass sentinel:Heartbeat ;
    sh:node sentinel:EventShape .
    # Heartbeats carry no sentinel-specific payload beyond the base envelope.

# -----------------------------------------------------------------------------
# sentinel-conn — connection events
# -----------------------------------------------------------------------------

sentinel:ConnectionUpShape
    a sh:NodeShape ;
    sh:targetClass sentinel:ConnectionUp ;
    sh:node sentinel:EventShape ;

    sh:property [
        sh:path sentinel:target ;
        sh:datatype xsd:string ;
        sh:minCount 1 ; sh:maxCount 1 ;
        sh:pattern "^tcp:[^:]+:[0-9]{1,5}$" ;
        sh:message "target must be of the form tcp:<host>:<port>."
    ] ;

    # Feature of interest must be a peer feature
    sh:property [
        sh:path sosa:hasFeatureOfInterest ;
        sh:pattern "^urn:sentinel:feature:peer:" ;
        sh:message "ConnectionUp must reference a peer feature."
    ] .

sentinel:ConnectionDownShape
    a sh:NodeShape ;
    sh:targetClass sentinel:ConnectionDown ;
    sh:node sentinel:EventShape ;

    sh:property [
        sh:path sentinel:reason ;
        sh:datatype xsd:string ;
        sh:minCount 1 ; sh:maxCount 1 ;
        sh:in (
            "dial_refused" "dial_timeout" "host_unreachable" "net_unreachable"
            "read_eof" "read_reset" "keepalive_failed" "write_error"
            "dial_other"
        ) ;
        sh:message "ConnectionDown reason must be one of the defined classification values."
    ] ;
    sh:property [
        sh:path sentinel:errorDetail ;
        sh:datatype xsd:string ;
        sh:maxCount 1 ;
        sh:maxLength 1024 ;
    ] ;
    sh:property [
        sh:path sentinel:uptimeMs ;
        sh:datatype xsd:long ;
        sh:minCount 1 ; sh:maxCount 1 ;
        sh:minInclusive 0 ;
        sh:message "ConnectionDown must report uptime of the just-ended session (0 if never established)."
    ] ;
    sh:property [
        sh:path sosa:hasFeatureOfInterest ;
        sh:pattern "^urn:sentinel:feature:peer:" ;
    ] .

# -----------------------------------------------------------------------------
# sentinel-kernel — kernel events
# -----------------------------------------------------------------------------

sentinel:PanicPreviousBootShape
    a sh:NodeShape ;
    sh:targetClass sentinel:PanicPreviousBoot ;
    sh:node sentinel:EventShape ;

    sh:property [
        sh:path sentinel:kmsgText ;
        sh:datatype xsd:string ;
        sh:minCount 1 ; sh:maxCount 1 ;
        sh:minLength 1 ;
        sh:maxLength 65536 ;
        sh:message "Panic record text is required, bounded to 64KiB."
    ] ;
    sh:property [
        sh:path sosa:hasFeatureOfInterest ;
        sh:pattern "^urn:sentinel:feature:kernel:" ;
    ] .

sentinel:KmsgEventShape
    a sh:NodeShape ;
    sh:targetClass sentinel:KmsgEvent ;
    sh:node sentinel:EventShape ;

    sh:property [
        sh:path sentinel:kmsgPriority ;
        sh:datatype xsd:int ;
        sh:minCount 1 ; sh:maxCount 1 ;
        sh:minInclusive 0 ; sh:maxInclusive 7 ;
        sh:message "kmsg priority must be 0 (emerg) to 7 (debug)."
    ] ;
    sh:property [
        sh:path sentinel:kmsgFacility ;
        sh:datatype xsd:string ;
        sh:maxCount 1 ;
    ] ;
    sh:property [
        sh:path sentinel:kmsgText ;
        sh:datatype xsd:string ;
        sh:minCount 1 ; sh:maxCount 1 ;
        sh:minLength 1 ;
        sh:maxLength 8192 ;
    ] ;
    sh:property [
        sh:path sosa:hasFeatureOfInterest ;
        sh:pattern "^urn:sentinel:feature:kernel:" ;
    ] .

sentinel:TaintChangeShape
    a sh:NodeShape ;
    sh:targetClass sentinel:TaintChange ;
    sh:node sentinel:EventShape ;

    sh:property [
        sh:path sentinel:oldTaintFlags ;
        sh:datatype xsd:long ;
        sh:minCount 1 ; sh:maxCount 1 ;
        sh:minInclusive 0 ;
    ] ;
    sh:property [
        sh:path sentinel:taintFlags ;
        sh:datatype xsd:long ;
        sh:minCount 1 ; sh:maxCount 1 ;
        sh:minInclusive 0 ;
    ] ;
    sh:property [
        sh:path sosa:hasFeatureOfInterest ;
        sh:pattern "^urn:sentinel:feature:kernel:" ;
    ] .

# -----------------------------------------------------------------------------
# sentinel-clock — clock drift and sync
# -----------------------------------------------------------------------------

sentinel:ClockSkewShape
    a sh:NodeShape ;
    sh:targetClass sentinel:ClockSkew ;
    sh:node sentinel:EventShape ;

    sh:property [
        sh:path sentinel:monotonicDeltaMs ;
        sh:datatype xsd:long ;
        sh:minCount 1 ; sh:maxCount 1 ;
    ] ;
    sh:property [
        sh:path sentinel:wallDeltaMs ;
        sh:datatype xsd:long ;
        sh:minCount 1 ; sh:maxCount 1 ;
    ] ;
    sh:property [
        sh:path sentinel:clockSkewMs ;
        sh:datatype xsd:long ;
        sh:minCount 1 ; sh:maxCount 1 ;
        sh:message "skew must be reported (wall_delta - monotonic_delta) so consumers don't recompute."
    ] ;
    sh:property [
        sh:path sosa:hasFeatureOfInterest ;
        sh:pattern "^urn:sentinel:feature:clock:" ;
    ] .

sentinel:NtpStateChangeShape
    a sh:NodeShape ;
    sh:targetClass sentinel:NtpStateChange ;
    sh:node sentinel:EventShape ;

    sh:property [
        sh:path sentinel:ntpState ;
        sh:datatype xsd:string ;
        sh:minCount 1 ; sh:maxCount 1 ;
        sh:in ( "ok" "ins" "del" "oop" "wait" "error" ) ;
        sh:message "ntpState must match the adjtimex status values."
    ] ;
    sh:property [
        sh:path sentinel:ntpOffsetUs ;
        sh:datatype xsd:long ;
        sh:maxCount 1 ;
    ] ;
    sh:property [
        sh:path sosa:hasFeatureOfInterest ;
        sh:pattern "^urn:sentinel:feature:clock:" ;
    ] .

sentinel:ClocksourceChangeShape
    a sh:NodeShape ;
    sh:targetClass sentinel:ClocksourceChange ;
    sh:node sentinel:EventShape ;

    sh:property [
        sh:path sentinel:oldClocksource ;
        sh:datatype xsd:string ;
        sh:minCount 1 ; sh:maxCount 1 ;
    ] ;
    sh:property [
        sh:path sentinel:newClocksource ;
        sh:datatype xsd:string ;
        sh:minCount 1 ; sh:maxCount 1 ;
    ] ;
    sh:property [
        sh:path sosa:hasFeatureOfInterest ;
        sh:pattern "^urn:sentinel:feature:clock:" ;
    ] .

# -----------------------------------------------------------------------------
# sentinel-disk — filesystem and mount
# -----------------------------------------------------------------------------

sentinel:CanaryResultShape
    a sh:NodeShape ;
    sh:targetClass sentinel:CanaryResult ;
    sh:node sentinel:EventShape ;

    sh:property [
        sh:path sentinel:canaryResult ;
        sh:datatype xsd:string ;
        sh:minCount 1 ; sh:maxCount 1 ;
        sh:in ( "ok" "write_failed" "fsync_failed" "read_mismatch" "permission_denied" "path_missing" ) ;
    ] ;
    sh:property [
        sh:path sentinel:mountPath ;
        sh:datatype xsd:string ;
        sh:minCount 1 ; sh:maxCount 1 ;
        sh:minLength 1 ;
    ] ;
    sh:property [
        sh:path sentinel:canaryLatencyMs ;
        sh:datatype xsd:long ;
        sh:maxCount 1 ;
        sh:minInclusive 0 ;
    ] ;
    sh:property [
        sh:path sosa:hasFeatureOfInterest ;
        sh:pattern "^urn:sentinel:feature:mount:" ;
    ] .

sentinel:SpaceThresholdShape
    a sh:NodeShape ;
    sh:targetClass sentinel:SpaceThreshold ;
    sh:node sentinel:EventShape ;

    sh:property [
        sh:path sentinel:spaceKind ;
        sh:datatype xsd:string ;
        sh:minCount 1 ; sh:maxCount 1 ;
        sh:in ( "bytes" "inodes" ) ;
    ] ;
    sh:property [
        sh:path sentinel:spacePercentFree ;
        sh:datatype xsd:decimal ;
        sh:minCount 1 ; sh:maxCount 1 ;
        sh:minInclusive 0 ; sh:maxInclusive 100 ;
    ] ;
    sh:property [
        sh:path sentinel:spaceBandCrossed ;
        sh:datatype xsd:string ;
        sh:minCount 1 ; sh:maxCount 1 ;
        sh:in ( "20pct" "10pct" "5pct" "1pct" "recovered" ) ;
    ] .

sentinel:MountStateChangeShape
    a sh:NodeShape ;
    sh:targetClass sentinel:MountStateChange ;
    sh:node sentinel:EventShape ;

    sh:property [
        sh:path sentinel:mountPath ;
        sh:datatype xsd:string ;
        sh:minCount 1 ; sh:maxCount 1 ;
    ] ;
    sh:property [
        sh:path sentinel:mountState ;
        sh:datatype xsd:string ;
        sh:minCount 1 ; sh:maxCount 1 ;
        sh:in ( "appeared" "disappeared" "remounted_ro" "remounted_rw" ) ;
    ] .

sentinel:SmartAttributeShape
    a sh:NodeShape ;
    sh:targetClass sentinel:SmartAttribute ;
    sh:node sentinel:EventShape ;

    sh:property [
        sh:path sentinel:smartAttributeId ;
        sh:datatype xsd:int ;
        sh:minCount 1 ; sh:maxCount 1 ;
        sh:minInclusive 1 ; sh:maxInclusive 255 ;
    ] ;
    sh:property [
        sh:path sentinel:smartAttributeName ;
        sh:datatype xsd:string ;
        sh:minCount 1 ; sh:maxCount 1 ;
    ] ;
    sh:property [
        sh:path sentinel:smartRawValue ;
        sh:datatype xsd:long ;
        sh:minCount 1 ; sh:maxCount 1 ;
    ] .

# -----------------------------------------------------------------------------
# sentinel-shipper — the drainer
# -----------------------------------------------------------------------------

sentinel:ShipResultShape
    a sh:NodeShape ;
    sh:targetClass sentinel:ShipResult ;
    sh:node sentinel:EventShape ;

    sh:property [
        sh:path sentinel:shipSource ;
        sh:datatype xsd:string ;
        sh:minCount 1 ; sh:maxCount 1 ;
    ] ;
    sh:property [
        sh:path sentinel:shipDestination ;
        sh:datatype xsd:string ;
        sh:minCount 1 ; sh:maxCount 1 ;
    ] ;
    sh:property [
        sh:path sentinel:shipEventCount ;
        sh:datatype xsd:long ;
        sh:minCount 1 ; sh:maxCount 1 ;
        sh:minInclusive 0 ;
    ] ;
    sh:property [
        sh:path sentinel:shipLatencyMs ;
        sh:datatype xsd:long ;
        sh:minCount 1 ; sh:maxCount 1 ;
        sh:minInclusive 0 ;
    ] .

sentinel:ShipFailureShape
    a sh:NodeShape ;
    sh:targetClass sentinel:ShipFailure ;
    sh:node sentinel:EventShape ;

    sh:property [
        sh:path sentinel:shipDestination ;
        sh:datatype xsd:string ;
        sh:minCount 1 ; sh:maxCount 1 ;
    ] ;
    sh:property [
        sh:path sentinel:reason ;
        sh:datatype xsd:string ;
        sh:minCount 1 ; sh:maxCount 1 ;
    ] ;
    sh:property [
        sh:path sentinel:errorDetail ;
        sh:datatype xsd:string ;
        sh:maxCount 1 ;
        sh:maxLength 2048 ;
    ] ;
    sh:property [
        sh:path sentinel:shipRetryCount ;
        sh:datatype xsd:int ;
        sh:minCount 1 ; sh:maxCount 1 ;
        sh:minInclusive 0 ;
    ] .

sentinel:BacklogShape
    a sh:NodeShape ;
    sh:targetClass sentinel:Backlog ;
    sh:node sentinel:EventShape ;

    sh:property [
        sh:path sentinel:backlogBytes ;
        sh:datatype xsd:long ;
        sh:minCount 1 ; sh:maxCount 1 ;
        sh:minInclusive 0 ;
    ] ;
    sh:property [
        sh:path sentinel:backlogEvents ;
        sh:datatype xsd:long ;
        sh:minCount 1 ; sh:maxCount 1 ;
        sh:minInclusive 0 ;
    ] .

# -----------------------------------------------------------------------------
# Store overflow — emitted when a sentinel drops events due to size cap
# -----------------------------------------------------------------------------

sentinel:StoreOverflowShape
    a sh:NodeShape ;
    sh:targetClass sentinel:StoreOverflow ;
    sh:node sentinel:EventShape ;

    sh:property [
        sh:path sentinel:droppedEvents ;
        sh:datatype xsd:long ;
        sh:minCount 1 ; sh:maxCount 1 ;
        sh:minInclusive 1 ;
        sh:message "StoreOverflow must report how many events were lost."
    ] ;
    sh:property [
        sh:path sentinel:droppedFromSeq ;
        sh:datatype xsd:long ;
        sh:minCount 1 ; sh:maxCount 1 ;
    ] ;
    sh:property [
        sh:path sentinel:droppedToSeq ;
        sh:datatype xsd:long ;
        sh:minCount 1 ; sh:maxCount 1 ;
    ] .

# -----------------------------------------------------------------------------
# Validation failure — emitted by the shipper when an event fails SHACL
# -----------------------------------------------------------------------------

sentinel:ValidationFailureShape
    a sh:NodeShape ;
    sh:targetClass sentinel:ValidationFailure ;
    sh:node sentinel:EventShape ;

    sh:property [
        sh:path sentinel:offendingEvent ;
        sh:nodeKind sh:IRI ;
        sh:minCount 1 ; sh:maxCount 1 ;
        sh:pattern "^urn:sentinel:event:" ;
    ] ;
    sh:property [
        sh:path sentinel:validationReport ;
        sh:datatype xsd:string ;
        sh:minCount 1 ; sh:maxCount 1 ;
        sh:message "A serialized SHACL validation report (Turtle or JSON-LD) must be attached."
    ] .
