@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix ph: <https://id.pedalhidrografi.co/terms#> .
@prefix dcterms: <http://purl.org/dc/terms/> .
@prefix prov: <http://www.w3.org/ns/prov#> .
@prefix schema: <https://schema.org/> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
@prefix geo: <http://www.opengis.net/ont/geosparql#> .
@prefix sf: <http://www.opengis.net/ont/sf#> .
@prefix pav: <http://purl.org/pav/> .
@prefix exif: <http://www.w3.org/2003/12/exif/ns#> .
@prefix nfo: <http://www.semanticdesktop.org/ontologies/2007/03/22/nfo#> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .

# Mídia visual — base abstrata ph:Image. Restrições COMPARTILHADAS por foto e
# vídeo. Sob inference=rdfs (que o backend usa ao validar), toda instância
# `a ph:StillImage` / `a ph:MotionImage` é inferida `a ph:Image`, então este
# shape dispara nas duas subclasses sem duplicação.
ph:VisualMediaShape a sh:NodeShape ;
    sh:targetClass ph:Image ;
    sh:property [
        sh:path dcterms:date ;
        sh:severity sh:Warning ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:datatype xsd:dateTime ;
        sh:message "Ideal: mídia com um único timestamp (dcterms:date). Sem ele, a mídia não participa de filtros/navegação temporais."@pt ;
    ] ;
    sh:property [
        sh:path schema:locationCreated ;
        sh:severity sh:Warning ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:class schema:GeoCoordinates ;
        sh:message "Ideal: mídia com uma única coordenada GPS (schema:locationCreated → schema:GeoCoordinates). Sem ela, a mídia não vira marcador no mapa (ver ph:GeoreferencedImage)."@pt ;
    ] ;
    sh:property [
        sh:path ph:capturedDuring ;
        sh:severity sh:Warning ;
        sh:minCount 1 ;
        sh:class ph:Tour ;
        sh:message "Ideal: associar a mídia a um passeio."@pt ;
    ] ;
    sh:property [
        sh:path prov:wasAttributedTo ;
        sh:severity sh:Warning ;
        sh:minCount 1 ;
        sh:class schema:Person ;
        sh:message "Mídia de autora (prov:wasAttributedTo)."@pt ;
    ] ;
    sh:property [
        sh:path dcterms:license ;
        sh:severity sh:Warning ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:nodeKind sh:IRI ;
        sh:message "Mídia precisa de exatamente uma licença (IRI, ex.: CC BY-SA 4.0)."@pt ;
    ] ;
    sh:property [
        sh:path pav:providedBy ;
        sh:severity sh:Warning ;
        sh:minCount 1 ;
        sh:class schema:Person ;
        sh:message "Mídia precisa de proveniência (pav:providedBy)."@pt ;
    ] ;
    sh:property [
        sh:path schema:isPartOf ;
        sh:severity sh:Warning ;
        sh:nodeKind sh:IRI ;
        sh:class schema:Collection ;
        sh:message "Se declarada, a lista (schema:isPartOf) deve ser schema:Collection."@pt ;
    ] .

# Foto (ph:StillImage) — restrições específicas de imagem estática (direção,
# focal, hash, flags de anonimização/compressão). Herda as regras de mídia via
# ph:VisualMediaShape acima.
ph:StillImageShape a sh:NodeShape ;
    sh:targetClass ph:StillImage ;
    sh:property [
        sh:path exif:gpsImgDirection ;
        sh:severity sh:Warning ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:datatype xsd:decimal ;
        sh:minInclusive 0 ;
        sh:maxExclusive 360 ;
        sh:message "Ideal: declarar direção (exif:gpsImgDirection, 0–360°)."@pt ;
    ] ;
    sh:property [
        sh:path exif:focalLengthIn35mmFilm ;
        sh:severity sh:Warning ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:datatype xsd:decimal ;
        sh:minExclusive 0 ;
        sh:maxInclusive 100000 ;
        sh:message "Ideal: declarar distância de focal equivalente a 35mm"@pt ;
    ] ;
    sh:property [
        sh:path nfo:hasHash ;
        sh:severity sh:Warning ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:class nfo:FileHash ;
        sh:message "Ideal: declarar hash do arquivo (nfo:FileHash)."@pt ;
    ] ;
    sh:property [
        sh:path ph:anonymized ;
        sh:severity sh:Warning ;
        sh:maxCount 1 ;
        sh:datatype xsd:boolean ;
        sh:message "Se declarado, ph:anonymized deve ser booleano."@pt ;
    ] ;
    sh:property [
        sh:path ph:compressed ;
        sh:severity sh:Warning ;
        sh:maxCount 1 ;
        sh:datatype xsd:boolean ;
        sh:message "Se declarado, ph:compressed deve ser booleano."@pt ;
    ] .

# Vídeo (ph:MotionImage) — restrições específicas de imagem em movimento
# (duração, resoluções, áudio, thumbnail). Herda as regras de mídia via
# ph:VisualMediaShape acima.
ph:MotionImageShape a sh:NodeShape ;
    sh:targetClass ph:MotionImage ;
    sh:property [
        sh:path schema:duration ;
        sh:severity sh:Violation ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:datatype xsd:duration ;
        sh:message "Vídeo precisa de duração (schema:duration, xsd:duration ex.: PT12.34S)."@pt ;
    ] ;
    sh:property [
        sh:path ph:availableResolution ;
        sh:severity sh:Violation ;
        sh:minCount 1 ;
        sh:datatype xsd:string ;
        sh:in ( "audio" "360p" "480p" "720p" "1080p" ) ;
        sh:message "Vídeo precisa declarar ao menos uma resolução em ph:availableResolution (audio/360p/480p/720p/1080p)."@pt ;
    ] ;
    sh:property [
        sh:path ph:audio ;
        sh:severity sh:Violation ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:datatype xsd:string ;
        sh:message "Vídeo precisa de um arquivo de áudio (ph:audio)."@pt ;
    ] ;
    sh:property [
        sh:path ph:video360p ;
        sh:severity sh:Warning ;
        sh:maxCount 1 ;
        sh:datatype xsd:string ;
        sh:message "Se declarado, ph:video360p deve ser único."@pt ;
    ] ;
    sh:property [
        sh:path ph:video720p ;
        sh:severity sh:Warning ;
        sh:maxCount 1 ;
        sh:datatype xsd:string ;
        sh:message "Se declarado, ph:video720p deve ser único."@pt ;
    ] ;
    sh:property [
        sh:path schema:thumbnail ;
        sh:severity sh:Warning ;
        sh:maxCount 1 ;
        sh:datatype xsd:string ;
        sh:message "Ideal: declarar schema:thumbnail (caminho relativo a web/clips/) pra marker do mapa."@pt ;
    ] .

# Mídia georreferenciada (ph:GeoreferencedImage) — o SUBCONJUNTO das mídias que
# vira marcador de foto/vídeo no mapa. Pra essas, a coordenada deixa de ser
# ideal e vira obrigatória (a base ph:Image só emite Warning). Os emitters
# (upload_images.html, build-clips.py) acrescentam este tipo quando a mídia tem
# GPS; o lat/long em si é validado por ph:GeoCoordinatesShape abaixo.
ph:GeoreferencedImageShape a sh:NodeShape ;
    sh:targetClass ph:GeoreferencedImage ;
    sh:property [
        sh:path schema:locationCreated ;
        sh:severity sh:Violation ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:class schema:GeoCoordinates ;
        sh:message "Mídia georreferenciada exige exatamente uma coordenada GPS (schema:locationCreated → schema:GeoCoordinates)."@pt ;
    ] .

ph:TourShape a sh:NodeShape ;
    sh:targetClass ph:Tour ;
    sh:property [
        sh:path dcterms:title ;
        sh:severity sh:Violation ;
        sh:datatype xsd:string ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:message "Passeio precisa de nome (dcterms:title)."@pt ;
    ] ;
    sh:property [
        sh:path dcterms:date ;
        sh:severity sh:Violation ;
        sh:datatype xsd:dateTime ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:message "Passeio precisa de data-hora (dcterms:date)."@pt ;
    ] ;
    sh:property [
        # Slug legível da URL /passeio/<slug> — mintado pelo servidor a
        # partir do título na criação (imutável); opcional no envio.
        sh:path schema:identifier ;
        sh:severity sh:Violation ;
        sh:datatype xsd:string ;
        sh:maxCount 1 ;
        sh:pattern "^[a-z0-9][a-z0-9-]*$" ;
        sh:message "schema:identifier (slug da URL) deve ser minúsculo, só [a-z0-9-] — normalmente é mintado pelo servidor; não precisa enviar."@pt ;
    ] ;
    sh:property [
        sh:path ph:linkRoute ;
        sh:severity sh:Warning ;
        sh:minCount 1 ;
        sh:class ph:RouteReference ;
        sh:message "Passeio precisa de uma referência a rota (ph:linkRoute → ph:RouteReference)."@pt ;
    ] ;
    sh:property [
        sh:path schema:organizer ;
        sh:severity sh:Info ;
        sh:nodeKind sh:IRI ;
        sh:message "Se declarado, schema:organizer deve apontar para uma Pessoa/Organização."@pt ;
    ] ;
    sh:property [
        sh:path ph:inSeriesEdition ;
        sh:severity sh:Warning ;
        sh:minCount 1 ;
        sh:class ph:SeriesEdition ;
        sh:message "Passeio precisa de edição de série (ph:inSeriesEdition → ph:SeriesEdition)."@pt ;
    ] ;
    sh:property [
        sh:path prov:wasAttributedTo ;
        sh:severity sh:Info ;
        sh:class schema:Person ;
        sh:message "Se declarado, prov:wasAttributedTo deve apontar para schema:Person."@pt ;
    ] ;
    sh:property [
        sh:path pav:providedBy ;
        sh:severity sh:Info ;
        sh:class schema:Person ;
        sh:message "Se declarado, pav:providedBy deve apontar para schema:Person."@pt ;
    ] ;
    sh:property [
        sh:path ph:linkInstagram ;
        sh:severity sh:Warning ;
        sh:maxCount 1 ;
        sh:datatype xsd:anyURI ;
        sh:message "Ideal: declarar link do Instagram."@pt ;
    ] ;
    sh:property [
        sh:path ph:linkActivity ;
        sh:severity sh:Warning ;
        sh:datatype xsd:anyURI ;
        sh:message "Se declarada, cada atividade gravada (ph:linkActivity) deve ser uma URL."@pt ;
    ] ;
    sh:property [
        sh:path ph:countAttendee ;
        sh:severity sh:Warning ;
        sh:maxCount 1 ;
        sh:datatype xsd:integer ;
        sh:minInclusive 0 ;
        sh:message "Ideal: número de participantes (ph:countAttendee)."@pt ;
    ] ;
    sh:property [
        sh:path ph:countNewcomer ;
        sh:severity sh:Warning ;
        sh:maxCount 1 ;
        sh:datatype xsd:integer ;
        sh:minInclusive 0 ;
        sh:message "Ideal: número de iniciantes (ph:countNewcomer)."@pt ;
    ] ;
    sh:property [
        sh:path ph:energyEstimate ;
        sh:severity sh:Violation ;
        sh:maxCount 1 ;
        sh:datatype xsd:decimal ;
        sh:minInclusive 0 ;
        sh:message "ph:energyEstimate deve ser um literal numérico (xsd:decimal) em kJ — não mais aceito como IRI/QuantityValue."@pt ;
    ] ;
    sh:property [
        sh:path ph:measuredEnergy ;
        sh:severity sh:Violation ;
        sh:maxCount 1 ;
        sh:datatype xsd:decimal ;
        sh:minInclusive 0 ;
        sh:message "ph:measuredEnergy deve ser um literal numérico (xsd:decimal) em kJ — não mais aceito como IRI/QuantityValue."@pt ;
    ] ;
    sh:property [
        sh:path ph:departedAt ;
        sh:severity sh:Warning ;
        sh:maxCount 1 ;
        sh:datatype xsd:dateTime ;
        sh:message "Ideal: horário de saída (ph:departedAt)."@pt ;
    ] ;
    sh:property [
        sh:path ph:arrivedAt ;
        sh:severity sh:Warning ;
        sh:maxCount 1 ;
        sh:datatype xsd:dateTime ;
        sh:message "Ideal: horário de chegada (ph:arrivedAt)."@pt ;
    ] ;
    sh:property [
        sh:path ph:departureLocation ;
        sh:severity sh:Warning ;
        sh:maxCount 1 ;
        sh:datatype xsd:string ;
        sh:message "Ideal: local de saída em texto livre (ph:departureLocation)."@pt ;
    ] ;
    sh:property [
        sh:path ph:arrivalLocation ;
        sh:severity sh:Warning ;
        sh:maxCount 1 ;
        sh:datatype xsd:string ;
        sh:message "Ideal: local de chegada em texto livre (ph:arrivalLocation)."@pt ;
    ] ;
    sh:property [
        sh:path ph:movingDuration ;
        sh:severity sh:Warning ;
        sh:maxCount 1 ;
        sh:datatype xsd:duration ;
        sh:message "Ideal: tempo em movimento (ph:movingDuration)."@pt ;
    ] ;
    sh:property [
        sh:path ph:totalDuration ;
        sh:maxCount 1 ;
        sh:datatype xsd:duration ;
        sh:message "Se declarado, ph:totalDuration deve ser um xsd:duration (ex.: PT3H35M) — e só faz sentido sem ph:departedAt+ph:arrivedAt, que o derivam."@pt ;
    ] ;
    sh:property [
        sh:path ph:hadIncident ;
        sh:severity sh:Warning ;
        sh:or ( [ sh:datatype xsd:string ] [ sh:datatype rdf:langString ] ) ;
        sh:message "Se declarado, ph:hadIncident deve ser texto, com ou sem tag de idioma (repetível)."@pt ;
    ] ;
    sh:property [
        sh:path ph:hadBonde ;
        sh:severity sh:Warning ;
        sh:maxCount 1 ;
        sh:datatype xsd:boolean ;
        sh:message "Se declarado, ph:hadBonde deve ser booleano."@pt ;
    ] ;
    sh:property [
        sh:path ph:hadRain ;
        sh:severity sh:Warning ;
        sh:maxCount 1 ;
        sh:datatype xsd:boolean ;
        sh:message "Se declarado, ph:hadRain deve ser booleano."@pt ;
    ] ;
    # Listas nominais de participantes / iniciantes (opcionais — só
    # aparecem quando a feature de "listar attendees" está ligada nas
    # Ajustes do app).
    sh:property [
        sh:path schema:attendee ;
        sh:severity sh:Warning ;
        sh:class schema:Person ;
        sh:message "Cada participante nominal deve ser schema:Person."@pt ;
    ] ;
    sh:property [
        sh:path ph:hasNewcomer ;
        sh:severity sh:Warning ;
        sh:class schema:Person ;
        sh:message "Cada iniciante nominal deve ser schema:Person."@pt ;
    ] ;
    # Narrativa do chamado. Ficava FORA das shapes — por isso 32% dos
    # passeios não têm nenhuma. É o texto do anúncio, não um detalhe.
    sh:property [
        sh:path dcterms:description ;
        sh:severity sh:Warning ;
        sh:minCount 1 ;
        sh:or ( [ sh:datatype xsd:string ] [ sh:datatype rdf:langString ] ) ;
        sh:message "Passeio deveria ter narrativa (dcterms:description)."@pt ;
    ] ;
    # Arte do anúncio. Também ficava FORA das shapes — foi por isso que uma
    # IRI `http://localhost:8080/…`, assada por um backend de dev e depois
    # publicada, passou sem ninguém notar (PH/96).
    #
    # O host local é sh:Warning, NÃO sh:Violation, de propósito: quem
    # auto-hospeda o amora num laptop (STORAGE_BACKEND=local) tem localhost
    # como host legítimo, e uma Violation impediria essa pessoa de cadastrar
    # passeios com arte. Quem tranca a porta é o `audit-captura.py`, que
    # trata a IRI local como lacuna, e o painel, que a mostra em vermelho.
    sh:property [
        sh:path schema:image ;
        sh:severity sh:Warning ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:nodeKind sh:IRI ;
        sh:message "Passeio deveria ter arte de anúncio (schema:image) como IRI."@pt ;
    ] ;
    sh:property [
        sh:path schema:image ;
        sh:severity sh:Warning ;
        sh:not [ sh:pattern "^(https?://(localhost|127\\.0\\.0\\.1|0\\.0\\.0\\.0)|file:)" ] ;
        sh:message "Arte do anúncio aponta pra um host local — essa IRI não resolve pra ninguém fora da sua máquina. Configure PUBLIC_BASE_URL no backend e reenvie a arte."@pt ;
    ] ;
    sh:property [
        sh:path ph:mediaSweep ;
        sh:severity sh:Warning ;
        sh:maxCount 1 ;
        sh:class ph:MediaSweep ;
        sh:message "Passe de coleta deve apontar pra um ph:MediaSweep."@pt ;
    ] ;
    # Constraint cross-tour: a mesma pessoa não pode ser iniciante em
    # dois passeios da mesma série. Avalia sobre o grafo mesclado
    # (validador já mescla catálogo + TTL recebido), então enxerga
    # passeios previamente registrados.
    sh:sparql [
        sh:severity sh:Violation ;
        sh:message "Pessoa já é iniciante em outro passeio da mesma série."@pt ;
        sh:prefixes [
            sh:declare [ sh:prefix "ph"     ; sh:namespace "https://id.pedalhidrografi.co/terms#"^^xsd:anyURI ] ;
        ] ;
        sh:select """
            PREFIX ph: <https://id.pedalhidrografi.co/terms#>
            SELECT $this ?value ?series WHERE {
                $this ph:hasNewcomer ?value ;
                      ph:inSeriesEdition/ph:inEventSeries ?series .
                ?other ph:hasNewcomer ?value ;
                       ph:inSeriesEdition/ph:inEventSeries ?series .
                FILTER (?other != $this)
            }
        """ ;
    ] .

ph:RouteReferenceShape a sh:NodeShape ;
    sh:targetClass ph:RouteReference ;
    sh:property [
        sh:path schema:url ;
        sh:severity sh:Violation ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:nodeKind sh:IRI ;
        sh:message "Rota precisa de URL (schema:url) como IRI."@pt ;
    ] ;
    sh:property [
        sh:path schema:provider ;
        sh:severity sh:Violation ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:class schema:Organization ;
        sh:message "Rota precisa de provedor (schema:provider — ex.: ph:rwgps, ph:strava)."@pt ;
    ] .

# Passe de coleta de mídia — o nó pas:<slug>_sweep.
# A EXISTÊNCIA do nó já é o marcador ("o passe foi feito"), então a data é
# obrigatória: um passe sem data não informa nada que a mera presença do nó
# já não informasse. A contagem pode ser 0 — significa "fui buscar e ninguém
# tinha compartilhado nada", que é diferente de nó ausente ("não fui buscar").
ph:MediaSweepShape a sh:NodeShape ;
    sh:targetClass ph:MediaSweep ;
    sh:property [
        sh:path dcterms:date ;
        sh:severity sh:Violation ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:datatype xsd:dateTime ;
        sh:message "Passe de coleta precisa da data em que foi feito."@pt ;
    ] ;
    sh:property [
        sh:path ph:collectedFileCount ;
        sh:severity sh:Violation ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:datatype xsd:integer ;
        sh:minInclusive 0 ;
        sh:message "Passe de coleta precisa da contagem de arquivos (0 é válido e significativo)."@pt ;
    ] ;
    sh:property [
        sh:path ph:archiveFolder ;
        sh:severity sh:Warning ;
        sh:maxCount 1 ;
        sh:datatype xsd:string ;
        sh:message "Pasta do acervo deve ser o nome da pasta na Biblioteca Hidrográfica."@pt ;
    ] ;
    sh:property [
        sh:path ph:sweepContributor ;
        sh:severity sh:Warning ;
        sh:datatype xsd:string ;
        sh:message "Cada contribuinte do passe é o slug (handle) de quem compartilhou."@pt ;
    ] ;
    sh:property [
        sh:path prov:wasAssociatedWith ;
        sh:severity sh:Warning ;
        sh:maxCount 1 ;
        sh:class schema:Person ;
        sh:message "Quem passou o zap deve ser schema:Person."@pt ;
    ] .

# Edição de série (ph:SeriesEdition) — o IRI …/passeio/<ES>/<n> é a chave natural
# da edição. Cada edição é realizada por EXATAMENTE um passeio (via
# ph:inSeriesEdition, caminho inverso): uma colisão de numeração (dois pedais
# ambos "BP 4") deve virar edições distintas (ex.: BP/4 e BP/3-5), NÃO um nó
# compartilhado. NÃO se exige unicidade de (série, sequência) — a duplicata de
# sequência é legítima; o que não pode é uma edição servir a dois passeios.
ph:SeriesEditionShape a sh:NodeShape ;
    sh:targetClass ph:SeriesEdition ;
    sh:property [
        sh:path [ sh:inversePath ph:inSeriesEdition ] ;
        sh:severity sh:Violation ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:message "Cada edição de série deve ser realizada por exatamente um passeio (colisão de numeração → edição distinta, ex.: BP/3-5)."@pt ;
    ] ;
    sh:property [
        sh:path ph:inEventSeries ;
        sh:severity sh:Warning ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:class schema:EventSeries ;
        sh:message "Edição precisa da série a que pertence (ph:inEventSeries → schema:EventSeries)."@pt ;
    ] ;
    sh:property [
        sh:path ph:sequenceInSeries ;
        sh:severity sh:Warning ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        # xsd:integer + minInclusive, NÃO xsd:positiveInteger: um inteiro solto
        # em Turtle (`ph:sequenceInSeries 96`) é xsd:integer, e nenhum emissor
        # do repo escreve o typed literal. A shape exigia positiveInteger e por
        # isso reprovava TODAS as 114 edições do catálogo — uma constraint morta,
        # avisando sempre, que só ensinava a ignorar os avisos. Mesmo idioma de
        # ph:countAttendee/ph:countNewcomer.
        sh:datatype xsd:integer ;
        sh:minInclusive 1 ;
        sh:message "Edição precisa da posição na sequência (ph:sequenceInSeries), inteiro ≥ 1."@pt ;
    ] .

ph:GeoCoordinatesShape a sh:NodeShape ;
    sh:targetClass schema:GeoCoordinates ;
    sh:property [
        sh:path schema:latitude ;
        sh:severity sh:Violation ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:datatype xsd:decimal ;
        sh:minInclusive -90 ;
        sh:maxInclusive 90 ;
        sh:message "Coordenada precisa de latitude (-90 a 90)."@pt ;
    ] ;
    sh:property [
        sh:path schema:longitude ;
        sh:severity sh:Violation ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:datatype xsd:decimal ;
        sh:minInclusive -180 ;
        sh:maxInclusive 180 ;
        sh:message "Coordenada precisa de longitude (-180 a 180)."@pt ;
    ] .

ph:FileHashShape a sh:NodeShape ;
    sh:targetClass nfo:FileHash ;
    sh:property [
        sh:path nfo:hashAlgorithm ;
        sh:severity sh:Violation ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:datatype xsd:string ;
        sh:message "Hash precisa de algoritmo (nfo:hashAlgorithm)."@pt ;
    ] ;
    sh:property [
        sh:path nfo:hashValue ;
        sh:severity sh:Violation ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:datatype xsd:string ;
        sh:message "Hash precisa de valor (nfo:hashValue)."@pt ;
    ] .

# ph:QuantityValueShape e ph:EnergyEstimateShape foram removidas: a energia
# agora é um literal xsd:decimal (kJ) direto no passeio, validado pelas
# propriedades ph:energyEstimate / ph:measuredEnergy na ph:TourShape acima.
# A intensidade ('De boa'…'Insano') é derivada do valor por faixas fixas e
# computada nos leitores (censo.html / app.js) — não é mais armazenada.

# Pessoa (schema:Person) — primeiro shape a mirar pessoas; antes eram
# irrestritas. TUDO soft (Warning/Info), então nenhuma pessoa existente é
# rejeitada e uploads de passeio/mídia não falham por causa dela.
ph:PersonShape a sh:NodeShape ;
    sh:targetClass schema:Person ;
    sh:property [
        sh:path schema:alternateName ;
        sh:severity sh:Warning ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:datatype xsd:string ;
        sh:message "Pessoa precisa de um apelido/handle (schema:alternateName) — o rótulo curto."@pt ;
    ] ;
    sh:property [
        sh:path schema:name ;
        sh:severity sh:Info ;
        sh:maxCount 1 ;
        sh:datatype xsd:string ;
        sh:message "Ideal: nome real da pessoa (schema:name)."@pt ;
    ] ;
    sh:property [
        sh:path schema:url ;
        sh:severity sh:Info ;
        sh:maxCount 1 ;
        sh:nodeKind sh:IRI ;
        sh:message "Se declarada, a página pessoal (schema:url) deve ser um IRI."@pt ;
    ] ;
    sh:property [
        sh:path rdfs:seeAlso ;
        sh:severity sh:Info ;
        sh:nodeKind sh:IRI ;
        sh:message "Perfis/links relacionados (rdfs:seeAlso — Instagram, Mastodon, etc.) devem ser IRIs."@pt ;
    ] ;
    sh:property [
        sh:path schema:mainEntityOfPage ;
        sh:severity sh:Info ;
        sh:maxCount 1 ;
        sh:nodeKind sh:IRI ;
        sh:message "Página humana da pessoa (schema:mainEntityOfPage) deve ser um IRI."@pt ;
    ] .