################################################################################
# MiKaDo — SHACL shapes (mko:)
#
# Structural invariants for a MiKaDo export. Complements the TBox
# (what may be said) with what must be said.
#
# sh:Violation — structural invariant
# sh:Warning   — quality heuristic
################################################################################


@prefix mko:    <https://w3id.org/durair/ontology/refoundation#> .
@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 owl:    <http://www.w3.org/2002/07/owl#> .
@prefix xsd:    <http://www.w3.org/2001/XMLSchema#> .
@prefix dct:    <http://purl.org/dc/terms/> .
@prefix foaf:   <http://xmlns.com/foaf/0.1/> .
@prefix prov:   <http://www.w3.org/ns/prov#> .
@prefix skos:   <http://www.w3.org/2004/02/skos/core#> .
@prefix schema: <https://schema.org/> .
@prefix wgs84:  <http://www.w3.org/2003/01/geo/wgs84_pos#> .

@base <https://w3id.org/durair/ontology/refoundation/shapes> .

<#> a owl:Ontology ;
    dct:title    "MiKaDo — SHACL Shapes"@en ;
    dct:description "Validatable structural invariants for a MiKaDo export."@en ;
    dct:creator  <https://orcid.org/0000-0001-7600-2273> ,
                 <https://orcid.org/0000-0003-3724-4829> ,
                 <https://orcid.org/0009-0001-3313-0587> ;
    dct:publisher [ a foaf:Organization ;
                    foaf:name "MAP — UPR 2002 CNRS" ;
                    foaf:homepage <https://www.map.cnrs.fr/> ] ;
    dct:rightsHolder "CNRS" ;
    schema:funder [ a schema:Organization ;
                    schema:name "European Research Council" ;
                    schema:alternateName "n-Dame_Heritage" ;
                    schema:identifier "101055423" ] ;
    rdfs:seeAlso <https://w3id.org/durair/ontology/refoundation> ,
                 <https://mikado.map.cnrs.fr/> ;
    owl:versionInfo "1.0.1" .

<https://orcid.org/0000-0001-7600-2273> a foaf:Person ;
    foaf:name "Livio De Luca" ;
    foaf:mbox <mailto:livio.deluca@map.cnrs.fr> .

<https://orcid.org/0000-0003-3724-4829> a foaf:Person ;
    foaf:name "Anthony Pamart" .

<https://orcid.org/0009-0001-3313-0587> a foaf:Person ;
    foaf:name "Quentin Vogel" .

################################################################################
# 1. Activity — mandatory agency and scale
################################################################################

mko:ActivityShape a sh:NodeShape ;
    sh:targetClass mko:Activity ;
    sh:property [
        sh:path mko:agentKind ;
        sh:minCount 1 ; sh:maxCount 1 ;
        sh:datatype xsd:string ;
        sh:in ( "human" "automatic" "mixed" ) ;
        sh:severity sh:Violation ;
        sh:message "Every mko:Activity must carry mko:agentKind ∈ {human, automatic, mixed}." ;
    ] ;
    sh:property [
        sh:path mko:agentRole ;
        sh:minCount 1 ; sh:maxCount 1 ;
        sh:datatype xsd:string ;
        sh:in ( "acquisition" "interpretation" "derivation" "curation" ) ;
        sh:severity sh:Violation ;
        sh:message "Every mko:Activity must carry mko:agentRole ∈ {acquisition, interpretation, derivation, curation}." ;
    ] ;
    sh:property [
        sh:path mko:scale ;
        sh:minCount 1 ; sh:maxCount 1 ;
        sh:datatype xsd:string ;
        sh:in ( "macro" "meso" "micro" ) ;
        sh:severity sh:Violation ;
        sh:message "Every mko:Activity must carry mko:scale ∈ {macro, meso, micro}." ;
    ] ;
    sh:property [
        sh:path rdfs:label ;
        sh:minCount 1 ;
        sh:severity sh:Warning ;
        sh:message "An mko:Activity without rdfs:label is usable but hard to read — at least one label is recommended." ;
    ] .

# 1.1 MicroActivity — time and session membership

mko:MicroActivityShape a sh:NodeShape ;
    sh:targetClass mko:MicroActivity ;
    sh:property [
        sh:path mko:scale ;
        sh:hasValue "micro" ;
        sh:severity sh:Violation ;
        sh:message "An mko:MicroActivity must carry mko:scale = \"micro\"." ;
    ] ;
    sh:property [
        sh:path mko:performedAt ;
        sh:minCount 1 ; sh:maxCount 1 ;
        sh:datatype xsd:dateTime ;
        sh:severity sh:Violation ;
        sh:message "An mko:MicroActivity must carry exactly one mko:performedAt (xsd:dateTime)." ;
    ] ;
    sh:property [
        sh:path mko:partOf ;
        sh:minCount 1 ; sh:maxCount 1 ;
        sh:nodeKind sh:IRI ;
        sh:severity sh:Violation ;
        sh:message "An mko:MicroActivity must belong (mko:partOf) to exactly one parent activity (typically a MesoActivity)." ;
    ] .

# 1.2 MesoActivity — programme membership

mko:MesoActivityShape a sh:NodeShape ;
    sh:targetClass mko:MesoActivity ;
    sh:property [
        sh:path mko:scale ;
        sh:hasValue "meso" ;
        sh:severity sh:Violation ;
        sh:message "An mko:MesoActivity must carry mko:scale = \"meso\"." ;
    ] ;
    sh:property [
        sh:path mko:partOf ;
        sh:minCount 1 ; sh:maxCount 1 ;
        sh:severity sh:Violation ;
        sh:message "An mko:MesoActivity must belong (mko:partOf) to exactly one mko:MacroActivity." ;
    ] ;
    sh:property [
        sh:path mko:firstStep ;
        sh:maxCount 1 ;
        sh:severity sh:Warning ;
        sh:message "A MesoActivity may carry mko:firstStep. If omitted, order remains recoverable from performedAt." ;
    ] ;
    sh:property [
        sh:path mko:lastStep ;
        sh:maxCount 1 ;
    ] .

# 1.3 MacroActivity — root, no partOf

mko:MacroActivityShape a sh:NodeShape ;
    sh:targetClass mko:MacroActivity ;
    sh:property [
        sh:path mko:scale ;
        sh:hasValue "macro" ;
        sh:severity sh:Violation ;
        sh:message "An mko:MacroActivity must carry mko:scale = \"macro\"." ;
    ] ;
    sh:property [
        sh:path mko:partOf ;
        sh:maxCount 0 ;
        sh:severity sh:Violation ;
        sh:message "An mko:MacroActivity is the activity-graph root and must not carry mko:partOf." ;
    ] .

################################################################################
# 2. Domain entities
################################################################################

mko:MaterialEntityShape a sh:NodeShape ;
    sh:targetClass mko:MaterialEntity ;
    sh:property [
        sh:path mko:domain ;
        sh:minCount 1 ; sh:maxCount 1 ;
        sh:hasValue "material" ;
        sh:severity sh:Violation ;
        sh:message "An mko:MaterialEntity must carry mko:domain = \"material\"." ;
    ] ;
    sh:property [
        sh:path mko:scale ;
        sh:datatype xsd:string ;
        sh:in ( "macro" "meso" "micro" ) ;
        sh:severity sh:Warning ;
        sh:message "An mko:MaterialEntity should carry mko:scale (typically \"macro\" for a site)." ;
    ] .

mko:DigitalEntityShape a sh:NodeShape ;
    sh:targetClass mko:DigitalEntity ;
    sh:property [
        sh:path mko:domain ;
        sh:minCount 1 ; sh:maxCount 1 ;
        sh:hasValue "digital" ;
        sh:severity sh:Violation ;
        sh:message "An mko:DigitalEntity must carry mko:domain = \"digital\"." ;
    ] .

mko:KnowledgeEntityShape a sh:NodeShape ;
    sh:targetClass mko:KnowledgeEntity ;
    sh:property [
        sh:path mko:domain ;
        sh:minCount 1 ; sh:maxCount 1 ;
        sh:hasValue "knowledge" ;
        sh:severity sh:Violation ;
        sh:message "An mko:KnowledgeEntity must carry mko:domain = \"knowledge\"." ;
    ] .

################################################################################
# 3. ObservedFeature — three-domain anchors
#
# Central invariant: every observed fact is anchored to a material object,
# represented by at least one digital entity, and usually interpreted by
# one or more knowledge entities.
################################################################################

mko:ObservedFeatureShape a sh:NodeShape ;
    sh:targetClass mko:ObservedFeature ;
    sh:property [
        sh:path mko:featureKind ;
        sh:minCount 1 ; sh:maxCount 1 ;
        sh:datatype xsd:string ;
        sh:in (
            "region" "measurement" "voice" "frame"
            "orthophoto" "video" "screenshot" "gcp"
        ) ;
        sh:severity sh:Violation ;
        sh:message "An mko:ObservedFeature must carry mko:featureKind from FeatureKindScheme." ;
    ] ;
    sh:property [
        sh:path mko:isFeatureOf ;
        sh:minCount 1 ; sh:maxCount 1 ;
        sh:class mko:MaterialEntity ;
        sh:severity sh:Violation ;
        sh:message "An mko:ObservedFeature must carry exactly one mko:isFeatureOf to a MaterialEntity." ;
    ] ;
    sh:property [
        sh:path mko:representedBy ;
        sh:minCount 1 ;
        sh:class mko:DigitalEntity ;
        sh:severity sh:Violation ;
        sh:message "An mko:ObservedFeature must carry at least one mko:representedBy to a DigitalEntity." ;
    ] ;
    sh:property [
        sh:path mko:interpretedBy ;
        sh:class mko:KnowledgeEntity ;
        sh:minCount 1 ;
        sh:severity sh:Warning ;
        sh:message "An ObservedFeature should carry at least one interpretedBy. Standalone frames without semantic annotation are tolerated." ;
    ] ;
    sh:property [
        sh:path mko:spatialAcquisitionContext ;
        sh:maxCount 1 ;
        sh:datatype xsd:string ;
        sh:in ( "inSitu" "exSitu" "unknown" ) ;
        sh:severity sh:Violation ;
        sh:message "If set, spatialAcquisitionContext must be ∈ {inSitu, exSitu, unknown}." ;
    ] ;
    sh:property [
        sh:path prov:wasGeneratedBy ;
        sh:minCount 1 ;
        sh:nodeKind sh:IRI ;
        sh:severity sh:Violation ;
        sh:message "An ObservedFeature must be produced by at least one Activity." ;
    ] .

################################################################################
# 4. DerivedFeature — computed fact, explicit lineage, no isFeatureOf
#
# A DerivedFeature is not a characteristic of the material object;
# it is a computed property of its source. Three grains; four SHACL branches:
#   gesture descriptors — derivedFrom + prov:wasGeneratedBy
#   similarity buckets  — bucketFamily
#   activity syntheses  — hasDerivationInput + derivationMethod
#   derivation-trace    — the method node itself (derivationMethod only)
# Material anchoring is transitive (ObservedFeature → isFeatureOf, or
# via the named activity / snapshot that hasDerivationInput points to).
# narrative-digest remains reserved and is not in the closed list.
#
################################################################################

mko:DerivedFeatureShape a sh:NodeShape ;
    sh:targetClass mko:DerivedFeature ;
    sh:property [
        sh:path mko:featureKind ;
        sh:minCount 1 ; sh:maxCount 1 ;
        sh:datatype xsd:string ;
        sh:in (
            "voice-spatial" "measurement-dimensional"
            "region-geometric" "region-visual"
            "spatial-bucket" "morphology-bucket" "appearance-bucket" "semantic-bucket"
            "statistical-profile" "coverage-matrix" "coverage-dimension"
            "spatial-coverage" "comparative-report" "comparison-item"
            "derivation-trace"
        ) ;
        sh:severity sh:Violation ;
        sh:message "A DerivedFeature must carry mko:featureKind from the controlled vocabulary (gesture descriptors, four bucket families, or activity-grain syntheses)." ;
    ] ;
    sh:or (
        [
            sh:property [
                sh:path mko:derivedFrom ;
                sh:minCount 1 ;
                sh:nodeKind sh:IRI ;
                sh:message "Lineage is required (ObservedFeature, optionally DigitalEntity) for gesture-synchronous DerivedFeatures.";
            ] ;
            sh:property [
                sh:path prov:wasGeneratedBy ;
                sh:minCount 1 ;
                sh:nodeKind sh:IRI ;
                sh:message "A gesture-synchronous DerivedFeature must be generated by the micro-activity of the gesture." ;
            ] ;
        ]
        [
            sh:property [
                sh:path mko:bucketFamily ;
                sh:minCount 1 ;
                sh:datatype xsd:string ;
                sh:in ( "spatial" "morphology" "appearance" "semantic" ) ;
                sh:message "A similarity bucket must carry mko:bucketFamily ∈ {spatial, morphology, appearance, semantic}." ;
            ]
        ]
        [
            sh:property [
                sh:path mko:hasDerivationInput ;
                sh:minCount 1 ;
                sh:nodeKind sh:IRI ;
                sh:message "An activity-grain synthesis must name the activity, GraphSnapshot or parent DerivedFeature it reads (hasDerivationInput)." ;
            ] ;
            sh:property [
                sh:path mko:derivationMethod ;
                sh:minCount 1 ;
                sh:datatype xsd:string ;
                sh:message "An activity-grain synthesis must carry mko:derivationMethod." ;
            ]
        ]
        [
            sh:property [
                sh:path mko:featureKind ;
                sh:hasValue "derivation-trace" ;
                sh:message "A derivation-trace node is the method itself and carries no hasDerivationInput." ;
            ] ;
            sh:property [
                sh:path mko:derivationMethod ;
                sh:minCount 1 ;
                sh:datatype xsd:string ;
                sh:message "A derivation-trace must carry mko:derivationMethod." ;
            ]
        ]
    ) ;
    sh:property [
        sh:path mko:isFeatureOf ;
        sh:maxCount 0 ;
        sh:severity sh:Violation ;
        sh:message "A DerivedFeature must not carry isFeatureOf. Material anchoring is transitive." ;
    ] .

################################################################################
# 5. FieldValue
################################################################################

mko:FieldValueShape a sh:NodeShape ;
    sh:targetClass mko:FieldValue ;
    sh:property [
        sh:path schema:propertyID ;
        sh:minCount 1 ; sh:maxCount 1 ;
        sh:datatype xsd:string ;
        sh:severity sh:Violation ;
        sh:message "A FieldValue must carry exactly one schema:propertyID." ;
    ] ;
    sh:property [
        sh:path schema:value ;
        sh:minCount 1 ;
        sh:severity sh:Violation ;
        sh:message "A FieldValue must carry at least one schema:value." ;
    ] ;
    sh:property [
        sh:path mko:fieldType ;
        sh:minCount 1 ; sh:maxCount 1 ;
        sh:datatype xsd:string ;
        sh:in ( "text" "textarea" "select" "multiselect" "number" "date" "boolean" "url" ) ;
        sh:severity sh:Violation ;
        sh:message "A FieldValue must carry mko:fieldType." ;
    ] .

################################################################################
# 6. GraphSnapshot
################################################################################

mko:GraphSnapshotShape a sh:NodeShape ;
    sh:targetClass mko:GraphSnapshot ;
    sh:property [
        sh:path mko:exportProfile ;
        sh:minCount 1 ; sh:maxCount 1 ;
        sh:datatype xsd:string ;
        sh:severity sh:Violation ;
        sh:message "A GraphSnapshot must identify its export profile with mko:exportProfile." ;
    ] ;
    sh:property [
        sh:path mko:schemaVersion ;
        sh:minCount 1 ; sh:maxCount 1 ;
        sh:datatype xsd:integer ;
        sh:minInclusive 1 ;
        sh:severity sh:Violation ;
        sh:message "A GraphSnapshot must carry mko:schemaVersion (integer ≥ 1)." ;
    ] ;
    sh:property [
        sh:path mko:describesProject ;
        sh:minCount 1 ; sh:maxCount 1 ;
        sh:nodeKind sh:IRI ;
        sh:severity sh:Violation ;
        sh:message "A GraphSnapshot must describe exactly one MacroActivity." ;
    ] .

################################################################################
# 7. Place — paired coordinates and parent chain
################################################################################

mko:PlaceShape a sh:NodeShape ;
    sh:targetClass schema:Place ;
    sh:property [
        sh:path schema:containedInPlace ;
        sh:maxCount 1 ;
        sh:nodeKind sh:IRI ;
        sh:severity sh:Violation ;
        sh:message "A schema:Place may have at most one direct parent Place." ;
    ] ;
    # If one of lat/long is set, the other must be set too.
    sh:or (
        [
            sh:path wgs84:lat ; sh:maxCount 0 ;
        ]
        [
            sh:and (
                [ sh:path wgs84:lat  ; sh:minCount 1 ; sh:maxCount 1 ; sh:datatype xsd:float ]
                [ sh:path wgs84:long ; sh:minCount 1 ; sh:maxCount 1 ; sh:datatype xsd:float ]
            )
        ]
    ) ;
    sh:severity sh:Violation ;
    sh:message "A Place has either no WGS84 coordinates or both lat and long." .

################################################################################
# 8. Quality heuristics (Warning, not blocking)
################################################################################

# 8.1 Orphan documentation
# Warning severity is on the NodeShape so pyshacl ≥ 0.20 honours it.
#
mko:DocumentationLinkedShape a sh:NodeShape ;
    sh:targetClass mko:Documentation ;
    sh:severity sh:Warning ;
    sh:sparql [
        a sh:SPARQLConstraint ;
        sh:message "Orphan documentation: no Activity points here via a documents* property." ;
        sh:prefixes <#> ;
        sh:select """
            PREFIX mko: <https://w3id.org/durair/ontology/refoundation#>
            SELECT $this WHERE {
                FILTER NOT EXISTS {
                    ?activity ?p $this .
                    FILTER (?p IN (
                        mko:documentsWho, mko:documentsWhat, mko:documentsWhy,
                        mko:documentsWhen, mko:documentsWhere, mko:documentsHow,
                        mko:documentsWhich
                    ))
                }
            }
        """ ;
    ] .

# Prefix used by the SPARQL constraint above
<#> sh:declare [
    sh:prefix "mko" ;
    sh:namespace "https://w3id.org/durair/ontology/refoundation#"^^xsd:anyURI ;
] .
