@prefix sh:     <http://www.w3.org/ns/shacl#> .
@prefix rdf:    <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix hon:    <https://id.honoria.co.uk/def/honours/> .
@prefix hons:   <https://id.honoria.co.uk/def/honours-shapes/> .
@prefix skos:   <http://www.w3.org/2004/02/skos/core#> .
@prefix rdfs:   <http://www.w3.org/2000/01/rdf-schema#> .
@prefix dct:    <http://purl.org/dc/terms/> .
@prefix owl:    <http://www.w3.org/2002/07/owl#> .
@prefix xsd:    <http://www.w3.org/2001/XMLSchema#> .

#################################################################
# SHACL shapes for the Honours vocabulary.
#
# SHACL VALIDATES data; SKOS (honours.ttl) DEFINES the vocabulary.
# This is the closed-world quality gate: unlike OWL, SHACL flags
# MISSING and MALFORMED data. Run with honours.ttl as the data graph
# (warnings are expected for concepts with no Wikidata item yet):
#   pyshacl --allow-warnings -s honours-shapes.ttl honours.ttl
#
# Severity maps to the Honoria confidence vocabulary:
#   sh:Violation -> blocks (data is wrong)
#   sh:Warning   -> 'machine' / unverified, allowed but flagged
#################################################################

<https://id.honoria.co.uk/def/honours-shapes>
    a owl:Ontology ;
    dct:title       "Honours shapes"@en ;
    dct:description "SHACL validation shapes for the Honoria honours vocabulary - the well-formedness rules every award-type concept must satisfy (a language-tagged preferred label and scheme membership at violation severity; a definition and an outward Wikidata mapping at warning severity)."@en ;
    rdfs:seeAlso    <https://id.honoria.co.uk/def/honours> ;
    dct:modified    "2026-07-27"^^xsd:date ;
    dct:license     <https://creativecommons.org/licenses/by/4.0/> .

#---------------------------------------------------------------
# Every award-type concept in the scheme must be well-formed.
#---------------------------------------------------------------
hons:ConceptShape
    a sh:NodeShape ;
    sh:targetClass skos:Concept ;
    rdfs:label "Honoria award-type concept shape"@en ;

    # Exactly one preferred label (a language-tagged string).
    sh:property [
        sh:path        skos:prefLabel ;
        sh:minCount    1 ;
        sh:maxCount    1 ;
        sh:datatype    rdf:langString ;
        sh:message     "A concept must have exactly one language-tagged skos:prefLabel." ;
        sh:severity    sh:Violation ;
    ] ;

    # Must belong to the Honoria honours scheme.
    sh:property [
        sh:path        skos:inScheme ;
        sh:minCount    1 ;
        sh:hasValue    <https://id.honoria.co.uk/def/honours> ;
        sh:message     "A concept must be skos:inScheme <https://id.honoria.co.uk/def/honours>." ;
        sh:severity    sh:Violation ;
    ] ;

    # Should carry a human-readable definition (warn-only; tracks coverage).
    sh:property [
        sh:path        skos:definition ;
        sh:minCount    1 ;
        sh:datatype    rdf:langString ;
        sh:message     "A concept should have a skos:definition." ;
        sh:severity    sh:Warning ;
    ] ;

    # A closeMatch is a low-confidence external alignment - it must point
    # at either a Gazette honours.owl class or a Wikidata entity (e.g. a
    # monarch-named alias). Anything else is malformed.
    sh:property [
        sh:path        skos:closeMatch ;
        sh:nodeKind    sh:IRI ;
        sh:pattern     "^https://www\\.thegazette\\.co\\.uk/def/honours#|^http://www\\.wikidata\\.org/entity/Q[0-9]+$" ;
        sh:message     "skos:closeMatch must target a Gazette honours.owl class or a Wikidata entity." ;
        sh:severity    sh:Warning ;
    ] .

#---------------------------------------------------------------
# Each concept should map to Wikidata - by a skos:exactMatch (preferred),
# or at least a Wikidata skos:closeMatch (e.g. the Queen's-reign variant,
# whose name is the single Wikidata item's alias). Concepts with neither
# are flagged (warn-only) as a coverage gap.
#---------------------------------------------------------------
hons:WikidataMappingShape
    a sh:NodeShape ;
    sh:targetClass skos:Concept ;
    sh:severity sh:Warning ;
    sh:message "A concept should map to Wikidata: a skos:exactMatch (preferred), or at least a Wikidata skos:closeMatch." ;
    sh:or (
        [ sh:path     skos:exactMatch ;
          sh:minCount 1 ;
          sh:nodeKind sh:IRI ;
          sh:pattern  "^http://www\\.wikidata\\.org/entity/Q[0-9]+$" ]
        [ sh:path     skos:closeMatch ;
          sh:qualifiedValueShape [
              sh:nodeKind sh:IRI ;
              sh:pattern  "^http://www\\.wikidata\\.org/entity/Q[0-9]+$" ] ;
          sh:qualifiedMinCount 1 ]
    ) .

#---------------------------------------------------------------
# Honour-record template (for the read model). Any node that asserts an
# award type via hon:hasAwardType must point at exactly one concept that
# is in the scheme. Aligns to honours.json award_id. Enable by typing
# records hon:Honour (or switch to sh:targetSubjectsOf hon:hasAwardType).
#---------------------------------------------------------------
hons:HonourShape
    a sh:NodeShape ;
    sh:targetClass hon:Honour ;
    rdfs:label "Honour record shape (template)"@en ;
    sh:property [
        sh:path        hon:hasAwardType ;
        sh:minCount    1 ;
        sh:maxCount    1 ;
        sh:class       skos:Concept ;
        sh:message     "An Honour must reference exactly one award-type concept from the Honoria honours scheme." ;
        sh:severity    sh:Violation ;
    ] .
