# Ontology-level integrity rules as SHACL-SPARQL constraints (W3C SHACL vocabulary).
# Executed by lib/integrity.js, a small runner for this subset: for each shape it binds $this to every
# instance of sh:targetClass (or every subject, when the shape has no target) and runs sh:select.
# It is not a full SHACL engine (no property shapes, no sh:path); it exists to run these rules live.

@prefix sh:   <http://www.w3.org/ns/shacl#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix pm:   <https://semantic-layer.mhndlabs.com/ontology/pm#> .
@prefix pmv:  <https://semantic-layer.mhndlabs.com/vocab/> .
@prefix sl:   <https://semantic-layer.mhndlabs.com/shapes/> .

sl:deal-has-company a sh:NodeShape ;
    rdfs:label "Every deal links to at least one company" ;
    sh:targetClass pm:Deal ; sh:severity sh:Violation ;
    sh:sparql [ sh:message "Deal has no pm:targetCompany that is a pm:Company" ;
        sh:select """SELECT $this WHERE { FILTER NOT EXISTS { $this pm:targetCompany ?c . ?c a pm:Company } }""" ] .

sl:taxonomy-value-in-scheme a sh:NodeShape ;
    rdfs:label "Taxonomy values come from the controlled vocabulary" ;
    sh:severity sh:Violation ;
    sh:sparql [ sh:message "Value is not a concept in the property's concept scheme" ;
        sh:select """SELECT $this ?path ?value WHERE { $this ?path ?value . ?path pm:rangeScheme ?scheme . FILTER NOT EXISTS { ?value skos:inScheme ?scheme } }""" ] .

sl:deal-size-positive a sh:NodeShape ;
    rdfs:label "Deal size is positive" ;
    sh:targetClass pm:Deal ; sh:severity sh:Violation ;
    sh:sparql [ sh:message "pm:dealSizeUsd must be greater than zero" ;
        sh:select """SELECT $this ?value WHERE { $this pm:dealSizeUsd ?value . FILTER(?value <= 0) }""" ] .

sl:no-deprecated-concepts a sh:NodeShape ;
    rdfs:label "No instance points at a deprecated concept" ;
    sh:severity sh:Warning ;
    sh:sparql [ sh:message "Uses a deprecated concept; migrate to its dcterms:isReplacedBy" ;
        sh:select """SELECT $this ?path ?value WHERE { $this ?path ?value . ?path pm:rangeScheme ?s . ?value owl:deprecated true }""" ] .

sl:affiliation-dates-ordered a sh:NodeShape ;
    rdfs:label "An affiliation ends after it starts" ;
    sh:targetClass pm:Affiliation ; sh:severity sh:Violation ;
    sh:sparql [ sh:message "pm:endDate is before pm:startDate" ;
        sh:select """SELECT $this ?start ?end WHERE { $this pm:startDate ?start ; pm:endDate ?end . FILTER(?end < ?start) }""" ] .

sl:fund-has-one-manager a sh:NodeShape ;
    rdfs:label "Every fund has exactly one manager" ;
    sh:targetClass pm:Fund ; sh:severity sh:Violation ;
    sh:sparql [ sh:message "Fund must have exactly one pm:managedBy" ;
        sh:select """SELECT $this WHERE { OPTIONAL { SELECT $this (COUNT(DISTINCT ?m) AS ?n) WHERE { $this pm:managedBy ?m } GROUP BY $this } FILTER(!BOUND(?n) || ?n != 1) }""" ] .

sl:participant-is-investor a sh:NodeShape ;
    rdfs:label "Only investor firms and funds participate in deals" ;
    sh:targetClass pm:Deal ; sh:severity sh:Violation ;
    sh:sparql [ sh:message "A deal participant is neither an investor firm nor a fund" ;
        sh:select """SELECT $this ?value WHERE { ?value pm:participatedIn $this . FILTER NOT EXISTS { ?value a ?k . ?k rdfs:subClassOf* pm:InvestorFirm } FILTER NOT EXISTS { ?value a pm:Fund } }""" ] .

sl:firm-has-one-type a sh:NodeShape ;
    rdfs:label "Every investor firm has exactly one investor-firm subclass" ;
    sh:targetClass pm:InvestorFirm ; sh:severity sh:Violation ;
    sh:sparql [ sh:message "Investor firm is typed with zero or several disjoint subclasses" ;
        sh:select """SELECT $this WHERE { OPTIONAL { SELECT $this (COUNT(DISTINCT ?k) AS ?n) WHERE { $this a ?k . ?k rdfs:subClassOf pm:InvestorFirm } GROUP BY $this } FILTER(!BOUND(?n) || ?n != 1) }""" ] .
