# SHACL shapes for the Sentinels v2 vocabulary — additive extension of v1.
# Adds three event-type families derived from OpenTelemetry signals:
#   - sentinel:Span      — one OTel span
#   - sentinel:Metric    — one observation of an OTel metric instrument
#   - sentinel:LogEntry  — one OTel log record
# All three sh:and the v1 sentinel:EventShape so the universal envelope
# (seq, generatedAt, host, sensor, run, boot, feature) still applies.
#
# Validated with tggo/goRDFlib or any SHACL Core processor.
# v1 is loaded alongside this file by the shipper; the shapes co-exist.

@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 otel:     <https://opentelemetry.io/schemas/resource/> .
@prefix sentinel: <https://sentinels.simon.services/ns/v1#> .
@prefix v2:       <https://sentinels.simon.services/ns/v2#> .

# -----------------------------------------------------------------------------
# Common helpers
# -----------------------------------------------------------------------------
# OTel trace_id is 16 bytes hex-encoded → 32 lowercase-hex chars
# OTel span_id  is 8 bytes hex-encoded  → 16 lowercase-hex chars
#
# Feature IRI for OTel signals: urn:sentinel:feature:otel:<host_id>:<service>
# Sensor IRI for the collector itself remains the v1 pattern.

# -----------------------------------------------------------------------------
# sentinel:Span — one OTel span
# -----------------------------------------------------------------------------

v2:SpanShape
    a sh:NodeShape ;
    rdfs:label "OTel span event" ;
    rdfs:comment "One span emitted by an OTel-instrumented process. Universal envelope inherited from v1 sentinel:EventShape." ;
    sh:targetClass v2:Span ;
    sh:node sentinel:EventShape ;

    sh:property [
        sh:path otel:trace.id ;
        sh:datatype xsd:string ;
        sh:minCount 1 ; sh:maxCount 1 ;
        sh:pattern "^[0-9a-f]{32}$" ;
        sh:message "traceId must be 32 lowercase hex chars (16 bytes)."
    ] ;
    sh:property [
        sh:path otel:span.id ;
        sh:datatype xsd:string ;
        sh:minCount 1 ; sh:maxCount 1 ;
        sh:pattern "^[0-9a-f]{16}$" ;
        sh:message "spanId must be 16 lowercase hex chars (8 bytes)."
    ] ;
    sh:property [
        sh:path otel:parent.span.id ;
        sh:datatype xsd:string ;
        sh:maxCount 1 ;
        sh:pattern "^[0-9a-f]{16}$" ;
        sh:message "parentSpanId, when present, must be 16 lowercase hex chars."
    ] ;
    sh:property [
        sh:path v2:spanName ;
        sh:datatype xsd:string ;
        sh:minCount 1 ; sh:maxCount 1 ;
        sh:minLength 1 ;
        sh:maxLength 1024 ;
    ] ;
    sh:property [
        sh:path v2:spanKind ;
        sh:datatype xsd:string ;
        sh:minCount 1 ; sh:maxCount 1 ;
        sh:in ( "INTERNAL" "SERVER" "CLIENT" "PRODUCER" "CONSUMER" ) ;
        sh:message "spanKind must match one of OTel SpanKind enum values."
    ] ;
    sh:property [
        sh:path v2:startTimeUnixNano ;
        sh:datatype xsd:long ;
        sh:minCount 1 ; sh:maxCount 1 ;
        sh:minInclusive 0 ;
    ] ;
    sh:property [
        sh:path v2:endTimeUnixNano ;
        sh:datatype xsd:long ;
        sh:minCount 1 ; sh:maxCount 1 ;
        sh:minInclusive 0 ;
        sh:message "endTimeUnixNano is required (a span without an end is not yet exportable)."
    ] ;
    sh:property [
        sh:path v2:statusCode ;
        sh:datatype xsd:string ;
        sh:maxCount 1 ;
        sh:in ( "UNSET" "OK" "ERROR" ) ;
    ] ;
    sh:property [
        sh:path v2:statusMessage ;
        sh:datatype xsd:string ;
        sh:maxCount 1 ;
        sh:maxLength 4096 ;
    ] ;
    sh:property [
        sh:path otel:service.name ;
        sh:datatype xsd:string ;
        sh:minCount 1 ; sh:maxCount 1 ;
        sh:message "service.name is required so spans can be filtered per producer."
    ] ;
    sh:property [
        sh:path sosa:hasFeatureOfInterest ;
        sh:pattern "^urn:sentinel:feature:otel:" ;
        sh:message "Span featureOfInterest must reference an otel feature IRI."
    ] .

# -----------------------------------------------------------------------------
# sentinel:Metric — one observation of an OTel metric instrument
# -----------------------------------------------------------------------------
# Each emitted record is ONE data point. Counter/gauge use metricValue.
# Histograms emit one record per bucket boundary plus one summary record
# carrying histogramSum/histogramCount.

v2:MetricShape
    a sh:NodeShape ;
    rdfs:label "OTel metric data point" ;
    rdfs:comment "One observation. Universal envelope inherited from v1." ;
    sh:targetClass v2:Metric ;
    sh:node sentinel:EventShape ;

    sh:property [
        sh:path v2:instrumentName ;
        sh:datatype xsd:string ;
        sh:minCount 1 ; sh:maxCount 1 ;
        sh:minLength 1 ;
        sh:maxLength 256 ;
    ] ;
    sh:property [
        sh:path v2:instrumentKind ;
        sh:datatype xsd:string ;
        sh:minCount 1 ; sh:maxCount 1 ;
        sh:in (
            "counter" "updowncounter" "gauge"
            "histogram" "histogramBucket" "histogramSummary"
            "exponentialHistogram"
        ) ;
        sh:message "instrumentKind must match an OTel instrument kind or one of our histogram-decomposition variants."
    ] ;
    sh:property [
        sh:path v2:instrumentUnit ;
        sh:datatype xsd:string ;
        sh:maxCount 1 ;
        sh:maxLength 64 ;
    ] ;
    sh:property [
        sh:path v2:timeUnixNano ;
        sh:datatype xsd:long ;
        sh:minCount 1 ; sh:maxCount 1 ;
        sh:minInclusive 0 ;
    ] ;
    sh:property [
        sh:path v2:metricValue ;
        sh:datatype xsd:double ;
        sh:maxCount 1 ;
        sh:message "metricValue carries counter/gauge/updowncounter values."
    ] ;
    sh:property [
        sh:path v2:histogramBucketBound ;
        sh:datatype xsd:double ;
        sh:maxCount 1 ;
    ] ;
    sh:property [
        sh:path v2:histogramBucketCount ;
        sh:datatype xsd:long ;
        sh:maxCount 1 ;
        sh:minInclusive 0 ;
    ] ;
    sh:property [
        sh:path v2:histogramSum ;
        sh:datatype xsd:double ;
        sh:maxCount 1 ;
    ] ;
    sh:property [
        sh:path v2:histogramCount ;
        sh:datatype xsd:long ;
        sh:maxCount 1 ;
        sh:minInclusive 0 ;
    ] ;
    sh:property [
        sh:path otel:service.name ;
        sh:datatype xsd:string ;
        sh:minCount 1 ; sh:maxCount 1 ;
    ] ;
    sh:property [
        sh:path sosa:hasFeatureOfInterest ;
        sh:pattern "^urn:sentinel:feature:otel:" ;
    ] .

# -----------------------------------------------------------------------------
# sentinel:LogEntry — one OTel log record
# -----------------------------------------------------------------------------
# severityNumber: 1..24 per OTel SeverityNumber enum.
# traceId / spanId optional — present when the log was emitted inside a span.

v2:LogEntryShape
    a sh:NodeShape ;
    rdfs:label "OTel log record" ;
    rdfs:comment "One log line emitted via the OTel log API. Universal envelope inherited from v1." ;
    sh:targetClass v2:LogEntry ;
    sh:node sentinel:EventShape ;

    sh:property [
        sh:path v2:severityNumber ;
        sh:datatype xsd:int ;
        sh:minCount 1 ; sh:maxCount 1 ;
        sh:minInclusive 1 ;
        sh:maxInclusive 24 ;
        sh:message "severityNumber must be in the OTel SeverityNumber range 1..24."
    ] ;
    sh:property [
        sh:path v2:severityText ;
        sh:datatype xsd:string ;
        sh:minCount 1 ; sh:maxCount 1 ;
        sh:minLength 1 ;
        sh:maxLength 32 ;
    ] ;
    sh:property [
        sh:path v2:logBody ;
        sh:datatype xsd:string ;
        sh:minCount 1 ; sh:maxCount 1 ;
        sh:maxLength 65536 ;
        sh:message "logBody is required, bounded to 64KiB."
    ] ;
    sh:property [
        sh:path v2:timeUnixNano ;
        sh:datatype xsd:long ;
        sh:minCount 1 ; sh:maxCount 1 ;
        sh:minInclusive 0 ;
    ] ;
    sh:property [
        sh:path otel:trace.id ;
        sh:datatype xsd:string ;
        sh:maxCount 1 ;
        sh:pattern "^[0-9a-f]{32}$" ;
        sh:message "traceId, when present, must be 32 lowercase hex chars."
    ] ;
    sh:property [
        sh:path otel:span.id ;
        sh:datatype xsd:string ;
        sh:maxCount 1 ;
        sh:pattern "^[0-9a-f]{16}$" ;
        sh:message "spanId, when present, must be 16 lowercase hex chars."
    ] ;
    sh:property [
        sh:path otel:service.name ;
        sh:datatype xsd:string ;
        sh:minCount 1 ; sh:maxCount 1 ;
    ] ;
    sh:property [
        sh:path sosa:hasFeatureOfInterest ;
        sh:pattern "^urn:sentinel:feature:otel:" ;
    ] .

# -----------------------------------------------------------------------------
# v2:CoverageProfile — one observation of which Go source line was hit
# in production over an observation window. ADR-013.
#
# Each event reports per-(package, function?, line range) hit count for a
# bounded time window (profileObservedAt = window end). The drainer
# (sentinel-coverage-drain) translates `go tool covdata textfmt` output
# into a stream of these events; sentinel-shipper drains the stream into
# Postgres → QLever exactly like every other v2 event.
# -----------------------------------------------------------------------------

v2:CoverageProfileShape
    a sh:NodeShape ;
    rdfs:label "Coverage profile observation" ;
    rdfs:comment "One observation of Go-source line coverage from a running -cover-instrumented binary. ADR-013." ;
    sh:targetClass v2:CoverageProfile ;
    sh:node sentinel:EventShape ;

    sh:property [
        sh:path v2:profilePackage ;
        sh:datatype xsd:string ;
        sh:minCount 1 ; sh:maxCount 1 ;
        sh:minLength 1 ;
        sh:message "profilePackage is the Go import path the line lives in (e.g. \"git.simon.services/.../sentinel-conn\")."
    ] ;
    sh:property [
        sh:path v2:profileFunction ;
        sh:datatype xsd:string ;
        sh:maxCount 1 ;
        sh:message "Function name within profilePackage. Optional — empty for line-only observations."
    ] ;
    sh:property [
        sh:path v2:profileLineStart ;
        sh:datatype xsd:int ;
        sh:minCount 1 ; sh:maxCount 1 ;
        sh:minInclusive 1 ;
        sh:message "First source line of the covered range (1-indexed)."
    ] ;
    sh:property [
        sh:path v2:profileLineEnd ;
        sh:datatype xsd:int ;
        sh:minCount 1 ; sh:maxCount 1 ;
        sh:minInclusive 1 ;
        sh:message "Last source line of the covered range (1-indexed; equal to lineStart for single-line observations)."
    ] ;
    sh:property [
        sh:path v2:profileHits ;
        sh:datatype xsd:long ;
        sh:minCount 1 ; sh:maxCount 1 ;
        sh:minInclusive 0 ;
        sh:message "Number of times the range was hit during the observation window. Zero means present-but-not-hit; absent ranges are not emitted."
    ] ;
    sh:property [
        sh:path v2:profileObservedAt ;
        sh:datatype xsd:dateTime ;
        sh:minCount 1 ; sh:maxCount 1 ;
        sh:message "End of the observation window."
    ] ;
    sh:property [
        sh:path sosa:hasFeatureOfInterest ;
        sh:pattern "^urn:sentinel:feature:coverage:" ;
        sh:message "Coverage events use a feature IRI urn:sentinel:feature:coverage:<host_id>:<binary>."
    ] .
