@prefix sh:     <http://www.w3.org/ns/shacl#> .
@prefix lz:     <https://linked.zone/ns#> .
@prefix lzt:    <https://linked.zone/topic/> .
@prefix skos:   <http://www.w3.org/2004/02/skos/core#> .
@prefix dct:    <http://purl.org/dc/terms/> .
@prefix schema: <https://schema.org/> .
@prefix xsd:    <http://www.w3.org/2001/XMLSchema#> .

# Publish gate for linked.zone.
# Run in CI over data/linked-zone.ttl before build.py. A note that fails
# here does not get a URI, which is the point: URIs are promises.

lz:NoteShape
    a sh:NodeShape ;
    sh:targetClass lz:Note ;
    sh:name "Note" ;

    sh:property [
        sh:path schema:name ;
        sh:datatype xsd:string ;
        sh:minCount 1 ; sh:maxCount 1 ;
        sh:maxLength 70 ;
        sh:message "A note needs exactly one title, 70 characters or fewer — the length that survives a search result." ;
    ] ;

    sh:property [
        sh:path lz:thesis ;
        sh:datatype xsd:string ;
        sh:minCount 1 ; sh:maxCount 1 ;
        sh:minLength 40 ;
        sh:message "Every note states one claim in one sentence. If the thesis cannot be written, the note is not ready." ;
    ] ;

    sh:property [
        sh:path schema:abstract ;
        sh:datatype xsd:string ;
        sh:minCount 1 ; sh:maxCount 1 ;
        sh:minLength 80 ; sh:maxLength 320 ;
        sh:message "The abstract is the index dek and the meta description. 80–320 characters." ;
    ] ;

    sh:property [
        sh:path dct:subject ;
        sh:class skos:Concept ;
        sh:minCount 1 ; sh:maxCount 3 ;
        sh:message "One to three topics, and each must exist in the site scheme. Free-text tags are how a taxonomy dies." ;
    ] ;

    sh:property [
        sh:path lz:genre ;
        sh:class lz:Genre ;
        sh:minCount 1 ; sh:maxCount 1 ;
    ] ;

    sh:property [
        sh:path lz:status ;
        sh:class lz:EditorialStatus ;
        sh:minCount 1 ; sh:maxCount 1 ;
    ] ;

    sh:property [
        sh:path schema:author ;
        sh:minCount 1 ;
    ] .

# Topics must sit in the site's own scheme, not be borrowed ad hoc.
lz:TopicInSchemeShape
    a sh:NodeShape ;
    sh:targetObjectsOf dct:subject ;
    sh:property [
        sh:path skos:inScheme ;
        sh:hasValue lzt:scheme ;
        sh:minCount 1 ;
        sh:message "Topic is not in the linked.zone scheme. Add it there first, with an exactMatch to the vocabulary term it means." ;
    ] .

# A published note is dated. A draft must not be.
# Revised counts as published: it keeps its URI, so it keeps its date.
lz:PublishedNoteShape
    a sh:NodeShape ;
    sh:targetClass lz:Note ;
    sh:sparql [
        sh:message "A published or revised note needs exactly one schema:datePublished; a draft must not carry one." ;
        sh:prefixes lz:prefixes ;
        sh:select """
            PREFIX lz:     <https://linked.zone/ns#>
            PREFIX lzt:    <https://linked.zone/topic/>
            PREFIX schema: <https://schema.org/>
            SELECT $this WHERE {
                { $this lz:status ?s .
                  FILTER (?s IN (lzt:Published, lzt:Revised))
                  FILTER NOT EXISTS { $this schema:datePublished ?d } }
                UNION
                { $this lz:status ?s ;
                        schema:datePublished ?d1, ?d2 .
                  FILTER (?s IN (lzt:Published, lzt:Revised) && ?d1 != ?d2) }
                UNION
                { $this lz:status lzt:Draft ;
                        schema:datePublished ?d . }
            }
        """ ;
    ] ;
    sh:sparql [
        sh:message "A revised note needs a schema:dateModified later than its schema:datePublished — revision is versioned, not silent." ;
        sh:prefixes lz:prefixes ;
        sh:select """
            PREFIX lz:     <https://linked.zone/ns#>
            PREFIX lzt:    <https://linked.zone/topic/>
            PREFIX schema: <https://schema.org/>
            SELECT $this WHERE {
                $this lz:status lzt:Revised ; schema:datePublished ?p .
                FILTER NOT EXISTS {
                    $this schema:dateModified ?m .
                    FILTER (?m > ?p)
                }
            }
        """ ;
    ] .

# Thin-content guard: a note that only restates a docs page is a duplicate.
# If it points at a normative source it must still carry its own argument.
lz:NoDuplicateOfDocsShape
    a sh:NodeShape ;
    sh:targetSubjectsOf lz:normativeSource ;
    sh:property [
        sh:path lz:thesis ;
        sh:minLength 60 ;
        sh:message "This note cites a meta.linked.archi page as its source. It needs its own argument, not a summary of that page — the canonical stays on the docs side." ;
    ] .

lz:prefixes
    sh:declare [ sh:prefix "lz" ; sh:namespace "https://linked.zone/ns#"^^xsd:anyURI ] ,
               [ sh:prefix "lzt" ; sh:namespace "https://linked.zone/topic/"^^xsd:anyURI ] ,
               [ sh:prefix "schema" ; sh:namespace "https://schema.org/"^^xsd:anyURI ] .
