@prefix gg:   <https://grindograph.darkcoff.ee/vocab#> .
@prefix ggx:  <https://grindograph.darkcoff.ee/proposed#> .
@prefix app:  <https://darkcoff.ee/grindhog/app#> .
@prefix rdf:  <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix owl:  <http://www.w3.org/2002/07/owl#> .
@prefix sh:   <http://www.w3.org/ns/shacl#> .
@prefix xsd:  <http://www.w3.org/2001/XMLSchema#> .
@prefix dct:  <http://purl.org/dc/terms/> .
@prefix prov: <http://www.w3.org/ns/prov#> .

<https://grindograph.darkcoff.ee/vocab>
    a owl:Ontology ;
    dct:title "Grindograph vocabulary" ;
    dct:description """Terms for community-contributed coffee bag observations and for
structured feedback on the Grindhog bean schema. Bean facts themselves reuse app: from
Grindhog; this vocabulary covers only what app: has no term for — the observation
apparatus, and the schema-feedback apparatus.""" ;
    dct:license <http://creativecommons.org/publicdomain/zero/1.0/> ;
    rdfs:seeAlso <https://darkcoff.ee/grindhog/app#> .


# ============================================================================
# Observation apparatus
# ============================================================================

gg:Submission
    a rdfs:Class ;
    rdfs:label "Submission" ;
    rdfs:comment """One coffee bag observed by one contributor. Each submission owns a
named graph; everything asserted about the bag lives in it, which is what makes
provenance, approval and export per-submission operations.""" .

gg:notPrinted
    a rdf:Property ;
    rdfs:label "not printed" ;
    rdfs:domain app:Bean ;
    rdfs:comment """States that a property is DEFINITELY ABSENT from the bag, as opposed
to merely unrecorded. RDF's open-world assumption has no native way to say this, and the
distinction is load-bearing: ground truth that cannot say "the label does not name a
region" can only score an extractor's misses, never its hallucinations. Objects are the
properties themselves, e.g. `gg:notPrinted app:region, app:variety`.""" .

gg:printedRoastDate
    a rdf:Property ;
    rdfs:label "printed roast date" ;
    rdfs:domain app:Bean ;
    rdfs:range xsd:date ;
    rdfs:comment """The roast date literally printed on the bag. Deliberately distinct
from app:roastDate, which downstream consumers may DERIVE (Grindhog infers it from a
best-before date by subtracting a year). Recording the observation rather than the
inference means the derivation rule can change and the whole corpus re-derives without
anyone re-reading a bag.""" .

gg:printedBestBefore
    a rdf:Property ;
    rdfs:label "printed best-before date" ;
    rdfs:domain app:Bean ;
    rdfs:range xsd:date ;
    rdfs:comment "The best-before / expiry date literally printed on the bag." .

gg:printedProcess
    a rdf:Property ;
    rdfs:label "printed process" ;
    rdfs:domain app:Bean ;
    rdfs:range xsd:string ;
    rdfs:comment """The processing method exactly as the bag prints it, kept alongside
app:process rather than instead of it. app:process holds the value MAPPED onto the
allowed list, which for anything outside it is "Other" — a bag reading "Carbonic
Maceration" would otherwise survive only as a SchemaNote, and the bean node itself would
have lost what the label said.

Same principle as gg:printedRoastDate: record the observation, derive the inference. When
the allowed list eventually grows, every affected case can be re-derived from this
property without anyone re-reading a bag.""" .

gg:printedRoastDegree
    a rdf:Property ;
    rdfs:label "printed roast degree" ;
    rdfs:domain app:Bean ;
    rdfs:range xsd:string ;
    rdfs:comment "The roast level exactly as the bag prints it. See gg:printedProcess." .

gg:photoFile
    a rdf:Property ;
    rdfs:label "photo file" ;
    rdfs:domain app:Bean ;
    rdfs:range xsd:string ;
    rdfs:comment """Filename of the published photo within the release bundle. The
storage location is operational and deliberately not part of the dataset.""" .

gg:describes
    a rdf:Property ;
    rdfs:label "describes" ;
    rdfs:domain gg:Submission ;
    rdfs:range app:Bean ;
    rdfs:comment "Links a submission to the bean it observed." .

gg:hasNote
    a rdf:Property ;
    rdfs:label "has note" ;
    rdfs:domain gg:Submission ;
    rdfs:range gg:SchemaNote ;
    rdfs:comment """Links a submission to the schema objections raised while labelling it.
The inverse of gg:provokedBy; both directions are asserted so the graph is navigable
without a reasoner.""" .

gg:primaryPhoto
    a rdf:Property ;
    rdfs:label "primary photo" ;
    rdfs:domain app:Bean ;
    rdfs:range xsd:string ;
    rdfs:comment """The single photo the labels were read from, and therefore the only one
exported as an extraction test case. Supplementary photos stay in the dataset as context
but are never scored against, so a field readable only on the back of a bag cannot make a
case unfairly unpassable.""" .


# ============================================================================
# Schema feedback apparatus
# ============================================================================

gg:SchemaNote
    a rdfs:Class ;
    rdfs:label "Schema note" ;
    rdfs:comment """A structured objection to the current schema, raised while labelling a
specific bag. Anchored to the submission that provoked it via gg:provokedBy, so every
claim about the schema is traceable to an artifact — a photo — that demonstrates it.""" .

gg:target
    a rdf:Property ;
    rdfs:label "target" ;
    rdfs:domain gg:SchemaNote ;
    rdfs:comment "The property the note is about (an app: term, or a proposed ggx: term)." .

gg:kind
    a rdf:Property ;
    rdfs:label "kind" ;
    rdfs:domain gg:SchemaNote ;
    rdfs:range gg:NoteKind ;
    rdfs:comment "What sort of objection this is. Closed vocabulary, so notes aggregate." .

gg:detail
    a rdf:Property ;
    rdfs:label "detail" ;
    rdfs:domain gg:SchemaNote ;
    rdfs:range xsd:string ;
    rdfs:comment """Literal supporting text — for MissingEnumValue, exactly what the bag
says; for ProposeField, the proposed name.""" .

gg:provokedBy
    a rdf:Property ;
    rdfs:label "provoked by" ;
    rdfs:domain gg:SchemaNote ;
    rdfs:range gg:Submission ;
    rdfs:comment "The submission whose bag prompted this note." .

gg:NoteKind
    a rdfs:Class ;
    rdfs:label "Note kind" ;
    rdfs:comment "The closed set of objections a contributor can raise." .

gg:MissingEnumValue
    a gg:NoteKind ;
    rdfs:label "Missing enum value" ;
    rdfs:comment """The allowed value list does not contain what the bag says. gg:detail
carries the literal text. Aggregating these yields sh:in entries directly.""" .

gg:NeedsMultipleValues
    a gg:NoteKind ;
    rdfs:label "Needs multiple values" ;
    rdfs:comment """The bag lists several values where the property admits one. Yields an
sh:maxCount correction — the defect app:variety has on any two-variety bag.""" .

gg:Ambiguous
    a gg:NoteKind ;
    rdfs:label "Ambiguous" ;
    rdfs:comment "The bag's wording does not map cleanly onto this property." .

gg:ShouldSplit
    a gg:NoteKind ;
    rdfs:label "Should split" ;
    rdfs:comment "The property conflates two things that bags report separately." .

gg:NotOnBags
    a gg:NoteKind ;
    rdfs:label "Not on bags" ;
    rdfs:comment "Rarely actually printed; a candidate for removal from the schema." .

gg:ProposeField
    a gg:NoteKind ;
    rdfs:label "Propose field" ;
    rdfs:comment "A property the schema lacks entirely. gg:detail carries the proposed name." .


gg:ShapeProposal
    a rdfs:Class ;
    rdfs:label "Shape proposal" ;
    rdfs:comment """A contributor's answer to how a property should be MODELLED, stored as
a SHACL property-shape fragment (sh:path with sh:datatype, sh:maxCount, sh:nodeKind …).
Usage frequency shows that a property is wanted but cannot say whether altitude is a
decimal, a range, or a string carrying units; this is where that is answered. Because the
answers are already SHACL, the released shape is generated by aggregating them rather
than transcribed by hand.""" .

gg:proposalFor
    a rdf:Property ;
    rdfs:label "proposal for" ;
    rdfs:domain gg:ShapeProposal ;
    rdfs:comment "The class whose shape this fragment contributes to (normally app:Bean)." .


# ============================================================================
# Validation apparatus
# ============================================================================

gg:ValidationReport
    a rdfs:Class ;
    rdfs:label "Validation report" ;
    rdfs:comment """A stored pyshacl report for one submission against the BeanShape in
force at submission time. Advisory, never blocking: a bag that violates the shape IS the
evidence that the shape is wrong, so violations are recorded as feedback rather than used
to reject data. SHACL here measures the schema; it does not enforce it.""" .

gg:validatedAgainst
    a rdf:Property ;
    rdfs:label "validated against" ;
    rdfs:domain gg:ValidationReport ;
    rdfs:comment "Version identifier of the shape the submission was checked against." .

gg:reportFor
    a rdf:Property ;
    rdfs:label "report for" ;
    rdfs:domain gg:ValidationReport ;
    rdfs:range gg:Submission ;
    rdfs:comment "The submission this report concerns." .


# ============================================================================
# Proposed-term quarantine
# ============================================================================

<https://grindograph.darkcoff.ee/proposed>
    a owl:Ontology ;
    rdfs:label "Grindograph proposed terms" ;
    rdfs:comment """Quarantine namespace. A predicate a contributor invents becomes real
data immediately, but must never be mistaken for vocabulary the project endorses, so it is
minted under ggx: rather than app:. Promotion is a deliberate act: a ggx: term that earns
enough evidence is added to Grindhog's kg/terms.ts as an app: term with a shape
constraint, and an owl:equivalentProperty triple keeps already-published data
resolvable.""" .
