@prefix convo: <https://ontology.inferal.com/modules/conversation/> .
@prefix dcterms: <http://purl.org/dc/terms/> .
@prefix prof: <http://www.w3.org/ns/dx/prof/> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix role: <http://www.w3.org/ns/dx/prof/role/> .
@prefix sh: <http://www.w3.org/ns/shacl#> .

convo:ThreadedConversationProfile
    a prof:Profile ;
    prof:isProfileOf convo: ;
    prof:hasResource <https://ontology.inferal.com/modules/conversation/profiles/threaded#constraints> .

<https://ontology.inferal.com/modules/conversation/profiles/threaded#constraints>
    a prof:ResourceDescriptor ;
    rdfs:label "Threaded Conversation constraints descriptor"@en ;
    rdfs:comment "Describes the SHACL artifact that validates threaded conversation structure."@en ;
    prof:hasArtifact <https://ontology.inferal.com/modules/conversation/profiles/threaded.ttl> ;
    prof:hasRole role:constraints, role:validation ;
    dcterms:conformsTo <http://www.w3.org/ns/shacl#> ;
    dcterms:format <https://www.iana.org/assignments/media-types/text/turtle> .

convo:ThreadedThreadShape
    a sh:NodeShape ;
    rdfs:label "threaded thread shape"@en ;
    rdfs:comment "Requires an explicit thread to contain at least one contribution through either direction of the membership inverse."@en ;
    sh:targetClass convo:Thread ;
    sh:property [
        sh:path [ sh:alternativePath ( convo:hasThreadContribution [ sh:inversePath convo:inThread ] ) ] ;
        sh:class convo:Contribution ;
        sh:minCount 1 ;
    ] ;
    sh:sparql [
        sh:message "Every contribution grouped by a thread must belong to that thread's conversation."@en ;
        sh:select """
            PREFIX convo: <https://ontology.inferal.com/modules/conversation/>
            SELECT $this ?contribution ?conversation WHERE {
                $this (convo:threadOfConversation|^convo:hasThread) ?conversation .
                { $this convo:hasThreadContribution ?contribution . }
                UNION
                { ?contribution convo:inThread $this . }
                FILTER NOT EXISTS {
                    ?contribution (convo:contributionOf|^convo:hasContribution) ?conversation .
                }
            }
        """ ;
    ] .

convo:AcyclicReplyShape
    a sh:NodeShape ;
    rdfs:label "acyclic reply shape"@en ;
    rdfs:comment "Rejects any contribution that can reach itself by following one or more reply edges."@en ;
    sh:targetClass convo:Contribution ;
    sh:sparql [
        sh:message "The reply relation must be acyclic in the Threaded Conversation Profile."@en ;
        sh:select """
            PREFIX convo: <https://ontology.inferal.com/modules/conversation/>
            SELECT $this WHERE { $this convo:inReplyTo+ $this . }
        """ ;
    ] .

convo:CrossConversationReplyWarningShape
    a sh:NodeShape ;
    rdfs:label "cross-conversation reply warning shape"@en ;
    rdfs:comment "Warns about, but does not prohibit, reply edges whose endpoints have no shared conversation membership."@en ;
    sh:targetClass convo:Contribution ;
    sh:severity sh:Warning ;
    sh:sparql [
        sh:message "This reply crosses conversation boundaries; verify that it intentionally resumes or references another interaction."@en ;
        sh:select """
            PREFIX convo: <https://ontology.inferal.com/modules/conversation/>
            SELECT $this ?parent WHERE {
                $this convo:inReplyTo ?parent .
                FILTER NOT EXISTS {
                    $this (convo:contributionOf|^convo:hasContribution) ?conversation .
                    ?parent (convo:contributionOf|^convo:hasContribution) ?conversation .
                }
            }
        """ ;
    ] .
