# AFMRO — SHACL shapes for the CLEAN ontology
# Workshop: AI-Assisted Ontology Engineering — KGC 2026
# Author: Dougal Watt, Graph Research Labs
#
# These shapes correspond to afmro-clean.ttl (gist 14.1.0 aligned).

@prefix afmro: <https://example.org/afmro/> .
@prefix gist:  <https://w3id.org/semanticarts/ns/ontology/gist/> .
@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 skos:  <http://www.w3.org/2004/02/skos/core#> .
@prefix xsd:   <http://www.w3.org/2001/XMLSchema#> .

#####################################################################
# Aircraft NodeShape
#####################################################################

afmro:AircraftShape a sh:NodeShape ;
  sh:targetClass afmro:Aircraft ;

  sh:property [
    sh:path gist:isCategorizedBy ;
    sh:minCount 1 ;
    sh:maxCount 1 ;
    sh:class afmro:AircraftCategory ;
    sh:message "An Aircraft must have exactly one AircraftCategory (its design type)."@en ;
  ] ;

  sh:property [
    sh:path afmro:operatedBy ;
    sh:minCount 1 ;
    sh:maxCount 1 ;
    sh:class afmro:Squadron ;
    sh:message "An Aircraft must be operated by exactly one Squadron."@en ;
  ] ;

  sh:property [
    sh:path afmro:stationedAt ;
    sh:minCount 1 ;
    sh:maxCount 1 ;
    sh:class afmro:Base ;
    sh:message "An Aircraft must be stationed at exactly one Base."@en ;
  ] ;

  sh:property [
    sh:path afmro:hasReadiness ;
    sh:minCount 1 ;
    sh:class afmro:ReadinessState ;
    sh:message "An Aircraft must have at least one ReadinessState."@en ;
  ] ;

  # Component check uses the inverse of gist:isDirectPartOf so it
  # validates regardless of whether inverse inference is enabled.
  sh:property [
    sh:path [ sh:inversePath gist:isDirectPartOf ] ;
    sh:minCount 1 ;
    sh:class afmro:Component ;
    sh:message "An Aircraft must have at least one Component (Component gist:isDirectPartOf Aircraft)."@en ;
  ] .

#####################################################################
# ReadinessState NodeShape
#####################################################################

afmro:ReadinessStateShape a sh:NodeShape ;
  sh:targetClass afmro:ReadinessState ;

  sh:property [
    sh:path gist:isCategorizedBy ;
    sh:minCount 1 ;
    sh:maxCount 1 ;
    sh:class afmro:ReadinessLevel ;
    sh:message "A ReadinessState must have exactly one ReadinessLevel category."@en ;
  ] ;

  sh:property [
    sh:path gist:occursIn ;
    sh:minCount 1 ;
    sh:maxCount 1 ;
    sh:class gist:TimeInterval ;
    sh:message "A ReadinessState must have a temporal extent (gist:occursIn)."@en ;
  ] .

#####################################################################
# Capability NodeShape
#####################################################################

afmro:CapabilityShape a sh:NodeShape ;
  sh:targetClass afmro:Capability ;

  sh:property [
    sh:path gist:isCategorizedBy ;
    sh:minCount 1 ;
    sh:maxCount 1 ;
    sh:class afmro:CapabilityCategory ;
    sh:message "A Capability must reference exactly one CapabilityCategory."@en ;
  ] .

#####################################################################
# Mission NodeShape — the OCCURRENCE.
# Forbidden: any planning-time property (those live on MissionPlan).
#####################################################################

afmro:MissionShape a sh:NodeShape ;
  sh:targetClass afmro:Mission ;

  sh:property [
    sh:path gist:hasParticipant ;
    sh:minCount 1 ;
    sh:class afmro:Aircraft ;
    sh:message "A Mission must have at least one participating Aircraft."@en ;
  ] ;

  sh:property [
    sh:path gist:occursIn ;
    sh:minCount 1 ;
    sh:maxCount 1 ;
    sh:class gist:TimeInterval ;
    sh:message "A Mission must have a temporal extent (gist:occursIn)."@en ;
  ] ;

  sh:property [
    sh:path gist:hasPhysicalLocation ;
    sh:maxCount 1 ;
    sh:class afmro:Base ;
    sh:message "If present, a Mission's location is single-valued."@en ;
  ] ;

  sh:property [
    sh:path afmro:implementsPlan ;
    sh:maxCount 1 ;
    sh:class afmro:MissionPlan ;
    sh:message "If present, a Mission implements at most one MissionPlan."@en ;
  ] ;

  sh:property [
    sh:path afmro:missionCallsign ;
    sh:maxCount 1 ;
    sh:datatype xsd:string ;
    sh:message "If present, a Mission has at most one callsign string."@en ;
  ] ;

  sh:property [
    sh:path afmro:missionOutcome ;
    sh:maxCount 1 ;
    sh:class afmro:MissionOutcome ;
    sh:message "If present, a Mission has at most one MissionOutcome."@en ;
  ] ;

  # Forbidden: planning-time properties on the occurrence.
  sh:property [
    sh:path afmro:requiresCapability ;
    sh:maxCount 0 ;
    sh:message "Mission must NOT carry requiresCapability — that is a planning property and belongs on MissionPlan."@en ;
  ] ;

  sh:property [
    sh:path afmro:plannedTimeWindow ;
    sh:maxCount 0 ;
    sh:message "Mission must NOT carry plannedTimeWindow — use gist:occursIn for the actual occurrence window."@en ;
  ] ;

  sh:property [
    sh:path afmro:plannedParticipantType ;
    sh:maxCount 0 ;
    sh:message "Mission must NOT carry plannedParticipantType — that is a planning property."@en ;
  ] ;

  sh:property [
    sh:path afmro:authorisedBy ;
    sh:maxCount 0 ;
    sh:message "Mission must NOT carry authorisedBy — that is a planning property."@en ;
  ] ;

  sh:property [
    sh:path afmro:planStatus ;
    sh:maxCount 0 ;
    sh:message "Mission must NOT carry planStatus — that is a planning property."@en ;
  ] .

#####################################################################
# MissionPlan NodeShape — the PLAN.
# Forbidden: any occurrence-time property.
#####################################################################

afmro:MissionPlanShape a sh:NodeShape ;
  sh:targetClass afmro:MissionPlan ;

  sh:property [
    sh:path afmro:planFor ;
    sh:minCount 1 ;
    sh:maxCount 1 ;
    sh:class afmro:Mission ;
    sh:message "A MissionPlan must reference exactly one Mission it plans."@en ;
  ] ;

  sh:property [
    sh:path afmro:requiresCapability ;
    sh:minCount 1 ;
    sh:class afmro:CapabilityCategory ;
    sh:message "A MissionPlan must require at least one CapabilityCategory."@en ;
  ] ;

  sh:property [
    sh:path afmro:plannedTimeWindow ;
    sh:minCount 1 ;
    sh:maxCount 1 ;
    sh:class gist:TimeInterval ;
    sh:message "A MissionPlan must specify exactly one planned TimeInterval."@en ;
  ] ;

  sh:property [
    sh:path afmro:plannedParticipantType ;
    sh:minCount 1 ;
    sh:class afmro:AircraftCategory ;
    sh:message "A MissionPlan must commit to at least one AircraftCategory."@en ;
  ] ;

  sh:property [
    sh:path afmro:authorisedBy ;
    sh:minCount 1 ;
    sh:maxCount 1 ;
    sh:or (
      [ sh:class gist:Person ]
      [ sh:class gist:Organization ]
    ) ;
    sh:message "A MissionPlan must be authorised by exactly one Person or Organization."@en ;
  ] ;

  sh:property [
    sh:path afmro:planStatus ;
    sh:minCount 1 ;
    sh:maxCount 1 ;
    sh:class afmro:PlanStatus ;
    sh:message "A MissionPlan must have exactly one PlanStatus."@en ;
  ] ;

  sh:property [
    sh:path afmro:plannedOperatingArea ;
    sh:maxCount 1 ;
    sh:class afmro:Base ;
    sh:message "If present, a MissionPlan's planned operating area is single-valued."@en ;
  ] ;

  sh:property [
    sh:path afmro:planRiskLevel ;
    sh:maxCount 1 ;
    sh:class afmro:RiskLevel ;
    sh:message "If present, a MissionPlan has at most one assessed RiskLevel."@en ;
  ] ;

  # Forbidden: occurrence-time properties on the plan.
  sh:property [
    sh:path gist:hasParticipant ;
    sh:maxCount 0 ;
    sh:message "MissionPlan must NOT carry gist:hasParticipant — that is occurrence-time."@en ;
  ] ;

  sh:property [
    sh:path gist:occursIn ;
    sh:maxCount 0 ;
    sh:message "MissionPlan must NOT carry gist:occursIn — use plannedTimeWindow."@en ;
  ] ;

  sh:property [
    sh:path gist:hasPhysicalLocation ;
    sh:maxCount 0 ;
    sh:message "MissionPlan must NOT carry gist:hasPhysicalLocation — use plannedOperatingArea."@en ;
  ] ;

  sh:property [
    sh:path afmro:missionCallsign ;
    sh:maxCount 0 ;
    sh:message "MissionPlan must NOT carry missionCallsign — that is occurrence-time."@en ;
  ] ;

  sh:property [
    sh:path afmro:missionOutcome ;
    sh:maxCount 0 ;
    sh:message "MissionPlan must NOT carry missionOutcome — that is set on the Mission."@en ;
  ] .

#####################################################################
# Squadron NodeShape — must operate at least one aircraft.
#####################################################################

afmro:SquadronShape a sh:NodeShape ;
  sh:targetClass afmro:Squadron ;

  sh:property [
    sh:path [ sh:inversePath afmro:operatedBy ] ;
    sh:minCount 1 ;
    sh:class afmro:Aircraft ;
    sh:message "A Squadron must operate at least one Aircraft (Aircraft afmro:operatedBy Squadron)."@en ;
  ] ;

  sh:property [
    sh:path skos:prefLabel ;
    sh:minCount 1 ;
    sh:datatype rdf:langString ;
    sh:languageIn ( "en" ) ;
    sh:message "A Squadron must have an English skos:prefLabel."@en ;
  ] .

#####################################################################
# MaintenanceEvent NodeShape
#####################################################################

afmro:MaintenanceEventShape a sh:NodeShape ;
  sh:targetClass afmro:MaintenanceEvent ;

  sh:property [
    sh:path afmro:affectsAircraft ;
    sh:minCount 1 ;
    sh:class afmro:Aircraft ;
    sh:message "A MaintenanceEvent must affect at least one Aircraft."@en ;
  ] ;

  sh:property [
    sh:path gist:occursIn ;
    sh:minCount 1 ;
    sh:maxCount 1 ;
    sh:class gist:TimeInterval ;
    sh:message "A MaintenanceEvent must have a temporal extent (gist:occursIn)."@en ;
  ] .

#####################################################################
# Annotation discipline
#####################################################################

afmro:AnnotationDisciplineShape a sh:NodeShape ;
  sh:targetSubjectsOf afmro:operatedBy , afmro:hasReadiness , afmro:hasCapability , afmro:planFor ;
  sh:property [
    sh:path skos:prefLabel ;
    sh:minCount 1 ;
    sh:message "Every domain entity must carry skos:prefLabel."@en ;
  ] .
