# Meshpocalypse SHACL Shapes
# Version: 3.0
# Description: SHACL validation shapes for the Meshpocalypse ontology
# License: MIT

@prefix mesh: <https://meshpocalypse.dev/ontology#> .
@prefix owl: <http://www.w3.org/2002/07/owl#> .
@prefix prov: <http://www.w3.org/ns/prov#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

# ============================================================
# AgentCard Shape
# ============================================================

mesh:AgentCardShape a sh:NodeShape ;
    sh:targetClass mesh:Agent ;
    rdfs:label "Agent Card Shape"@en ;
    rdfs:comment "Validation shape for agent cards"@en ;
    
    # Required: DID
    sh:property [
        sh:path mesh:did ;
        sh:datatype xsd:anyURI ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:pattern "^did:mesh:.+$" ;
        sh:message "Agent must have a valid did:mesh identifier"@en ;
    ] ;
    
    # Required: at least one capability
    sh:property [
        sh:path mesh:hasCapability ;
        sh:class mesh:Capability ;
        sh:minCount 1 ;
        sh:message "Agent must have at least one capability"@en ;
    ] ;
    
    # Required: status
    sh:property [
        sh:path mesh:status ;
        sh:datatype xsd:string ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:in ("online" "offline" "busy" "maintenance") ;
        sh:message "Agent status must be online, offline, busy, or maintenance"@en ;
    ] ;
    
    # Optional: trust level (0-4)
    sh:property [
        sh:path mesh:trustLevel ;
        sh:datatype xsd:integer ;
        sh:maxCount 1 ;
        sh:minInclusive 0 ;
        sh:maxInclusive 4 ;
        sh:defaultValue 0 ;
        sh:message "Trust level must be between 0 and 4"@en ;
    ] .

# ============================================================
# Task Shape
# ============================================================

mesh:TaskShape a sh:NodeShape ;
    sh:targetClass mesh:Task ;
    rdfs:label "Task Shape"@en ;
    rdfs:comment "Validation shape for tasks"@en ;
    
    # Required: status
    sh:property [
        sh:path mesh:status ;
        sh:datatype xsd:string ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:in ("pending" "active" "complete" "failed" "cancelled") ;
        sh:message "Task status must be pending, active, complete, failed, or cancelled"@en ;
    ] ;
    
    # Required: assignedTo
    sh:property [
        sh:path mesh:assignedTo ;
        sh:class mesh:Agent ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:message "Task must be assigned to exactly one agent"@en ;
    ] ;
    
    # Optional: dependsOn (must reference existing tasks)
    sh:property [
        sh:path mesh:dependsOn ;
        sh:class mesh:Task ;
        sh:message "Task dependencies must reference valid tasks"@en ;
    ] ;
    
    # Optional: priority (1-10)
    sh:property [
        sh:path mesh:priority ;
        sh:datatype xsd:integer ;
        sh:maxCount 1 ;
        sh:minInclusive 1 ;
        sh:maxInclusive 10 ;
        sh:defaultValue 5 ;
        sh:message "Priority must be between 1 and 10"@en ;
    ] ;
    
    # Optional: description
    sh:property [
        sh:path mesh:description ;
        sh:datatype xsd:string ;
        sh:maxCount 1 ;
        sh:maxLength 1000 ;
        sh:message "Task description must be at most 1000 characters"@en ;
    ] .

# ============================================================
# Message Shape
# ============================================================

mesh:MessageShape a sh:NodeShape ;
    sh:targetClass mesh:Message ;
    rdfs:label "Message Shape"@en ;
    rdfs:comment "Validation shape for messages"@en ;
    
    # Required: sender
    sh:property [
        sh:path mesh:sender ;
        sh:class mesh:Agent ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:message "Message must have a sender"@en ;
    ] ;
    
    # Required: timestamp
    sh:property [
        sh:path mesh:timestamp ;
        sh:datatype xsd:dateTime ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:message "Message must have a timestamp"@en ;
    ] ;
    
    # Required: payload
    sh:property [
        sh:path mesh:payload ;
        sh:datatype xsd:string ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:maxLength 65536 ;
        sh:message "Message payload must be at most 64KB"@en ;
    ] ;
    
    # Optional: receiver
    sh:property [
        sh:path mesh:receiver ;
        sh:class mesh:Agent ;
        sh:maxCount 1 ;
        sh:message "Message may have at most one receiver"@en ;
    ] .

# ============================================================
# Capability Shape
# ============================================================

mesh:CapabilityShape a sh:NodeShape ;
    sh:targetClass mesh:Capability ;
    rdfs:label "Capability Shape"@en ;
    rdfs:comment "Validation shape for capabilities"@en ;
    
    # Required: label
    sh:property [
        sh:path rdfs:label ;
        sh:datatype xsd:string ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:message "Capability must have a label"@en ;
    ] ;
    
    # Inheritance chain must be acyclic (SPARQL constraint)
    sh:sparql [
        sh:message "Capability inheritance must not contain cycles"@en ;
        sh:select """
            PREFIX mesh: <https://meshpocalypse.dev/ontology#>
            PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
            SELECT $this
            WHERE {
                $this rdfs:subClassOf+ $this .
            }
        """ ;
    ] .

# ============================================================
# Trust Level Shapes
# ============================================================

mesh:InternToJuniorShape a sh:NodeShape ;
    sh:targetClass mesh:PromotionRequest ;
    rdfs:label "Intern to Junior Promotion Shape"@en ;
    
    sh:property [
        sh:path mesh:currentLevel ;
        sh:hasValue 0 ;
        sh:message "Must be at Intern level (0)"@en ;
    ] ;
    
    sh:property [
        sh:path mesh:successfulReads ;
        sh:datatype xsd:integer ;
        sh:minInclusive 50 ;
        sh:message "Must have at least 50 successful reads"@en ;
    ] ;
    
    sh:property [
        sh:path mesh:errorRate ;
        sh:datatype xsd:decimal ;
        sh:maxInclusive 0.01 ;
        sh:message "Error rate must be at most 1%"@en ;
    ] .

mesh:JuniorToSeniorShape a sh:NodeShape ;
    sh:targetClass mesh:PromotionRequest ;
    rdfs:label "Junior to Senior Promotion Shape"@en ;
    
    sh:property [
        sh:path mesh:currentLevel ;
        sh:hasValue 1 ;
        sh:message "Must be at Junior level (1)"@en ;
    ] ;
    
    sh:property [
        sh:path mesh:successfulWrites ;
        sh:datatype xsd:integer ;
        sh:minInclusive 100 ;
        sh:message "Must have at least 100 successful writes"@en ;
    ] ;
    
    sh:property [
        sh:path mesh:shaclFailures7d ;
        sh:datatype xsd:integer ;
        sh:hasValue 0 ;
        sh:message "Must have 0 SHACL validation failures in last 7 days"@en ;
    ] .

mesh:SeniorToPrincipalShape a sh:NodeShape ;
    sh:targetClass mesh:PromotionRequest ;
    rdfs:label "Senior to Principal Promotion Shape"@en ;
    
    sh:property [
        sh:path mesh:currentLevel ;
        sh:hasValue 2 ;
        sh:message "Must be at Senior level (2)"@en ;
    ] ;
    
    sh:property [
        sh:path mesh:totalOperations ;
        sh:datatype xsd:integer ;
        sh:minInclusive 500 ;
        sh:message "Must have at least 500 total operations"@en ;
    ] ;
    
    sh:property [
        sh:path mesh:daysAtCurrentLevel ;
        sh:datatype xsd:integer ;
        sh:minInclusive 30 ;
        sh:message "Must be at Senior level for at least 30 days"@en ;
    ] ;
    
    sh:property [
        sh:path mesh:peerEndorsements ;
        sh:datatype xsd:integer ;
        sh:minInclusive 2 ;
        sh:message "Must have at least 2 peer endorsements"@en ;
    ] .

# ============================================================
# SPARQL Access Control Shapes
# ============================================================

mesh:SparqlSelectAccessShape a sh:NodeShape ;
    sh:targetClass mesh:SparqlQueryRequest ;
    rdfs:label "SPARQL SELECT Access Shape"@en ;
    
    sh:property [
        sh:path mesh:requestedBy ;
        sh:node mesh:AnyAgentShape ;
        sh:message "SELECT queries allowed for all trust levels"@en ;
    ] .

mesh:SparqlUpdateAccessShape a sh:NodeShape ;
    sh:targetClass mesh:SparqlUpdateRequest ;
    rdfs:label "SPARQL UPDATE Access Shape"@en ;
    
    sh:property [
        sh:path mesh:requestedBy ;
        sh:node mesh:SeniorAgentShape ;
        sh:message "UPDATE queries require Senior+ trust level"@en ;
    ] .

mesh:SparqlServiceAccessShape a sh:NodeShape ;
    sh:targetClass mesh:SparqlServiceRequest ;
    rdfs:label "SPARQL SERVICE Access Shape"@en ;
    
    sh:property [
        sh:path mesh:requestedBy ;
        sh:node mesh:PrincipalAgentShape ;
        sh:message "SERVICE (federation) queries require Principal+ trust level"@en ;
    ] .

mesh:AnyAgentShape a sh:NodeShape ;
    rdfs:label "Any Agent Shape"@en ;
    sh:property [
        sh:path mesh:trustLevel ;
        sh:datatype xsd:integer ;
        sh:minInclusive 0 ;
        sh:maxInclusive 4 ;
    ] .

mesh:SeniorAgentShape a sh:NodeShape ;
    rdfs:label "Senior Agent Shape"@en ;
    sh:property [
        sh:path mesh:trustLevel ;
        sh:datatype xsd:integer ;
        sh:minInclusive 2 ;
        sh:maxInclusive 4 ;
    ] .

mesh:PrincipalAgentShape a sh:NodeShape ;
    rdfs:label "Principal Agent Shape"@en ;
    sh:property [
        sh:path mesh:trustLevel ;
        sh:datatype xsd:integer ;
        sh:minInclusive 3 ;
        sh:maxInclusive 4 ;
    ] .

# ============================================================
# Mesh Node Shape
# ============================================================

mesh:MeshNodeShape a sh:NodeShape ;
    sh:targetClass mesh:MeshNode ;
    rdfs:label "Mesh Node Shape"@en ;
    
    sh:property [
        sh:path mesh:sparqlEndpoint ;
        sh:datatype xsd:anyURI ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:message "Mesh node must have a SPARQL endpoint"@en ;
    ] ;
    
    sh:property [
        sh:path mesh:region ;
        sh:datatype xsd:string ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:message "Mesh node must have a region"@en ;
    ] ;
    
    sh:property [
        sh:path mesh:did ;
        sh:datatype xsd:anyURI ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:pattern "^did:mesh:.+$" ;
        sh:message "Mesh node must have a valid did:mesh identifier"@en ;
    ] ;
    
    sh:property [
        sh:path mesh:publicKey ;
        sh:datatype xsd:string ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:message "Mesh node must have a public key"@en ;
    ] ;
    
    sh:property [
        sh:path mesh:status ;
        sh:datatype xsd:string ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:in ("online" "offline" "degraded") ;
        sh:message "Mesh node status must be online, offline, or degraded"@en ;
    ] .

# ============================================================
# Provenance Record Shape
# ============================================================

mesh:ProvenanceRecordShape a sh:NodeShape ;
    sh:targetClass mesh:ProvenanceRecord ;
    rdfs:label "Provenance Record Shape"@en ;
    
    sh:property [
        sh:path mesh:actionType ;
        sh:datatype xsd:string ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:in ("SPARQL_SELECT" "SPARQL_UPDATE" "SPARQL_SERVICE" "TOOL_INVOCATION" "MODEL_INVOCATION") ;
        sh:message "Action type must be a valid provenance action type"@en ;
    ] ;
    
    sh:property [
        sh:path mesh:currentRecordHash ;
        sh:datatype xsd:string ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:pattern "^[a-f0-9]{64}$" ;
        sh:message "Current record hash must be a valid SHA-256 hash"@en ;
    ] ;
    
    sh:property [
        sh:path mesh:previousRecordHash ;
        sh:datatype xsd:string ;
        sh:maxCount 1 ;
        sh:pattern "^[a-f0-9]{64}$" ;
        sh:message "Previous record hash must be a valid SHA-256 hash"@en ;
    ] .

# ============================================================
# Phase 1 Additions — AgentCard and TaskMessage Shapes
# ============================================================

# AgentCard Shape
mesh:AgentCardShape a sh:NodeShape ;
    sh:targetClass mesh:AgentCard ;
    sh:property [
        sh:path mesh:agentId ;
        sh:datatype xsd:string ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:message "AgentCard must have exactly one agentId" ;
    ] ;
    sh:property [
        sh:path mesh:status ;
        sh:in ( "idle" "busy" "offline" "error" ) ;
        sh:datatype xsd:string ;
        sh:message "status must be: idle, busy, offline, or error" ;
    ] ;
    sh:property [
        sh:path mesh:trustLevel ;
        sh:datatype xsd:integer ;
        sh:minInclusive 0 ;
        sh:maxInclusive 4 ;
        sh:message "trustLevel must be 0-4" ;
    ] .

# TaskMessage Shape
mesh:TaskMessageShape a sh:NodeShape ;
    sh:targetClass mesh:TaskMessage ;
    sh:property [
        sh:path mesh:taskId ;
        sh:datatype xsd:string ;
        sh:minCount 1 ;
        sh:message "TaskMessage must have a taskId" ;
    ] ;
    sh:property [
        sh:path mesh:assignedTo ;
        sh:nodeKind sh:IRI ;
        sh:message "assignedTo must be an agent IRI" ;
    ] .

# ── ATF Trust Level Shapes ────────────────────────────────────────────────────

mesh:ATFAgentShape a sh:NodeShape ;
    sh:targetClass mesh:Agent ;
    sh:property [
        sh:path mesh:trustLevel ;
        sh:datatype xsd:integer ;
        sh:minInclusive 0 ;
        sh:maxInclusive 4 ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:message "ATF trust level must be an integer 0–4 (INTERN to SYSTEM)" ;
    ] ;
    sh:property [
        sh:path mesh:reads ;
        sh:datatype xsd:integer ;
        sh:minInclusive 0 ;
        sh:message "reads must be a non-negative integer" ;
    ] ;
    sh:property [
        sh:path mesh:errors ;
        sh:datatype xsd:integer ;
        sh:minInclusive 0 ;
        sh:message "errors must be a non-negative integer" ;
    ] .

mesh:ATFPromotionShape a sh:NodeShape ;
    sh:targetClass mesh:PromotionEvent ;
    sh:property [
        sh:path mesh:fromLevel ;
        sh:datatype xsd:integer ;
        sh:minInclusive 0 ;
        sh:maxInclusive 3 ;
        sh:minCount 1 ;
        sh:message "Promotion must have a fromLevel 0–3" ;
    ] ;
    sh:property [
        sh:path mesh:toLevel ;
        sh:datatype xsd:integer ;
        sh:minInclusive 1 ;
        sh:maxInclusive 4 ;
        sh:minCount 1 ;
        sh:message "Promotion must have a toLevel 1–4" ;
    ] ;
    sh:property [
        sh:path prov:wasAssociatedWith ;
        sh:nodeKind sh:IRI ;
        sh:minCount 1 ;
        sh:message "Promotion must be associated with an agent (not self)" ;
    ] .

# ============================================================
# TokenBudget SHACL Shapes (v1.2.0)
# Scope hierarchy: tenant (required) → member/agent/task (optional)
# ============================================================

mesh:TokenBudgetShape a sh:NodeShape ;
    rdfs:label "Token Budget Shape"@en ;
    rdfs:comment "SHACL constraints for mesh:TokenBudget instances."@en ;
    sh:targetClass mesh:TokenBudget ;

    # Required: budgetOwner (Tenant)
    sh:property [
        sh:path mesh:budgetOwner ;
        sh:name "budget owner" ;
        sh:description "Tenant that owns this token budget." ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:class mesh:Tenant ;
        sh:message "TokenBudget must have exactly one mesh:budgetOwner of type mesh:Tenant."@en ;
    ] ;

    # Optional: budgetMember — if present, must be a Member
    sh:property [
        sh:path mesh:budgetMember ;
        sh:name "budget member" ;
        sh:description "Optional member (user) this budget applies to." ;
        sh:maxCount 1 ;
        sh:class mesh:Member ;
        sh:message "mesh:budgetMember must reference a mesh:Member instance."@en ;
    ] ;

    # Optional: budgetAgent — if present, must be an Agent
    sh:property [
        sh:path mesh:budgetAgent ;
        sh:name "budget agent" ;
        sh:description "Optional agent this budget applies to." ;
        sh:maxCount 1 ;
        sh:class mesh:Agent ;
        sh:message "mesh:budgetAgent must reference a mesh:Agent instance."@en ;
    ] ;

    # Optional: budgetTask — if present, must be a Task
    sh:property [
        sh:path mesh:budgetTask ;
        sh:name "budget task" ;
        sh:description "Optional task or task type this budget applies to." ;
        sh:maxCount 1 ;
        sh:class mesh:Task ;
        sh:message "mesh:budgetTask must reference a mesh:Task instance."@en ;
    ] ;

    # Required: maxTokens (non-negative integer)
    sh:property [
        sh:path mesh:maxTokens ;
        sh:name "max tokens" ;
        sh:description "Hard token ceiling for this budget window." ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:datatype xsd:integer ;
        sh:minInclusive 0 ;
        sh:message "mesh:maxTokens must be a non-negative integer."@en ;
    ] ;

    # Required: softLimitTokens (non-negative integer)
    sh:property [
        sh:path mesh:softLimitTokens ;
        sh:name "soft limit tokens" ;
        sh:description "Warning threshold; must be <= maxTokens." ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:datatype xsd:integer ;
        sh:minInclusive 0 ;
        sh:message "mesh:softLimitTokens must be a non-negative integer."@en ;
    ] ;

    # Optional: spentTokens snapshot
    sh:property [
        sh:path mesh:spentTokens ;
        sh:name "spent tokens" ;
        sh:description "Current snapshot of tokens spent. Must be non-negative if present." ;
        sh:maxCount 1 ;
        sh:datatype xsd:integer ;
        sh:minInclusive 0 ;
        sh:message "mesh:spentTokens must be a non-negative integer."@en ;
    ] ;

    # Optional: windowStart
    sh:property [
        sh:path mesh:windowStart ;
        sh:name "window start" ;
        sh:description "Start of budget window." ;
        sh:maxCount 1 ;
        sh:datatype xsd:dateTime ;
        sh:message "mesh:windowStart must be an xsd:dateTime value."@en ;
    ] ;

    # Optional: windowEnd
    sh:property [
        sh:path mesh:windowEnd ;
        sh:name "window end" ;
        sh:description "End of budget window." ;
        sh:maxCount 1 ;
        sh:datatype xsd:dateTime ;
        sh:message "mesh:windowEnd must be an xsd:dateTime value."@en ;
    ] ;

    # Required: limitType — must be "cost", "safety", or "mixed"
    sh:property [
        sh:path mesh:limitType ;
        sh:name "limit type" ;
        sh:description "Semantic purpose of this budget: cost, safety, or mixed." ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:datatype xsd:string ;
        sh:in ( "cost" "safety" "mixed" ) ;
        sh:message "mesh:limitType must be one of: cost, safety, mixed."@en ;
    ] ;

    # Cross-property: softLimitTokens must be <= maxTokens
    sh:sparql [
        sh:message "mesh:softLimitTokens must be less than or equal to mesh:maxTokens." ;
        sh:prefixes [
            sh:declare [
                sh:prefix "mesh" ;
                sh:namespace "https://meshpocalypse.dev/ontology#" ;
            ] ;
        ] ;
        sh:select """
            SELECT $this
            WHERE {
              $this mesh:maxTokens ?max .
              $this mesh:softLimitTokens ?soft .
              FILTER (?soft > ?max)
            }
        """ ;
    ] ;

    # Cross-property: if budgetMember is set, its memberOf must match budgetOwner
    sh:sparql [
        sh:message "If mesh:budgetMember is set, the member's mesh:memberOf must equal the budget's mesh:budgetOwner." ;
        sh:prefixes [
            sh:declare [
                sh:prefix "mesh" ;
                sh:namespace "https://meshpocalypse.dev/ontology#" ;
            ] ;
        ] ;
        sh:select """
            SELECT $this
            WHERE {
              $this mesh:budgetMember ?member .
              $this mesh:budgetOwner ?tenant .
              ?member mesh:memberOf ?memberTenant .
              FILTER (?memberTenant != ?tenant)
            }
        """ ;
    ] .

# ============================================================
# Circuit Breaker State Shape (v1.3.0, Issue #115)
# Validates mesh:circuitState values for any agent that has a circuit entry.
# ============================================================

mesh:CircuitStateShape a sh:NodeShape ;
    rdfs:label "Circuit State Shape"@en ;
    rdfs:comment "Validates that an agent's circuit breaker state is one of the allowed values."@en ;
    sh:targetSubjectsOf mesh:circuitState ;

    sh:property [
        sh:path mesh:circuitState ;
        sh:datatype xsd:string ;
        sh:in ( "CLOSED" "OPEN" "HALF_OPEN" ) ;
        sh:maxCount 1 ;
        sh:minCount 1 ;
        sh:message "mesh:circuitState must be exactly one of: CLOSED, OPEN, HALF_OPEN"@en ;
    ] ;

    sh:property [
        sh:path mesh:lastCircuitChange ;
        sh:datatype xsd:dateTime ;
        sh:maxCount 1 ;
        sh:message "mesh:lastCircuitChange must be an xsd:dateTime value"@en ;
    ] .

mesh:ModelInvocationShape a sh:NodeShape ;
    rdfs:label "Model Invocation Shape"@en ;
    rdfs:comment "SHACL constraints for mesh:ModelInvocation instances when a budget is present."@en ;
    sh:targetClass mesh:ModelInvocation ;

    # chargedTo, if present, must reference a TokenBudget
    sh:property [
        sh:path mesh:chargedTo ;
        sh:name "charged to" ;
        sh:description "The token budget this invocation is charged against." ;
        sh:maxCount 1 ;
        sh:class mesh:TokenBudget ;
        sh:message "mesh:chargedTo must reference a mesh:TokenBudget instance."@en ;
    ] .

# ============================================================
# ModelTier Shape (Wave 3B — Issue #128)
# Validates that agents using mesh:modelTier reference a known
# tier instance (ECONOMY/STANDARD/PREMIUM) and that
# mesh:costPerKToken is non-negative when present.
# ============================================================

mesh:ModelTierShape a sh:NodeShape ;
    rdfs:label "Model Tier Shape"@en ;
    rdfs:comment "SHACL validation shape for agent model cost tier assignments."@en ;
    sh:targetSubjectsOf mesh:modelTier ;
    sh:property [
        sh:path mesh:modelTier ;
        sh:name "model tier" ;
        sh:description "The cost tier assigned to this agent's LLM backend." ;
        sh:class mesh:ModelTier ;
        sh:in ( mesh:ECONOMY mesh:STANDARD mesh:PREMIUM ) ;
        sh:maxCount 1 ;
        sh:message "mesh:modelTier must be one of mesh:ECONOMY, mesh:STANDARD, or mesh:PREMIUM."@en ;
    ] ;
    sh:property [
        sh:path mesh:costPerKToken ;
        sh:name "cost per kToken" ;
        sh:description "Approximate USD cost per 1000 tokens. Must be non-negative when present." ;
        sh:datatype xsd:decimal ;
        sh:minInclusive 0.0 ;
        sh:maxCount 1 ;
        sh:message "mesh:costPerKToken must be a non-negative xsd:decimal."@en ;
    ] .

# ============================================================
# TaskBid Shape — Capability Auction Protocol (ADR-009)
# ============================================================

mesh:TaskBidShape a sh:NodeShape ;
    sh:targetClass mesh:TaskBid ;
    rdfs:label "Task Bid Shape"@en ;
    rdfs:comment "Validation shape for capability auction bids."@en ;

    sh:property [
        sh:path mesh:bidScore ;
        sh:datatype xsd:decimal ;
        sh:minInclusive 0.0 ;
        sh:maxInclusive 1.0 ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:message "bidScore must be a decimal in [0.0, 1.0]."@en ;
    ] ;

    sh:property [
        sh:path mesh:bidStatus ;
        sh:datatype xsd:string ;
        sh:in ( "PENDING" "WON" "LOST" "TOMBSTONED" ) ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:message "bidStatus must be PENDING, WON, LOST, or TOMBSTONED."@en ;
    ] ;

    sh:property [
        sh:path mesh:taskId ;
        sh:datatype xsd:string ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:message "TaskBid must reference exactly one taskId."@en ;
    ] ;

    sh:property [
        sh:path mesh:biddingAgent ;
        sh:class mesh:Agent ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:message "TaskBid must reference exactly one biddingAgent."@en ;
    ] ;

    sh:property [
        sh:path mesh:agentLoad ;
        sh:datatype xsd:decimal ;
        sh:minInclusive 0.0 ;
        sh:maxInclusive 1.0 ;
        sh:maxCount 1 ;
        sh:message "agentLoad must be a decimal in [0.0, 1.0] when present."@en ;
    ] .

# ============================================================
# SubBudget Shape — Budget Delegation (v1.3.0, Issue #127)
# Any TokenBudget that has a mesh:parentBudget link must satisfy
# these constraints (allocation provenance fields are mandatory).
# =====================================================

mesh:SubBudgetShape a sh:NodeShape ;
    rdfs:label "Sub-Budget Shape"@en ;
    rdfs:comment "SHACL constraints for delegated child TokenBudget instances."@en ;
    sh:targetSubjectsOf mesh:parentBudget ;

    sh:property [
        sh:path mesh:allocationId ;
        sh:name "allocation id" ;
        sh:description "UUID v4 identifier for the allocation event." ;
        sh:datatype xsd:string ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:message "Sub-budget must have exactly one mesh:allocationId (UUID string)."@en ;
    ] ;

    sh:property [
        sh:path mesh:allocatedAt ;
        sh:name "allocated at" ;
        sh:description "Timestamp of the allocation event." ;
        sh:datatype xsd:dateTime ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:message "Sub-budget must have exactly one mesh:allocatedAt (xsd:dateTime)."@en ;
    ] ;

    sh:property [
        sh:path mesh:allocatedBy ;
        sh:name "allocated by" ;
        sh:description "The agent that performed the allocation." ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:message "Sub-budget must have exactly one mesh:allocatedBy agent."@en ;
    ] ;

    sh:property [
        sh:path mesh:parentBudget ;
        sh:name "parent budget" ;
        sh:description "Pointer to the parent TokenBudget this sub-budget was allocated from." ;
        sh:class mesh:TokenBudget ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:message "Sub-budget must have exactly one mesh:parentBudget of type mesh:TokenBudget."@en ;
    ] .

# ATF Review Flow — SHACL Shape (Wave 4B — Issue #129)
# ============================================================

mesh:TrustReviewShape
    a sh:NodeShape ;
    sh:targetClass mesh:TrustReviewRequired ;
    rdfs:comment "Validates that every TrustReviewRequired event carries required provenance fields." ;
    sh:property [
        sh:path mesh:suspendedAt ;
        sh:datatype xsd:dateTime ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:message "Each TrustReviewRequired event must have exactly one mesh:suspendedAt timestamp."@en
    ] ;
    sh:property [
        sh:path mesh:reviewReason ;
        sh:datatype xsd:string ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:message "Each TrustReviewRequired event must have exactly one mesh:reviewReason string."@en
    ] ;
    sh:property [
        sh:path mesh:reviewTtlSeconds ;
        sh:datatype xsd:integer ;
        sh:minInclusive 60 ;
        sh:maxCount 1 ;
        sh:message "mesh:reviewTtlSeconds must be at least 60 seconds when provided."@en
    ] ;
    sh:property [
        sh:path mesh:reviewOutcome ;
        sh:in ( "mesh:Reinstated" "mesh:PermanentlyDemoted" ) ;
        sh:maxCount 1 ;
        sh:message "mesh:reviewOutcome must be 'mesh:Reinstated' or 'mesh:PermanentlyDemoted' when present."@en
    ] .


# ============================================================
# Capability Taxonomy Shape (Issue #154)
# ============================================================

mesh:CapabilityTaxonomyShape a sh:NodeShape ;
    sh:targetSubjectsOf mesh:hasCapability ;
    rdfs:label "Capability Taxonomy Shape"@en ;
    rdfs:comment "Validates that mesh:hasCapability values are owl:Class instances that are rdfs:subClassOf mesh:Capability."@en ;
    sh:property [
        sh:path mesh:hasCapability ;
        sh:class owl:Class ;
        sh:sparql [
            sh:message "mesh:hasCapability value must be a class that is a subclass of mesh:Capability (directly or transitively)."@en ;
            sh:select """
                PREFIX mesh: <https://meshpocalypse.dev/ontology#>
                PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
                SELECT $this (?value AS ?value)
                WHERE {
                    $this mesh:hasCapability ?value .
                    FILTER NOT EXISTS {
                        ?value rdfs:subClassOf* mesh:Capability .
                    }
                }
            """ ;
        ] ;
    ] .

# ============================================================
# ModelCapability Shape (Issue #121)
# ============================================================

mesh:ModelCapabilityShape a sh:NodeShape ;
    sh:targetClass mesh:ModelCapability ;
    rdfs:label "Model Capability Shape"@en ;
    rdfs:comment "Validates that every mesh:ModelCapability has the required cost, context, quality, provider, and model ID properties with correct types and value ranges."@en ;

    sh:property [
        sh:path mesh:modelId ;
        sh:name "model ID" ;
        sh:description "Provider-specific model identifier (e.g. 'gpt-4o')." ;
        sh:datatype xsd:string ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:minLength 1 ;
        sh:message "Each mesh:ModelCapability must have exactly one non-empty mesh:modelId string."@en ;
    ] ;

    sh:property [
        sh:path mesh:modelProvider ;
        sh:name "model provider" ;
        sh:description "The organisation providing this model (e.g. 'openai')." ;
        sh:datatype xsd:string ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:minLength 1 ;
        sh:message "Each mesh:ModelCapability must have exactly one non-empty mesh:modelProvider string."@en ;
    ] ;

    sh:property [
        sh:path mesh:costPerToken ;
        sh:name "cost per token" ;
        sh:description "USD cost per output token. Must be non-negative." ;
        sh:datatype xsd:decimal ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:minInclusive 0.0 ;
        sh:message "mesh:costPerToken must be a non-negative xsd:decimal."@en ;
    ] ;

    sh:property [
        sh:path mesh:contextWindow ;
        sh:name "context window" ;
        sh:description "Maximum token context length. Must be at least 1." ;
        sh:datatype xsd:integer ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:minInclusive 1 ;
        sh:message "mesh:contextWindow must be a positive xsd:integer of at least 1."@en ;
    ] ;

    sh:property [
        sh:path mesh:qualityScore ;
        sh:name "quality score" ;
        sh:description "Normalised quality rating in [0.0, 1.0]." ;
        sh:datatype xsd:decimal ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:minInclusive 0.0 ;
        sh:maxInclusive 1.0 ;
        sh:message "mesh:qualityScore must be an xsd:decimal in the range [0.0, 1.0]."@en ;
    ] .
