@prefix sh:    <http://www.w3.org/ns/shacl#> .
@prefix vso:   <https://w3id.org/vson/v1/ontology#> .
@prefix vss:   <https://w3id.org/vson/v1/shapes#> .
@prefix rcc:   <https://w3id.org/vson/v1/rcc8#> .
@prefix xsd:   <http://www.w3.org/2001/XMLSchema#> .
@prefix rdf:   <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs:  <http://www.w3.org/2000/01/rdf-schema#> .
@prefix owl:   <http://www.w3.org/2002/07/owl#> .
@prefix dc:    <http://purl.org/dc/terms/> .
@prefix vann:  <http://purl.org/vocab/vann/> .

# The document IRI names the shapes graph itself, not a shape inside it. It was
# typed sh:NodeShape until v1.1 — inert at validation time (a node shape with no
# sh:target* and no constraints selects nothing), but wrong as metadata: it told
# every reader and every vocabulary tool that this file's identity is a shape.
# owl:Ontology is what the declaration always meant.
<https://w3id.org/vson/v1/shapes>
    a owl:Ontology ;
    rdfs:label "VSON v1.2 SHACL shapes" ;
    rdfs:comment "Strict validation profile: the shapes a conformant VSON document must satisfy." ;
    dc:title "VSON-S shapes (strict profile)" ;
    dc:creator "yamancan" ;
    dc:license <https://www.apache.org/licenses/LICENSE-2.0> ;
    dc:modified "2026-07-31"^^xsd:date ;
    owl:versionInfo "1.2" ;
    vann:preferredNamespacePrefix "vss" ;
    vann:preferredNamespaceUri "https://w3id.org/vson/v1/shapes#" .

#################################################################
# NB — why the sh:class checks below carry sh:not guards
#
# Validation runs with inference="rdfs" (docs/vson.md §2, clause C3). Six of the
# sh:class checks in this file sit on a property whose rdfs:range in
# ontology/vso.ttl already names that very class, so the type is *entailed* onto
# every value node before validation and the bare sh:class can never fail — it
# is vacuously true. Each of those sites therefore also carries sh:not guards
# naming the classes vso.ttl declares disjoint from the range. A value asserted
# into a disjoint class keeps its asserted type AND picks up the entailed range
# type, so the sh:not fires and the check has teeth. Same reasoning as the
# vss:HasQualityShape note further down, which documented the trap first.
#
# Guarded sites: CompositionShape/framedBy, EventShape/agent,
# StativeShape/experiencer, DirectionalNeedsViewerShape/viewer,
# BeliefStateShape/experiencer, PersonaShape/hasInvariant.
#################################################################

#################################################################
# Composition — must depict ≥1 entity, must be framed
#################################################################

vss:CompositionShape a sh:NodeShape ;
    sh:targetClass vso:Composition ;
    sh:property [
        sh:path vso:depicts ;
        sh:minCount 1 ;
        sh:message "A Composition must depict at least one Entity (vso:depicts)."
    ] ;
    sh:property [
        sh:path vso:framedBy ;
        sh:nodeKind sh:BlankNodeOrIRI ;
        sh:class vso:Frame ;
        # sh:class vso:Frame is entailed (vso:framedBy rdfs:range vso:Frame).
        # Frame owl:disjointWith Entity, so an Entity-typed frame fires here.
        sh:not [ sh:class vso:Entity ] ;
        sh:message "vso:framedBy must reference a Frame (SceneContext / VisualStyle / CameraView / Persona), never an Entity."
    ] .

#################################################################
# Event / Process / Stative — must have a lemma and at most one agent
#################################################################

vss:EventShape a sh:NodeShape ;
    sh:targetClass vso:Event ;
    sh:property [
        sh:path vso:lemma ;
        sh:datatype xsd:string ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:message "Event must have exactly one vso:lemma."
    ] ;
    sh:property [
        sh:path vso:agent ;
        sh:nodeKind sh:BlankNodeOrIRI ;
        sh:maxCount 1 ;
        sh:class vso:Endurant ;
        # sh:class vso:Endurant is entailed (vso:agent rdfs:range vso:Endurant).
        # Endurant is disjoint from Perdurant (ontology/vso.ttl §1) and from
        # Frame (Frame ⊥ Entity), so an occurrence or a camera used as an
        # agent fires here.
        sh:not [ sh:class vso:Perdurant ] ;
        sh:not [ sh:class vso:Frame ] ;
        sh:message "vso:agent must reference an Endurant, not a Perdurant or a Frame."
    ] .

vss:ProcessShape a sh:NodeShape ;
    sh:targetClass vso:Process ;
    sh:property [ sh:path vso:lemma ; sh:datatype xsd:string ; sh:minCount 1 ] .

vss:StativeShape a sh:NodeShape ;
    sh:targetClass vso:Stative ;
    sh:property [ sh:path vso:lemma ; sh:datatype xsd:string ; sh:minCount 1 ] ;
    # Same entailed-range guard as EventShape/agent above.
    sh:property [ sh:path vso:experiencer ; sh:maxCount 1 ; sh:class vso:Endurant ;
                  sh:not [ sh:class vso:Perdurant ] ;
                  sh:not [ sh:class vso:Frame ] ;
                  sh:message "vso:experiencer must reference an Endurant, not a Perdurant or a Frame." ] .

#################################################################
# Quality — must have dimension and value, exactly once each
#################################################################

# No sh:in on vso:dimension, deliberately — and this is the one place a reader
# will look for it. The VSO dimension registry (ontology/vso.ttl, docs/vson.md
# §5.5) is closed only *within the VSO namespace*: a Quality MAY carry a
# dimension IRI minted under the document's own namespace, and shipped v1.x
# documents did exactly that (examples/throne_room.ttl declared :Layout,
# :Focal, and :ActionState locally before the registry absorbed them). An
# sh:in listing the registry members would reject those documents, which §8
# forbids — v1.x MUST NOT change a shape so that a previously-conformant
# document stops conforming.
#
# What enforces the closed half of the policy instead:
#   - owl:AllDifferent over every registered dimension in ontology/vso.ttl, so
#     two dimensions on one Quality clash detectably in the owl-consistency
#     gate rather than sameAs-collapsing (vso:dimension is functional);
#   - the C2 coverage test in tests/test_shapes_gate.py, which fails if any
#     corpus document asserts a VSO-namespace IRI the ontology never declared.
# Clause C2 (docs/vson.md §2) already says validate-time SHACL does not
# establish C2; this comment records why that gap is by design here.

vss:QualityShape a sh:NodeShape ;
    sh:targetClass vso:Quality ;
    sh:property [
        sh:path vso:dimension ;
        sh:minCount 1 ;
        sh:maxCount 1
    ] ;
    sh:property [
        sh:path vso:value ;
        sh:minCount 1 ;
        sh:maxCount 1
    ] .

# vso:modifier — optional adverbial bareword on a Quality; at most one.
# (Referenced by vso:modifier's rdfs:comment; strict = Violation.)
vss:QualityModifierShape a sh:NodeShape ;
    sh:targetSubjectsOf vso:modifier ;
    sh:property [
        sh:path vso:modifier ;
        sh:datatype xsd:string ;
        sh:maxCount 1 ;
        sh:message "A Quality carries at most one vso:modifier (optional adverbial bareword)."
    ] .

#################################################################
# SpatialFact — must have figure + ground; AT LEAST ONE of rcc/directional/proximal
# Directional facts MUST carry a viewer (Talmy resolution).
#################################################################

vss:SpatialFactShape a sh:NodeShape ;
    sh:targetClass vso:SpatialFact ;
    sh:property [
        sh:path vso:figure ; sh:minCount 1 ; sh:maxCount 1 ;
        sh:nodeKind sh:BlankNodeOrIRI
    ] ;
    sh:property [
        sh:path vso:ground ; sh:minCount 1 ; sh:maxCount 1 ;
        sh:nodeKind sh:BlankNodeOrIRI
    ] ;
    sh:or (
        [ sh:property [ sh:path vso:rcc ;         sh:minCount 1 ] ]
        [ sh:property [ sh:path vso:directional ; sh:minCount 1 ] ]
        [ sh:property [ sh:path vso:proximal ;    sh:minCount 1 ] ]
    ) ;
    sh:message "SpatialFact must specify at least one of vso:rcc, vso:directional, or vso:proximal." .

# Talmy resolution: directional ⇒ viewer required.
vss:DirectionalNeedsViewerShape a sh:NodeShape ;
    sh:targetSubjectsOf vso:directional ;
    sh:property [
        sh:path vso:viewer ;
        sh:minCount 1 ;
        sh:class vso:CameraView ;
        # sh:class vso:CameraView is entailed (vso:viewer rdfs:range
        # vso:CameraView). CameraView is a Frame, and Frame owl:disjointWith
        # Entity, so a viewer pointed at a depicted object fires here.
        sh:not [ sh:class vso:Entity ] ;
        sh:message "Directional spatial facts require a vso:viewer for construal disambiguation, and that viewer must be a CameraView, never an Entity."
    ] .

# rcc value must be one of the eight RCC-8 base relations.
vss:RccValueShape a sh:NodeShape ;
    sh:targetSubjectsOf vso:rcc ;
    sh:property [
        sh:path vso:rcc ;
        sh:in ( rcc:DC rcc:EC rcc:PO rcc:EQ rcc:TPP rcc:NTPP rcc:TPPi rcc:NTPPi ) ;
        sh:message "vso:rcc value must be one of the eight RCC-8 relations."
    ] .

# directional/proximal values are closed vocabularies too (parity with RCC).
vss:DirectionalValueShape a sh:NodeShape ;
    sh:targetSubjectsOf vso:directional ;
    sh:property [
        sh:path vso:directional ;
        sh:in ( vso:above vso:below vso:left_of vso:right_of vso:in_front_of vso:behind ) ;
        sh:message "vso:directional value must be one of the six frame-relative directions."
    ] .

vss:ProximalValueShape a sh:NodeShape ;
    sh:targetSubjectsOf vso:proximal ;
    sh:property [
        sh:path vso:proximal ;
        sh:in ( vso:near vso:far vso:adjacent vso:next_to vso:facing ) ;
        sh:message "vso:proximal value must be one of the five proximity relations."
    ] .

#################################################################
# Trait properties — values must be from the published vocab
#################################################################

vss:IndividuationShape a sh:NodeShape ;
    sh:targetSubjectsOf vso:individuation ;
    sh:property [
        sh:path vso:individuation ;
        sh:in ( vso:Generic vso:Named vso:Kind vso:Skolem ) ;
        sh:maxCount 1
    ] .

vss:AnimacyShape a sh:NodeShape ;
    sh:targetSubjectsOf vso:animacy ;
    sh:property [
        sh:path vso:animacy ;
        sh:in ( vso:Agentive vso:Inert ) ;
        sh:maxCount 1
    ] .

vss:CountabilityShape a sh:NodeShape ;
    sh:targetSubjectsOf vso:countability ;
    sh:property [
        sh:path vso:countability ;
        sh:in ( vso:Count vso:Mass vso:Collective ) ;
        sh:maxCount 1
    ] .

# Affordance is the fourth trait axis of the trait bundle documented at
# docs/vson.md §3.2 (individuation × animacy × countability × affordance) and
# §5.4. Unlike its three siblings it is NOT functional —
# an Endurant may offer several affordances — so this shape pins the closed
# vocabulary without a sh:maxCount.
vss:AffordanceShape a sh:NodeShape ;
    sh:targetSubjectsOf vso:affordance ;
    sh:property [
        sh:path vso:affordance ;
        sh:in ( vso:Holdable vso:Wearable vso:Mountable vso:Container vso:Edible ) ;
        sh:message "vso:affordance value must be one of the five published affordances (Holdable|Wearable|Mountable|Container|Edible)."
    ] .

#################################################################
# Frame layer — Frame is disjoint from Entity (no Frame may be depicted)
#################################################################

vss:FrameNotDepictedShape a sh:NodeShape ;
    sh:targetObjectsOf vso:depicts ;
    sh:not [ sh:class vso:Frame ] ;
    sh:message "vso:depicts must point to an Entity, not a Frame. Use vso:framedBy for the perspectival layer." .

# Discipline: vso:depicts is for the depicted world (Entities). Reification
# nodes (SpatialFact/Negation/BeliefState/Quantification/Annotation) attach via
# vso:hasFact. Warning (not Violation) so legacy documents that depict a
# reification node still conform while the convention is migrated.
vss:DepictsEntityShape a sh:NodeShape ;
    sh:targetObjectsOf vso:depicts ;
    sh:class vso:Entity ;
    sh:severity sh:Warning ;
    sh:message "vso:depicts SHOULD point to an Entity; attach reification nodes (SpatialFact/Negation/BeliefState/Quantification/Annotation) via vso:hasFact." .

#################################################################
# Quality bearer must be an Entity or a Composition (a QualityBearer)
#################################################################

# NB: do NOT use `sh:class vso:QualityBearer` here. QualityBearer is the
# rdfs:domain of vso:hasQuality, so under inference="rdfs" every subject of
# hasQuality is entailed into QualityBearer — the check would be vacuous and
# never fire. Asserting the concrete subclasses (Entity / Composition) instead
# is real: rdfs entailment never derives a subclass from a superclass, so a
# non-Composition Frame bearing a quality genuinely fails this shape.
vss:HasQualityShape a sh:NodeShape ;
    sh:targetSubjectsOf vso:hasQuality ;
    sh:or ( [ sh:class vso:Entity ] [ sh:class vso:Composition ] ) ;
    sh:message "Only a QualityBearer (an Entity, or a Composition bearing compositional qualities) may carry vso:hasQuality." .

#################################################################
# Negation — must reference a quoted triple OR Annotation reification
#################################################################

vss:NegationShape a sh:NodeShape ;
    sh:targetClass vso:Negation ;
    sh:property [
        sh:path vso:negatedStatement ;
        sh:minCount 1 ;
        sh:maxCount 1
    ] .

#################################################################
# BeliefState — must have experiencer (Endurant) and proposition
#################################################################

vss:BeliefStateShape a sh:NodeShape ;
    sh:targetClass vso:BeliefState ;
    sh:property [
        sh:path vso:experiencer ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:class vso:Endurant ;
        # Entailed-range guard — see the NB at the top of this file.
        sh:not [ sh:class vso:Perdurant ] ;
        sh:not [ sh:class vso:Frame ] ;
        sh:message "BeliefState must have exactly one experiencer, and it must be an Endurant — not a Perdurant or a Frame."
    ] ;
    sh:property [
        sh:path vso:proposition ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:message "BeliefState must reference exactly one proposition (quoted triple or Annotation node)."
    ] .

#################################################################
# Quantification — must have quantifier, variable, scope
#################################################################

vss:QuantificationShape a sh:NodeShape ;
    sh:targetClass vso:Quantification ;
    sh:property [
        sh:path vso:quantifier ;
        sh:datatype xsd:string ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:in ( "all" "every" "some" "no" "most" "few" ) ;
        sh:message "Quantification must have exactly one quantifier from the closed list (all|every|some|no|most|few)."
    ] ;
    sh:property [
        sh:path vso:variable ;
        sh:datatype xsd:string ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:message "Quantification must declare exactly one bound variable name."
    ] ;
    sh:property [
        sh:path vso:scope ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:message "Quantification must declare exactly one scope proposition."
    ] .

#################################################################
# Annotation — reified RDF-star triple (RDF 1.1 portable form, spec §18)
#################################################################

vss:AnnotationShape a sh:NodeShape ;
    sh:targetClass vso:Annotation ;
    sh:property [
        sh:path vso:annotatedSubject ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:message "Annotation must declare exactly one annotatedSubject."
    ] ;
    sh:property [
        sh:path vso:annotatedPredicate ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:message "Annotation must declare exactly one annotatedPredicate."
    ] ;
    sh:property [
        sh:path vso:annotatedObject ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:message "Annotation must declare exactly one annotatedObject."
    ] .

#################################################################
# Persona (v1.1) — must declare at least one hasInvariant Quality
#################################################################

vss:PersonaShape a sh:NodeShape ;
    sh:targetClass vso:Persona ;
    sh:property [
        sh:path vso:hasInvariant ;
        sh:minCount 1 ;
        sh:class vso:Quality ;
        # sh:class vso:Quality is entailed (vso:hasInvariant rdfs:range
        # vso:Quality). NB the guard here must NOT name vso:Entity: Quality is
        # itself rdfs:subClassOf vso:Entity, so an Entity guard would fire on
        # every well-formed Persona. Quality's actual disjoint siblings are
        # Endurant / Perdurant / Region (ontology/vso.ttl §1), plus the Frame
        # layer.
        sh:not [ sh:class vso:Endurant ] ;
        sh:not [ sh:class vso:Perdurant ] ;
        sh:not [ sh:class vso:Region ] ;
        sh:not [ sh:class vso:Frame ] ;
        sh:message "Persona must declare at least one hasInvariant Quality; an invariant may not be an Endurant, Perdurant, Region, or Frame."
    ] .

# vss:EmbodimentConsistencyShape — Entity's hasQuality on dimension D should not
# contradict embodied Persona's hasInvariant on the same dimension D.
# Severity: Warning (extractor noise expected; not strict-conform fatal).
vss:EmbodimentConsistencyShape a sh:NodeShape ;
    sh:targetSubjectsOf vso:embodies ;
    sh:severity sh:Warning ;
    sh:sparql [
        sh:select """
            PREFIX vso: <https://w3id.org/vson/v1/ontology#>
            SELECT $this ?dim ?invariantValue ?embodiedValue WHERE {
                $this vso:embodies ?persona .
                ?persona vso:hasInvariant ?invQ .
                ?invQ vso:dimension ?dim ;
                      vso:value     ?invariantValue .
                $this vso:hasQuality ?embQ .
                ?embQ vso:dimension ?dim ;
                      vso:value     ?embodiedValue .
                FILTER (?invariantValue != ?embodiedValue)
            }
        """ ;
        sh:message "Entity hasQuality value contradicts embodied Persona's hasInvariant on the same dimension."
    ] .
