# Cascade Workbench Vocabulary — SHACL shapes (v1-draft)

@prefix workbench: <https://ns.cascadeprotocol.org/workbench/v1#> .
@prefix sh:   <http://www.w3.org/ns/shacl#> .
@prefix owl:  <http://www.w3.org/2002/07/owl#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix xsd:  <http://www.w3.org/2001/XMLSchema#> .
@prefix oa:   <http://www.w3.org/ns/oa#> .
@prefix prov: <http://www.w3.org/ns/prov#> .
@prefix ical: <http://www.w3.org/2002/12/cal/ical#> .

<https://ns.cascadeprotocol.org/workbench/v1/shapes> a owl:Ontology ;
    owl:imports <https://ns.cascadeprotocol.org/workbench/v1#> ;
    rdfs:comment "SHACL shapes for the Cascade Workbench vocabulary (v1-draft)."@en .

workbench:InvestigationShape a sh:NodeShape ;
    sh:targetClass workbench:Investigation ;
    sh:property [ sh:path workbench:investigationTitle ; sh:minCount 1 ; sh:datatype xsd:string ] ;
    sh:property [ sh:path workbench:aboutPatient ; sh:minCount 1 ] ;
    sh:property [ sh:path workbench:investigationStatus ; sh:minCount 1 ; sh:maxCount 1 ; sh:class workbench:InvestigationStatusValue ] .

workbench:ImportedConversationShape a sh:NodeShape ;
    sh:targetClass workbench:ImportedConversation ;
    sh:property [ sh:path workbench:conversationSource ; sh:minCount 1 ; sh:datatype xsd:string ] ;
    sh:property [ sh:path workbench:rawContentLocation ; sh:minCount 1 ; sh:datatype xsd:string ] .

# ── Append-only record amendment overlays (v1-draft.0.2) ────────────────────

workbench:AmendmentShape a sh:NodeShape ;
    sh:targetClass workbench:Amendment ;
    sh:property [ sh:path workbench:amendsRecord ; sh:minCount 1 ; sh:nodeKind sh:IRI ] ;
    sh:property [ sh:path workbench:amendsProperty ; sh:minCount 1 ; sh:datatype xsd:string ] ;
    sh:property [ sh:path workbench:amendedValue ; sh:minCount 1 ; sh:datatype xsd:string ] ;
    sh:property [ sh:path workbench:amendmentReason ; sh:datatype xsd:string ] .

workbench:AnnotationShape a sh:NodeShape ;
    sh:targetClass workbench:Annotation ;
    sh:property [ sh:path workbench:annotatesRecord ; sh:minCount 1 ; sh:nodeKind sh:IRI ] ;
    sh:property [ sh:path workbench:annotationText ; sh:datatype xsd:string ] ;
    sh:property [ sh:path workbench:annotationProperty ; sh:datatype xsd:string ] ;
    sh:property [ sh:path workbench:annotationValue ; sh:datatype xsd:string ] ;
    # Require at least one of annotationText / annotationValue.
    sh:or (
        [ sh:path workbench:annotationText ; sh:minCount 1 ]
        [ sh:path workbench:annotationValue ; sh:minCount 1 ]
    ) .

workbench:RetractionShape a sh:NodeShape ;
    sh:targetClass workbench:Retraction ;
    sh:property [ sh:path workbench:retractsRecord ; sh:minCount 1 ; sh:nodeKind sh:IRI ] ;
    sh:property [ sh:path workbench:retractionReason ; sh:datatype xsd:string ] ;
    sh:property [ sh:path workbench:supersededBy ; sh:nodeKind sh:IRI ] .

# Content-free audit event (v1-draft.0.3): records the erasure EVENT only —
# erased record id, action, optional category/reason — never a content hash.
workbench:TombstoneShape a sh:NodeShape ;
    sh:targetClass workbench:Tombstone ;
    sh:property [ sh:path workbench:erasedRecord ; sh:minCount 1 ; sh:nodeKind sh:IRI ] ;
    sh:property [ sh:path workbench:erasureAction ; sh:minCount 1 ; sh:datatype xsd:string ] ;
    sh:property [ sh:path workbench:erasedType ; sh:datatype xsd:string ] ;
    sh:property [ sh:path workbench:erasureReason ; sh:datatype xsd:string ] .

# Verification axis (v1-draft.0.3): orthogonal to cascade:dataProvenance.
# Uses sh:in (not sh:class) so the constraint is self-contained in the shapes
# graph — validating a bucket record needs no ontology individual-typing.
workbench:VerificationStatusShape a sh:NodeShape ;
    sh:targetObjectsOf workbench:verificationStatus ;
    sh:in ( workbench:Unverified workbench:Confirmed workbench:Refuted ) .

# ── Notes / flags / follow-ups — the Web Annotation substrate (v1-draft.0.5) ─
# Targets every oa:Annotation in the Pod (they live under notes/). SHACL Core
# only, like the evidence grounding invariant, so `cascade validate` actually
# enforces it. The conditional constraints use the sh:or( sh:not(...) ... )
# pattern: "NOT motivated-by-X, OR carries the fields X requires".

workbench:WebAnnotationShape a sh:NodeShape ;
    sh:targetClass oa:Annotation ;
    # At least one target: a note floats on nothing.
    sh:property [ sh:path oa:hasTarget ; sh:minCount 1 ;
                  sh:nodeKind sh:BlankNodeOrIRI ] ;
    # Exactly which kind of note this is.
    sh:property [ sh:path oa:motivatedBy ; sh:minCount 1 ; sh:nodeKind sh:IRI ] ;
    # Attribution is load-bearing (caregiver vs patient vs agent): required.
    sh:property [ sh:path prov:wasAttributedTo ; sh:minCount 1 ] ;
    sh:property [ sh:path prov:generatedAtTime ; sh:minCount 1 ; sh:maxCount 1 ;
                  sh:datatype xsd:dateTime ] .

# A commenting note must actually say something: motivatedBy oa:commenting
# requires a body.
workbench:CommentingBodyShape a sh:NodeShape ;
    sh:targetClass oa:Annotation ;
    sh:or (
        [ sh:not [ sh:property [ sh:path oa:motivatedBy ; sh:hasValue oa:commenting ] ] ]
        [ sh:property [ sh:path oa:hasBody ; sh:minCount 1 ] ]
    ) .

# A follow-up is a to-do: motivatedBy workbench:followUp requires the RFC 5545
# VTODO status (reused ical:status, closed enum); ical:due stays optional.
workbench:FollowUpShape a sh:NodeShape ;
    sh:targetClass oa:Annotation ;
    sh:or (
        [ sh:not [ sh:property [ sh:path oa:motivatedBy ; sh:hasValue workbench:followUp ] ] ]
        [ sh:property [ sh:path ical:status ; sh:minCount 1 ; sh:maxCount 1 ;
                        sh:in ( "NEEDS-ACTION" "IN-PROCESS" "COMPLETED" "CANCELLED" ) ] ]
    ) .
