# Copyright (c) 2026 Cisco Systems, Inc. and its affiliates # SPDX-License-Identifier: Apache-2.0 @prefix mas: . @prefix owl: . @prefix rdf: . @prefix rdfs: . @prefix xsd: . @prefix sh: . # ============================================================ # MAS Custom SHACL Shapes # # Hand-maintained supplement to mas-shapes.ttl. mas-shapes.ttl is fully # auto-generated by scripts/generate_shacl_shapes.py (gitignored, # regenerated by `make generate-shapes` / CI) and only ever contains # class-level sh:NodeShape scaffolding + bare sh:property references to # the self-contained sh:PropertyShapes declared inline on each property in # the ontology files themselves. # # Most constraints that used to live here as separate NodeShapes were # simplified away: # - range typing (sh:class) and exact-one cardinality (sh:maxCount) fold # directly onto the relevant property's own inline sh:PropertyShape -- # see mas:executesLLM/executesTool/executesProcessing/inputTo/leadsTo # in mas-ontology.ttl. Prefer that when adding a new rule: if it's # expressible as sh:minCount/maxCount/class/datatype/severity on a # single property's own values, put it directly on that property's # declaration, not here. # - a conditional "if A then B" requirement (mas:aboutMetric, in # analysis-ontology.ttl) can't fold onto the property itself -- a # sh:PropertyShape with sh:path binds $this to each VALUE of the path, # not the focus node, so sh:or/sh:not on it only run when a value # already exists. It needs its own small NodeShape (sh:targetClass, no # sh:path) instead, placed right next to the property it's about -- # still core SHACL (sh:or/sh:not), not SPARQL, just not foldable onto # the property declaration. # # What's left below genuinely can't be either of those: it needs sh:sparql # because it reasons across MULTIPLE nodes/edges (not one focus node's own # values) -- counting other subjects, or checking rdf:type completeness. # # Loaded alongside mas-shapes.ttl by both validation.py (lint-time TTL # checks) and verification.py (pyshacl runtime checks against KG # instances). Never touched by the generator -- edit freely. # # sh:sparql query text is evaluated against whatever data graph pyshacl is # validating, which -- unlike this file -- generally has no @prefix # bindings of its own (verify_kg_object() builds its instance graph # programmatically, with no namespace bindings at all). So every sh:sparql # constraint below carries its own sh:prefixes, per the SHACL-SPARQL spec's # sh:declare/sh:prefix/sh:namespace mechanism -- relying on the mas: # prefix "just working" inside sh:select would break the moment it's run # against a graph that never declared it. # ============================================================ mas:CustomShapesPrefixes sh:declare [ sh:prefix "mas" ; sh:namespace "https://outshift-open.github.io/oxp-ontology/mas#"^^xsd:anyURI ; ] . # --------------------------------------------------------------------------- # Abstract base classes must never be directly instantiated in KG output. # Not a property constraint -- it's a check that $this's rdf:type includes # some concrete subclass, which sh:class/sh:targetClass alone can't express. # --------------------------------------------------------------------------- mas:NoDirectExecutionElementInstancesShape a sh:NodeShape ; sh:targetClass mas:ExecutionElement ; sh:sparql [ sh:prefixes mas:CustomShapesPrefixes ; sh:message "ExecutionElement is abstract and must not be directly instantiated; emit a concrete subclass (Session, AgentCall, ToolCall, ...)." ; sh:select """ SELECT $this WHERE { $this a mas:ExecutionElement . FILTER NOT EXISTS { $this a ?concrete . ?concrete rdfs:subClassOf+ mas:ExecutionElement . } } """ ; ] . # --------------------------------------------------------------------------- # Rule: a Session's initial/final State is shared with the boundary # ExecutionElement that actually starts/ends the session (typically the # first/last AgentCall or ProcessingCall directly under it) -- it isn't # claimed by the Session alone. Needs sh:sparql because it counts OTHER # subjects pointing at the same value node -- not $this's own property # values, which is all a plain property shape can see. # # Every ExecutionElement (including Session) already requires >=1 # hasInitialState/hasFinalState of its own via the property shapes declared # inline on mas-ontology.ttl -- this shape only adds the extra sharing # constraint that is specific to a Session's own boundary states. # --------------------------------------------------------------------------- mas:SessionInitialStateSharedShape a sh:NodeShape ; sh:targetClass mas:Session ; sh:sparql [ sh:prefixes mas:CustomShapesPrefixes ; sh:message "A Session's initial State must also be claimed as the initial state of at least one other ExecutionElement (e.g. the AgentCall or ProcessingCall that actually starts the session)." ; sh:select """ SELECT $this WHERE { $this mas:hasInitialState ?initial . FILTER NOT EXISTS { ?other mas:hasInitialState ?initial . FILTER (?other != $this) } } """ ; ] . mas:SessionFinalStateSharedShape a sh:NodeShape ; sh:targetClass mas:Session ; sh:sparql [ sh:prefixes mas:CustomShapesPrefixes ; sh:message "A Session's final State must also be claimed as the final state of at least one other ExecutionElement (e.g. the AgentCall or ProcessingCall that actually ends the session)." ; sh:select """ SELECT $this WHERE { $this mas:hasFinalState ?final . FILTER NOT EXISTS { ?other mas:hasFinalState ?final . FILTER (?other != $this) } } """ ; ] .