@prefix pan:     <https://repolex.ai/ontology/pan/> .
@prefix git-lex: <https://repolex.ai/ontology/git-lex/> .
@prefix subtexture: <https://repolex.ai/ontology/subtexture/> .
@prefix owl:     <http://www.w3.org/2002/07/owl#> .
@prefix rdfs:    <http://www.w3.org/2000/01/rdf-schema#> .
@prefix xsd:     <http://www.w3.org/2001/XMLSchema#> .

# =============================================================================
# PAN KIT ONTOLOGY — the media store's vocabulary
# =============================================================================
# Pan stores media, describes it with a graph, and searches it by graph pattern
# and vector similarity. This file is the WHOLE of what Pan may say about a
# stored object. Code implements this; it never adds to it — Rob ruled on
# 2026-09-03 that the ontology comes first and every fact Pan emits is declared here.
#
# Pan's parents: subtexture (the base ontology, subtexture/ontology/
# subtexture.ttl: Thing, Set, Sequence and the universals) and git-lex (the
# kit layer: git-lex:Thing sits under subtexture:Thing).
#
# Published to git-lex as the kit `repolex-ai/git-lex-kit-pan` so a soul that
# installs it can query its own media graph through git-lex and Syrinx.
# Instances are graph-only: nothing here is authored as a markdown file. They
# live in `<repo>/.pan/_ignore/oxigraph` (the `_ignore/` pocket law, Rob
# 2026-08-05), written by pand and by nothing else.
#
# WHERE THE DATA LIVES (Rob, 2026-08-25, unchanged): bulk model output does
# NOT ride inside the image. Each stage writes its own DATA FILE beside the
# media (caption/, sam3/, pose/, vectors/, thumbnail/) and the image carries
# one REFERENCE per output — an pan:Enrichment record naming the model, the
# file, how many items it holds and when it was produced. Every path is
# declared here; nothing is found by filesystem convention.
#
# WHAT BELONGS ON THE MEDIA OBJECT ITSELF (goodlux, 2026-09-08) — the test
# every property on pan:Media / pan:Image has to pass, one of:
#   (a) data Pan needs to function: the descriptions feed the embedding, the
#       scene objects are the segmentation prompts;
#   (b) information that should be to hand when viewing the file;
#   (c) links out to other data — the per-stage references (pose, regions,
#       vectors, the raw model answer);
#   (d) anything that must be searchable by graph pattern on the object.
# Everything else is a record of its own, reached through (c).
# =============================================================================

<https://repolex.ai/ontology/pan> a owl:Ontology ;
    rdfs:label "Pan media store ontology" ;
    rdfs:comment "Identity, location, timing and model-derived enrichment facts Pan records for stored media." ;
    owl:versionInfo "0.4.17" .

# -----------------------------------------------------------------------------
# CHANGELOG
# -----------------------------------------------------------------------------
# v0.4.17 (2026-09-21, ruled by goodlux): WHAT THE IMAGE IS.
#   Pan has hashed images since the store was written and never recorded the
#   answer. The hash lived inside tests, proving that writing metadata leaves
#   the image alone, and a stored file had no name for its own contents in the
#   graph or in itself.
#   + pan:imageSha256Hash on pan:Image. The sha256 of every block of the PNG
#   except its XMP, each taken as it sits on disk: the header, the pixel data,
#   the palette, the transparency, the colour profile, the generator's render
#   chunk, every other text block, in file order. Nothing is decoded and
#   nothing is normalised.
#   The XMP is left out because Pan writes it. Pan rewrites that block every
#   time a stage finishes, and a hash that moved whenever Pan touched its own
#   metadata would say nothing about the image. Leaving it out fixes the hash
#   from the moment the file is stored.
#   On pan:Image, not pan:Media: the rule "everything but the XMP" is a rule
#   about a PNG. What a video or an audio file would have to exclude to be
#   stable under Pan's own writes is a different question, and belongs to
#   whoever declares that hash.
#   NOT an identity. Identity is pan:id, assigned once. This answers whether
#   two files hold the same thing: the same image arriving again under another
#   name, from another store, or shared back.
#   Written at ingest by pand. A file whose blocks will not read is still
#   stored and simply carries no hash.
#   Removed with it: the rule that this hash match another program byte for
#   byte. That rule came from Pool, where the hash WAS the identity and three
#   languages had to agree on it; it is why alpha and the low byte of a 16-bit
#   sample were being thrown away. Identity moved to an assigned pan:id and
#   the rule should have gone then.
#   Deliberately not done here: comparing an arrival's contents with the
#   stored file's after a JPEG is converted. That stays in issue #23.
# v0.4.16 (2026-09-20, ruled by goodlux): IS THE BODY RIGHT.
#   0.4.15 added two scores and said anatomy was "a separate question and is
#   not ruled". It is ruled now: a third score and critique on the same
#   footing as the other two, asked for in the same caption call.
#   (1) + pan:imageAnatomyScore on pan:Image — 0 to 100, pan:ScoreValue, the
#       same scale and direction as the other two: HIGH is good. 100 is a body
#       with nothing wrong with it; a low score is extra fingers, a second
#       elbow, a limb that joins nowhere, a duplicated head.
#   (2) + pan:imageAnatomyCritique — one sentence naming the defect it saw, or
#       saying it saw none.
#   A score rather than true/false, raised as an option on 2026-09-19 and
#   settled here: generators fail anatomy by degree, so a threshold belongs to
#   whoever is querying, not to the fact.
#   The value applies to whatever body is in the frame — person or animal. An
#   image with no body in it omits both keys, as any inapplicable key is
#   omitted.
# v0.4.15 (2026-09-19, ruled by goodlux): HOW GOOD IS THE IMAGE.
#   The caption stage has described what is in an image since 0.3.4 and never
#   said anything about how good it is. Specced on 2026-09-13 (Pan expanded
#   scope, section 2.1) as part of the same single pass: the encoder has
#   already turned the pixels into tokens, so four more fields cost about sixty
#   output tokens instead of a second model run.
#   (1) + pan:imageTechnicalScore, + pan:imageAestheticScore on pan:Image —
#       0 to 100, declared as the bounded datatype pan:ScoreValue. Technical is
#       the craft: focus, noise, blown highlights. Aesthetic is the appeal:
#       composition, light, whether it is worth looking at.
#   (2) + pan:imageTechnicalCritique, + pan:imageAestheticCritique — one
#       sentence saying why each score is what it is. A number nobody can
#       argue with is a number nobody can trust.
#   (3) + pan:ScoreValue a rdfs:Datatype — xsd:decimal, 0 to 100 inclusive,
#       the same shape pan:RatingValue already uses for a person's 0 to 5.
#   The names are goodlux's, 2026-09-19: imageTechnicalScore,
#   imageAestheticScore, imageTechnicalCritique, imageAestheticCritique, over
#   the spec's aestheticTechnical / aestheticAppeal / technicalCritique /
#   appealCritique, so the four line up with each other.
#   Deliberately not done: a separate aesthetic model. Iris has one (ERNIE
#   Image Aesthetics on the Mac) and it is not called: one pass, one model,
#   four more keys in the prompt. Also not done: anything about anatomy — that
#   is a separate question and is not ruled.
# v0.4.14 (2026-09-19, ruled by goodlux): READY FOR WHAT?
#   pan:readyDate RENAMED pan:enrichmentCompleteDate. The name said an object
#   was "ready" without saying ready for what, and a reader had to find the
#   code to learn that it means every configured enrichment stage has recorded
#   a result for this object. That is what it has always meant and all it has
#   ever meant; only the name changes.
#   No alias for the old spelling: stored triples under pan:readyDate are test
#   data, and pand writes the new name from here on.
# v0.4.13 (2026-09-19, ruled by goodlux): ONE STRING, NOT A VOCABULARY.
#   0.4.11 and 0.4.12 turned the PNG render chunk into a class with a property
#   per setting, then into a class plus a name/value class for the settings a
#   roster could not hold. Both were wrong for the same reason: they built
#   Pan's ontology against one image generator's format, and that format is
#   whatever its extensions decide it is — 94 distinct keys across the built-in
#   ones alone, changing release to release.
#   (1) + pan:renderRequestInformation on pan:Media — the whole chunk as one
#       string, kept as written. The graph holds it so a reader need not open
#       the file; Pan does not take it apart.
#   (2) - pan:RenderRequest, - pan:RenderSetting, - pan:renderRequest,
#       - pan:renderSetting, - pan:settingName, - pan:settingValue and all
#       twenty-one render* properties. Deleted, not deprecated: nothing
#       authored them and pand no longer writes them.
#   Not searchable by field, deliberately. Finding every image from one seed
#   means reading the string, and that is the trade: Pan stores media and runs
#   perception, and is not a reader of any one generator's format.
# v0.4.12 (2026-09-19, ruled by goodlux): THE RENDER REQUEST, READ FROM THE
#   GENERATOR'S SOURCE RATHER THAN FROM SAMPLES.
#   0.4.11 declared twelve settings guessed from one image. The generator's own
#   code (sd-webui-forge-neo, modules/processing.py create_infotext and
#   modules/infotext_utils.py parse_generation_parameters, read 2026-09-19)
#   says the key set CANNOT be fixed: every script and extension adds its own
#   through p.extra_generation_params, and the built-in ones alone contribute
#   around fifty. A fixed roster would silently drop whatever it had not heard
#   of. So:
#   (1) + pan:RenderSetting a owl:Class ; rdfs:subClassOf pan:Node — one
#       setting from the chunk that Pan declares no property for, holding
#       pan:settingName (the key exactly as written, "Token merging ratio")
#       and pan:settingValue. + pan:renderSetting links a RenderRequest to
#       each. Nothing is dropped and no new vocabulary is needed when an
#       extension invents a key.
#   (2) The settings with a property of their own are the ones worth querying
#       by name, and now match the generator's spellings: + renderDistilledCfgScale,
#       renderVariationSeed, renderVariationSeedStrength, renderVaeEncoder,
#       renderVaeDecoder, renderLoraHashes, renderDiffusionPrecision,
#       renderHiresSteps, renderHiresCheckpoint. pan:renderModule takes
#       `Module 1`, `Module 2`, … one value each.
#   (3) - pan:renderScheduleType keeps its name; - the 0.4.11 guesses that the
#       source does not write are gone.
#   Parsing follows the generator exactly: the settings are the LAST line and
#   only if three or more pairs parse from it; a value in double quotes is a
#   JSON string, so a comma inside `Lora hashes: "a: 1, b: 2"` stays with its
#   value; a value shaped 1536x1536 is two numbers; a setting whose value
#   equals its key is written bare and is skipped.
#   Deliberately not done: LoRAs as nodes of their own. `Lora hashes` is one
#   string of name-to-hash pairs and stays one string until something needs to
#   query LoRAs. Also not done: re-reading images stored before this version.
# v0.4.11 (2026-09-19, ruled by goodlux): THE CALL THAT MADE THE IMAGE, AND
#   THE REQUEST THAT FOUND THE REGIONS.
#   (1) + pan:RenderRequest a owl:Class ; rdfs:subClassOf pan:Node — the
#       generation call a diffusion user interface wrote into the PNG's
#       `parameters` text chunk: prompt, negative prompt, steps, sampler,
#       schedule type, CFG scale, seed, size, checkpoint and its hash, the
#       modules, the random number source and the version. Pan has copied that
#       chunk through untouched since 0.3.0 and never read it, so "every image
#       with seed 1030317025" was a question the graph could not answer.
#       + pan:renderRequest on pan:Media links the image to it.
#   (2) + the fields: pan:renderPrompt, pan:renderNegativePrompt,
#       pan:renderSteps, pan:renderSampler, pan:renderScheduleType,
#       pan:renderCfgScale, pan:renderSeed, pan:renderModel,
#       pan:renderModelHash, pan:renderModule (one per value),
#       pan:renderRng, pan:renderVersion, pan:renderDenoisingStrength,
#       pan:renderHiresUpscaler, pan:renderClipSkip. Width and height reuse
#       pan:width / pan:height. + pan:renderParameters holds the WHOLE chunk
#       verbatim, so a key Pan does not declare is not a key Pan has lost; a
#       key earns its own property when someone needs to query it.
#   (3) + the segmentation request, on pan:RegionData: pan:requestNouns (the
#       comma-separated list Pan actually sent), pan:requestMinConfidence and
#       pan:requestPolygonVerts (the two numbers the node was given). A noun
#       that found nothing used to vanish — an image with no person region
#       could not say whether Pan asked and the model found none, or whether
#       Pan never asked. Now it can.
#   Deliberately not done: no class for LoRAs or image-to-image inputs as
#   nodes of their own. A LoRA arrives inside the settings line and is in
#   pan:renderParameters; when something needs to query LoRAs it gets a
#   property, or a class if it earns one. Also not done: reading the chunk
#   from formats other than PNG, and re-reading images stored before this
#   version (no unasked migration).
# v0.4.10 (2026-09-19, ruled by goodlux): WHICH WAY THE SUBJECT FACES, WHICH
#   PROMPT ASKED, AND WHERE THE SERVER'S OWN ANSWER WENT.
#   (1) + pan:sceneSubjectOrientation on pan:Image — which way the main
#       subject's body faces the camera: front, back, side, side-front,
#       side-back. A thirteenth scene field, asked of the caption model in the
#       same call as the others.
#   (2) + pan:modelPromptPath on pan:Node — the prompt file that asked for an
#       answer, as `custom/caption.md` or `default/caption.md`, relative to the
#       prompts directory. Written on the media object AND on the pan:Caption
#       record: the object says which prompt described it, and a second model
#       or a second prompt is a second record naming its own. Prompts live in
#       two folders from this version: `default/`, which pand rewrites only
#       when the prompt it ships differs from what is on disk, and `custom/`,
#       which Pan never writes. The config names which prompt a stage uses.
#   (3) + pan:modelReplyPath on pan:Enrichment — the model server's own answer,
#       saved whole beside the record. Pan already wrote these files for
#       segmentation, depth and embedding; nothing in the graph named them, so
#       a reader could not find them and a sweep could not tell them from
#       litter. The reference's pan:path names the record; pan:modelReplyPath names
#       the server's answer. At most one; absent when a stage saves none.
#   Deliberately not done: no enumeration of the permitted scene values in
#   OWL, and no validation in pand. The prompt lists the values the caption
#   model may answer with (the coded gaze forms, gaze-looking-at-viewer and
#   the rest, and the five orientations); Pan stores what comes back. Judging
#   a model's answer is not the media store's job (goodlux, 2026-09-19).
#   Also not done: storing the prompt TEXT, or a hash of it. The path is the
#   pointer; versions are separate files. One name serves every stage's
#   answer file, since a stage returns one.
# v0.4.9 (2026-09-18, ruled by goodlux): A STORE ID IS SIX CHARACTERS.
#   (1) The pan:Store section comment said the store's id is "the soul's
#       genesis SHA". It is the FIRST SIX CHARACTERS of that SHA: 700c5b.
#       That sentence is where a forty-character identifier got its authority,
#       and pand wrote the whole hash into <pan/Store/...>, into the Instance's
#       pan:primaryGraph endpoint, into every /stores/<id>/ route and into the
#       log lines, while the media folder on disk was six characters all along.
#       Long identifiers everywhere are the fault Pan was rewritten to undo.
#   (2) A bare store's configured storage_id is cut the same way, so ~/.pan's
#       forty zeros are the id 000000.
#   Comment only: no class, property, domain, range or cardinality changes.
#   Deliberately not done: no rule about which characters an id may contain
#   (hex or otherwise) and no rejection of an id that is not a genesis hash —
#   nobody has ruled on that, so the first six characters are taken as given.
#   Code: pand cuts the id once, where a store is resolved, so nothing
#   downstream can lengthen it; a caller naming a store by the whole SHA is
#   still answered. A store node left behind under a long id is removed when
#   the store is opened.
# v0.4.8 (2026-09-18, ruled by goodlux): CAPTIONS ARE CAPTIONS.
#   (1) pan:shortDescription RENAMED pan:shortCaption; pan:longDescription
#       RENAMED pan:longCaption. A text the caption stage makes by captioning
#       is a caption, not a description. "description" is git-lex's word
#       (git-lex:description, a summary a person writes) and pan:description
#       is its file spelling; the two names were colliding in meaning.
#   Deliberately not done: pan:description is untouched; stored triples under
#   the old names are not migrated (the stores were wiped on 2026-09-17 and
#   the caption stage rewrites its facts on the next run). The caption prompt
#   (~/.config/pan/prompts/caption.md) keys its JSON by these names and was
#   changed with them.
# v0.4.7 (2026-09-17, ruled by goodlux): MEDIASET AND IMAGESET REFACTOR.
#   (1) + pan:MediaSet a owl:Class ; rdfs:subClassOf subtexture:Set , pan:Node —
#       an unordered set of Pan media of any kind (image, audio, video) a person
#       curates; own file on disk; no ordering. Declared so media kinds get their
#       own set classes (UI handlers differ per kind).
#   (2) pan:Photoset RENAMED pan:ImageSet ; rdfs:subClassOf pan:MediaSet —
#       a MediaSet holding only pan:Image (enforced at add time in code).
#   (3) + pan:VideoSet, pan:AudioSet a owl:Class ; rdfs:subClassOf pan:MediaSet —
#       stubs declared so the set hierarchy is complete before any UI reads it;
#       not yet implemented (no pan:Video / pan:Audio media class exists yet
#       and nothing writes one).
#   (4) Cardinality restrictions (pan:id 1, pan:createdDate 1, pan:description max 1)
#       placed on pan:MediaSet only; subclasses (ImageSet, VideoSet, AudioSet)
#       inherit them.
#   (5) Section header and comments updated: imagesets/<id>.xml on disk,
#       IRI <pan/ImageSet/<id>>.
# v0.4.6 (2026-09-17, ruled by goodlux): PAN SPELLS EVERY FACT PAN:.
#   (1) Pan spells every fact pan:, graph and file alike. The git-lex spelling
#       was the 0.3.6 SHACL-generator workaround and the generator is being
#       fixed to follow owl:equivalentProperty.
#   (2) Cardinality restrictions on pan:Node, pan:Store, pan:Media, pan:Photoset,
#       pan:Instance, pan:Enrichment, pan:Region, pan:Pose, pan:Caption,
#       pan:Embedding, pan:Thumbnail, pan:Depth now restrict pan:id (was git-lex:id).
#   (3) Cardinality restrictions on pan:Media and pan:Photoset now restrict
#       pan:createdDate (was git-lex:createdDate).
#   (4) Cardinality restriction on pan:Photoset now restricts pan:description
#       (was git-lex:description).
#   (5) No class parents change. pan:id retains owl:equivalentProperty
#       subtexture:id, git-lex:id.
# v0.4.5 (2026-09-16, ruled by goodlux; pan issue #24): MONOCULAR DEPTH ENRICHMENT.
#   (1) + pan:depthData on pan:Image — reference to one depth run's output file
#       holding its pan:Depth record. A plain Enrichment like poseData/captionData.
#   (2) + pan:Depth a owl:Class ; rdfs:subClassOf pan:Node — one monocular depth
#       estimate: an 8-bit normalized depth map PNG for the whole image with its
#       raw min/max range.
#   (3) + pan:depthMapPath, pan:depthMin, pan:depthMax on pan:Depth. width/height
#       and model/precision/provider/producedDate reuse existing pan:Node terms.
# v0.4.4 (2026-09-16, ruled by goodlux): PAN:NODE AND PROPERTY SCOPING.
#   (1) + pan:Node a owl:Class ; rdfs:subClassOf subtexture:Thing, git-lex:Thing.
#       Abstract base class for all Pan nodes. Pan stands alone while bridging
#       cleanly into subtexture:Thing.
#   (2) Every Pan class (pan:Store, pan:Media, pan:Instance, pan:Enrichment,
#       pan:Region, pan:Pose, pan:Caption, pan:Embedding, pan:Thumbnail,
#       pan:Photoset) subclasses pan:Node.
#   (3) SCOPE PROPERTIES TO PAN: pan:id, pan:createdDate, pan:description,
#       pan:width, pan:height, pan:model, pan:precision, pan:provider,
#       pan:producedDate, pan:path domain moved from git-lex:Thing to pan:Node.
#       These fields are scoped to Pan and no longer pollute the universal Thing.
#   (4) pan:id retains owl:equivalentProperty subtexture:id, git-lex:id and
#       rdfs:range git-lex:Thing (angle-bracket identifier shape).
# v0.4.3 (2026-09-16, ruled by goodlux): THE SOURCE FILE.
#   + pan:sourceFile on pan:Media — the file this source PNG was made from,
#     relative to pan:mediaRoot. The managed store keeps ONE working format,
#     PNG; a JPEG, WebP or TIFF that arrives is kept as delivered under
#     img/original/ and never read again, and the PNG made from it is the
#     source. sourceFile names that original. A PNG arrival points at the
#     source itself, so the field is always present and one rule holds.
#   Structural: written by pand at ingest, never by hand, refused by pan set.
#   Not done: carrying the arrival's ICC/EXIF/XMP into the PNG (issue #23).
# v0.4.2 (2026-09-16, ruled by goodlux): PHOTOSET EXTENDS subtexture:Set.
#   (1) pan:Photoset rdfs:subClassOf subtexture:Set — the new base ontology
#       (subtexture.ttl 0.1.0, 2026-09-16) declares Set and Sequence under
#       subtexture:Thing; Pan extends the base directly, not git-lex:Set.
#   (2) pan:member and pan:inPhotoset DELETED. Membership is the universal:
#       the image carries pan:relatedToId <pan/Photoset/id> in its XMP (the
#       file spelling of git-lex:relatedToId, one value per set). The
#       Photoset node carries only id, description and createdDate; its
#       members are read from the images.
#   (3) The Photoset cardinality block keeps its restrictions on the git-lex
#       universals: git-lex:Thing is under subtexture:Thing and the universals
#       are bridged by owl:equivalentProperty in subtexture.ttl.
#   Not done: nothing in git-lex-kit-base changes here.
# v0.4.1 (2026-09-16, ruled by goodlux; pan issue #5): THE INSTANCE.
#   + pan:Instance — one pand deployment as a node: its primary graph, an
#     optional pan-ui cache graph, its storage root, its mode (managed or
#     referenced), its source media format and its port. Six properties,
#     named exactly as the expanded-scope doc of 2026-09-13 names them:
#     pan:primaryGraph, pan:localGraph, pan:fsRoot, pan:sourceFormat,
#     pan:instanceMode, pan:listenPort.
#   Declared ahead of code: nothing in pand writes an Instance yet; pand will
#   write one Instance node per daemon at start in a later release.
#   Left open: how pan:Store relates to pan:Instance (a daemon serves many
#   stores). No link is declared until that is ruled.
# v0.4.0 (2026-09-16, ruled by goodlux): PHOTOSETS.
#   (1) + pan:Photoset, a subclass of git-lex:Set (base kit 0.18.0): an
#       unordered set of media a person curates. Each set has its own file on
#       disk, photosets/<id>.xml, so the graph is rebuilt from files alone.
#   (2) + pan:member — the file spelling of git-lex:member (Set → Media).
#   (3) + pan:inPhotoset on pan:Media — one value per set the media belongs
#       to; rides in the image XMP as a bag of <pan/Photoset/id>, so
#       membership travels with the file.
#   (4) + pan:description — the file spelling of git-lex:description, same
#       pattern as pan:id and pan:createdDate (owl:equivalentProperty).
#   (5) pan:createdDate domain widened from pan:Media to git-lex:Thing so a
#       Photoset carries it.
#   Deliberately not done: pan:sequence — one image in two sets has two
#   positions, so a single integer on the image cannot express order; if
#   order is ever wanted it is the order of pan:member values in the set's
#   file. Also not done: pan:coverImage, pan:tag, pan:itemCount (not asked
#   for; nothing reads them). The photoset file writer, the `pan photoset`
#   commands and the rebuild pass are code, tracked in pan issue #4.
# v0.3.9 (2026-09-16, ruled by goodlux): every Pan date is named <what>Date,
#   type at the end, like producedDate and readyDate. pan:dateCreated is
#   RENAMED pan:createdDate. The base kit renames its universals the same
#   day (git-lex:dateCreated → git-lex:createdDate, git-lex:dateUpdated →
#   git-lex:updatedDate; base kit 0.18.0), so pan:createdDate is
#   owl:equivalentProperty git-lex:createdDate and the graph now stores
#   git-lex:createdDate. The Media cardinality restriction moves with it and
#   stays on the universal (the SHACL generator reads owl:onProperty literally).
#   No alias for the old spelling in the reader: files written before this
#   version are test data and are wiped before switchover.
# v0.3.8 (2026-09-16, ruled by goodlux; two rulings landed together):
#   (1) HOW AN IMAGE LINKS ITS RECORDS — option B of the record-link question
#       (pan issue #18). The image links ONLY to the references
#       (captionData, vectorData, poseData, regionData); each reference links
#       the records its file holds with ONE property, pan:item. The four
#       direct record links pan:captionItem, pan:embedding, pan:pose and
#       pan:region are DELETED (deleted, not deprecated: predicates come from
#       the writer, and pand no longer writes them). A data file now opens
#       with its reference node listing pan:item links, then each record in
#       full; the image's XMP is unchanged (references only). Reading a
#       caption's text is one hop longer: ?img pan:captionData ?d . ?d
#       pan:item ?c . ?c pan:text ?t. The shape is the one the file already
#       had, so file and graph agree, and the name that was open since v0.2
#       (captionItem) is gone rather than settled.
#   (2) THREE CURATION FACTS A PERSON SETS ON THE MEDIA: pan:rating (integer
#       0..5, declared as the bounded datatype pan:RatingValue), pan:isPicked
#       and pan:isRejected (booleans). At most one each. They ride in the
#       image's XMP like every pan fact, so a rating travels with the file
#       and needs no second database. The command that writes them is pan
#       issue #19; this version only declares them.
#   Deliberately not done: pan:tag. Machine tags belong in the git-lex or
#   subtexture namespace, not here; a field for loose words earns nothing.
#   Also not done: putting regions themselves in the image's XMP (considered,
#   ruled against: the image links to the region data, as before).
#
# v0.3.7 (2026-09-16, ruled by goodlux, the same evening; final word on count):
#   (1) pan:count exists for REGIONS ONLY and lives on a class of its own.
#       pan:RegionData (subclass of pan:Enrichment) is the reference to one
#       segmentation run's file, the range of pan:regionData, and the only
#       reference that carries a count: pan:count has domain pan:RegionData
#       and cardinality 1 there, and is gone from the pan:Enrichment block.
#       Caption and vector references name one file holding one answer,
#       nothing to count; pose skeletons are counted by reading the file.
#       pand types the regionData node pan:RegionData and writes no count on
#       captionData, vectorData or poseData, in the graph or the XMP. This
#       replaces 0.3.6 (3), which had kept a count on poseData.
#   (2) The thumbnail reference in the file carries its id. Every reference
#       bag entry in the XMP already wrote <pan:id>&lt;pan/Enrichment/…&gt;;
#       the pan:thumbnail struct wrote path, width and height only, although
#       the Thumbnail node in the graph has had git-lex:id all along. The
#       struct now opens with <pan:id>&lt;pan/Thumbnail/…&gt;. No ontology
#       statement changes for this; pan:id already applies to every node
#       (0.3.6). Recorded here because the file's shape changed.
#   Deliberately not done: the record-link redesign (#18) and everything
#   0.4.0 (#5); the other shared record fields keep their Thing domain.
#
# v0.3.6 (2026-09-16, ruled by goodlux, same day, reading one stored file):
#   (1) pan:id now applies to EVERY Pan node, not only media: its domain moves
#       from pan:Media to git-lex:Thing. Each reference in a media file's XMP
#       (captionData, poseData, regionData, vectorData entries) already carries
#       <pan:id>&lt;pan/Enrichment/xxxxxxxx&gt;</pan:id>, and every record in a
#       data file (Caption, Embedding, Pose, Region) carries its own pan:id; the
#       graph stores git-lex:id on each. The ruling: a reference is addressable
#       on its own, so its id is a declared field beside model, producedDate
#       and path — not a convention the writer happens to follow.
#   (2) Identity is REQUIRED on every class: each cardinality block gains
#       `git-lex:id cardinality 1`. Restricted under the git-lex spelling, not
#       pan:id, because the SHACL generator (git-lex/src/shacl.rs) reads
#       owl:onProperty literally and does not follow owl:equivalentProperty;
#       the pan:Media block already restricts git-lex:dateCreated the same way.
#   Found while verifying, not fixed here: the pan:thumbnail struct in the
#   XMP carries path, width and height but no pan:id, although the Thumbnail
#   node in the graph has one. Code change, not ontology; tracked separately.
#   (3) pan:count is no longer required on every reference: cardinality 1
#       becomes maxCardinality 1 on pan:Enrichment. It is written only where
#       one model run yields many records in one file — regionData (one
#       Region per thing SAM3 found) and poseData (one skeleton per person).
#       A caption or vector reference names one file holding one answer and
#       carries no count. pand stops writing it there. Its domain narrows
#       from git-lex:Thing to pan:Enrichment: the reference is the only class
#       that carries a count. The other shared fields (model, precision,
#       provider, producedDate, path) keep their Thing domain in this bump;
#       they are a separate question.
#   Deliberately not done: the record-link redesign (#18) and everything
#   0.4.0 (#5).
#
# v0.3.5 (2026-09-16, ruled by goodlux; corrections only, nothing new said):
#   (1) pan:id, pan:dateCreated, pan:relatedToId are declared as THE SAME
#       PROPERTY as git-lex:id, git-lex:dateCreated, git-lex:relatedToId
#       (owl:equivalentProperty — the "sameAs" ruling in its form for
#       properties; owl:sameAs itself is for individuals). A media file
#       carries only the pan and copia namespaces (ruled 2026-09-08), so the
#       file spells these facts pan:; the graph stores the git-lex universal.
#       The two spellings are one fact. pand already translated on read;
#       until now the file's spelling was undeclared here.
#   (2) The pan:caption maxCardinality line in the pan:Media block, left over
#       from the 0.3.4 deletion, is removed.
#   (3) pan:importedFrom DELETED: its reader (the migration code) was removed
#       on 2026-09-07 and nothing in src/ names it.
#   (4) pan:captionItem's comment no longer claims pan:caption exists.
#   Deliberately not done: the record-link redesign (how a media object holds
#   its Caption/Pose/Region/Embedding records, pan issue #18) and every 0.4.0
#   addition (pan issue #5). Both wait for their rulings.
#
# v0.3.4 (2026-09-08, ruled by goodlux after a night of reading stored files):
#   (1) pan:shortDescription, pan:longDescription on pan:Media — any media
#       gets described; the long one is what the embedding is built from.
#   (2) pan:sceneObjects on pan:Image — one value per physical thing visible,
#       the segmentation prompts. Image only: meaningless for audio or video.
#   (3) The twelve scene fields (pan:sceneCamera … pan:sceneLocation) on
#       pan:Image — the same names the caption prompt has asked for since
#       September, now stored as facts instead of lines in a text blob.
#   (4) pan:caption DELETED: the two descriptions replace it. The Caption
#       record (pan:text) keeps the model's whole answer verbatim.
#   Deliberately not done: a scene graph from the language model (objects
#   with boxes, attributes, relationships). Tried on one image 2026-09-08:
#   good names and relations, coarse boxes, a third of the objects SAM3
#   finds. Descriptions + nouns → SAM3 gives better data; relationships can
#   be read out of the long description later by any language model.
#   Also not done: dc:description. Pan's own fields are visible in viewers
#   now (ExifTool config declares every one), so the duplicate is dropped.
#
# v0.3.3 (2026-09-05, ruled by Rob looking at a stored file in a viewer):
#   (1) pan:createdDate DELETED. A pan:Image is a git-lex:Thing, and when it
#       came to be is the universal git-lex:dateCreated — Pan writes that, in
#       the image and in the graph, the way every kit class inherits it. The
#       cardinality-1 restriction moves onto the inherited property. A
#       producer's git-lex:dateCreated about THE FILE (Horae wrote its Moment's
#       date there) is no longer loaded onto the Image; about its own subject
#       it still is. Stores migrate at open.
#   (2) Identifiers in files are angle-bracket form, <pan/Image/id>, the same
#       text every kit writes in frontmatter; the IRI exists only in the graph.
#   Deliberately not done: renaming anything. No property here was ever
#   prefixed "image" — the "Image Media Path" a viewer showed came from an XMP
#   wrapper struct, removed the same day, not from this vocabulary.
#
# v0.3.2 (2026-09-05, ruled by Rob to m3rc and confirmed to w4r3z: one embedding
#         index for the Mac and the Salad node, named qwen3-vl-embedding-2b;
#         precision and provider are data on the record, not part of the name):
#   (1) pan:precision, pan:provider — data on a model-produced record saying
#       how (bf16-cuda, 8-bit) and where (salad, phala, mac) the model ran.
#       pan:model stays the functional label = the index name; two runs of
#       the same network at different precisions share it (cosine 0.998).
#
# v0.3.1 (2026-09-04, ruled by Rob to nomia and confirmed to w4r3z: Horae
#         sends pan:relatedToId=<copia/Moment/id>):
#   (1) pan:relatedToId — the one field a PRODUCER writes in Pan's namespace:
#       the Moment (or any Thing) this stored media relates to, written the
#       git-lex way as a reference <ns/Class/id> in the file's XMP; Pan
#       resolves it to the IRI in the graph. It is the transitional link that
#       keeps an image joined to its Moment until the CoPIA store exists.
#       Every other pan: statement arriving in a file is still held back (a
#       previous store's paths and records are not facts about this copy).
#   NOT done: git-lex:relatedToId as the edge. w4r3z shipped it for a few hours
#       on a reading of Rob's words that Rob did not make; reverted the same
#       night. The relay lesson is recorded, not the property.
#
# v0.3.0 (2026-09-03, DRAFT for Rob's ruling — nothing below is implemented
#         until he rules; Rob's directions of the same evening, w4r3z-pan):
#   (1) PAN IS A KIT. Every class now `rdfs:subClassOf git-lex:Thing`, so the
#       universal properties apply and git-lex can read the graph. Identity is
#       the UNIVERSAL git-lex:id (strategy (b), Rob-ruled 2026-08-23 for new
#       classes): the Thing's IRI is <pan/Image/k7m2p9x4>, written once at
#       ingest. pan:id is DELETED — it was a second identity for the same node.
#       The bare identifier is the last path segment of git-lex:id, derived,
#       never stored twice.
#   (2) + pan:Store — the store itself as a node, so the ONE fact a reader
#       needs that lives outside the media graph — where the media files are —
#       is declared, not inferred. Needed because a soul's media may live on
#       an external volume (`/Volumes/p02/_pan/<genesis>/media`) while the
#       graph stays in the repo. git-lex resolves pan:mediaPath against
#       pan:mediaRoot; without it a media path is a string nobody can open.
#   (3) TIME — all "Date" properties are date-times, RFC3339 in SYSTEM LOCAL
#       time (Rob: Pool used UTC; system time across the board). pan:createdAt
#       RENAMED pan:createdDate — when Pan created the record and its id.
#       + pan:readyDate on Media: set once every configured stage has a
#       record; absent means still in the pipeline. + pan:producedDate on
#       every enrichment record and reference. Readers: pipeline tracking
#       (Horae, CoPIA, Syrinx: is it ready, since when, how long per stage).
#       The MOMENT's own creation and render times are copia facts and arrive
#       in the copia block; Pan does not restate them.
#   (4) THE COPIA BLOCK RIDES IN THE IMAGE XMP. The producer (Horae) hands Pan
#       well-formed RDF/XML in copia vocabulary alongside the bytes; Pan
#       writes it into the image's XMP packet as the copia-namespace
#       Description(s), verbatim, and loads its triples unchanged. Standard
#       RDF-in-XMP, nothing invented: no Pan class describes it, no separate
#       file holds it. Malformed XML or non-RDF = the delivery is rejected.
#   (5) THE IMAGE KEEPS EVERYTHING IT ARRIVED WITH. Pan writes its own pan:
#       block and the copia block into the image XMP, and NEVER strips what
#       was already there: every other chunk (sdapi parameters, EXIF, prior
#       XMP Descriptions) is preserved byte-for-byte; pixels are untouched.
#       Pool stripped existing metadata; that is the nightmare this reverses.
#   DELIBERATELY NOT ADDED: any pan: property for perception scene tags
#   (sceneObjects, mood, lighting, …) — they are copia vocabulary and are
#   written as copia data, never re-declared under pan:. pan:storageId (store
#   scope is ambient; pan:Store carries identity). Audio/Video/Pdf classes —
#   not yet needed.
#   NAMING LEFT OPEN for Rob: pan:captionItem (placeholder since v0.2).
#
# v0.2.0 (2026-08-26, Rob-ruled over six demo rounds): enrichment layer —
#   Region/Pose/Caption/Embedding/Thumbnail as first-class nodes, model as
#   DATA (pan:model) never as a name, data files beside the blob with one
#   reference each, pan:id replacing panId.
# v0.1.0 (2026-07-16): identity + location + provenance.
# -----------------------------------------------------------------------------

# -----------------------------------------------------------------------------
# THE BASE NODE
# -----------------------------------------------------------------------------
# pan:Node is the abstract root for all entities in the Pan media store.
# Subclasses subtexture:Thing (and git-lex:Thing) so every Pan entity is a
# subtexture Thing. Properties shared across Pan classes (pan:id, model,
# dimensions, paths) have domain pan:Node, preventing them from polluting the
# universal Thing across other kits.
# -----------------------------------------------------------------------------

pan:Node a owl:Class ;
    rdfs:subClassOf subtexture:Thing , git-lex:Thing ;
    rdfs:label "Node" ;
    rdfs:comment "Abstract root entity for the Pan media store; sits under subtexture:Thing." .

# -----------------------------------------------------------------------------
# THE STORE
# -----------------------------------------------------------------------------
# One per .pan directory. IRI <pan/Store/<id>> where <id> is the FIRST SIX
# CHARACTERS of the soul's genesis SHA (a repo store) or of the configured
# store id (a bare store) — the same identity git-lex, Horae and Syrinx
# already use for the soul, cut to six: 700c5b. Six characters everywhere
# (goodlux, 2026-09-18): the IRI, the daemon's routes, the log lines and the
# media folder on the volume are one value, and the whole hash appears
# nowhere.
# -----------------------------------------------------------------------------

pan:Store a owl:Class ;
    rdfs:subClassOf pan:Node ;
    rdfs:label "Store" ;
    rdfs:comment "One Pan store: the .pan directory of one soul or one bare store directory. Written by pand at open; never authored." .

pan:mediaRoot a owl:DatatypeProperty ;
    rdfs:label "media root" ;
    rdfs:comment "Absolute directory every pan:mediaPath and pan:path in this store is relative to. Written by pand from its config; do not edit by hand." ;
    rdfs:domain pan:Store ;
    rdfs:range xsd:string .

# -----------------------------------------------------------------------------
# MEDIA
# -----------------------------------------------------------------------------

pan:Media a owl:Class ;
    rdfs:subClassOf pan:Node ;
    rdfs:label "Media" ;
    rdfs:comment "A stored media object. Pan's facts and the producer's copia block ride in its XMP; everything it arrived with is kept. IRI <pan/Image/id>." .

pan:Image a owl:Class ;
    rdfs:subClassOf pan:Media ;
    rdfs:label "Image" ;
    rdfs:comment "Media whose bytes are a raster image. Region and pose enrichment are facts about pixels and are declared on Image, not Media." .

pan:mediaPath a owl:DatatypeProperty ;
    rdfs:label "media path" ;
    rdfs:comment "Location of the bytes, relative to the store's pan:mediaRoot. Written by pand; never a guess from a naming scheme." ;
    rdfs:domain pan:Media ;
    rdfs:range xsd:string .

pan:mediaType a owl:DatatypeProperty ;
    rdfs:label "media type" ;
    rdfs:comment "MIME type of the stored bytes, e.g. image/png." ;
    rdfs:domain pan:Media ;
    rdfs:range xsd:string .

# The file the source was made from. Under img/original/ when the arrival was
# not PNG; the source's own pan:mediaPath when it was. Always present.
pan:sourceFile a owl:DatatypeProperty ;
    rdfs:label "source file" ;
    rdfs:comment "Path of the file this source was made from, relative to pan:mediaRoot; the source's own path when it arrived as PNG. Never an absolute path; never omitted." ;
    rdfs:domain pan:Media ;
    rdfs:range xsd:string .

# What the image IS: the sha256 of every block of the PNG except the XMP,
# which Pan writes and rewrites. Everything else counts — pixels, palette,
# transparency, colour profile, the generator's render chunk, every text
# block. Identity stays pan:id; this answers "is this the same image", so
# one reshared or re-delivered can be recognised (goodlux, 2026-09-21).
pan:imageSha256Hash a owl:DatatypeProperty ;
    rdfs:label "image sha256 hash" ;
    rdfs:comment "sha256 of the whole image file except its XMP. Written by pand at ingest; not an identity and not settable by hand." ;
    rdfs:domain pan:Image ;
    rdfs:range xsd:string .

# The request that made the image, kept whole and not taken apart. A
# diffusion user interface writes its entire call into a PNG text chunk;
# Pan copies that chunk into the stored file and also keeps its text here, so
# the graph holds it without opening the file. Pan does not parse it: a field
# per setting would tie this ontology to one generator's format, and that
# format is whatever its extensions decide it is this week (goodlux,
# 2026-09-19).
pan:renderRequestInformation a owl:DatatypeProperty ;
    rdfs:label "render request information" ;
    rdfs:comment "The generation request the file arrived carrying, verbatim: prompt, sampler, seed, checkpoint and the rest as the generator wrote them. One string; do not parse it into the graph." ;
    rdfs:domain pan:Media ;
    rdfs:range xsd:string .

pan:enrichmentCompleteDate a owl:DatatypeProperty ;
    rdfs:label "enrichment complete date" ;
    rdfs:comment "When every configured enrichment stage had recorded a result for this object, RFC3339 local time. Absent means a stage is still owed. Written once by pand, never by hand." ;
    rdfs:domain pan:Media ;
    rdfs:range xsd:dateTime .

# width/height apply to Media AND Thumbnail; domain is the shared parent Node.
pan:width a owl:DatatypeProperty ;
    rdfs:label "width" ;
    rdfs:comment "Pixel width." ;
    rdfs:domain pan:Node ;
    rdfs:range xsd:integer .

pan:height a owl:DatatypeProperty ;
    rdfs:label "height" ;
    rdfs:comment "Pixel height." ;
    rdfs:domain pan:Node ;
    rdfs:range xsd:integer .

pan:shortCaption a owl:DatatypeProperty ;
    rdfs:label "short caption" ;
    rdfs:comment "One sentence saying what this media shows. Written by the caption stage; do not write it by hand." ;
    rdfs:domain pan:Media ;
    rdfs:range xsd:string .

pan:longCaption a owl:DatatypeProperty ;
    rdfs:label "long caption" ;
    rdfs:comment "A detailed, literal caption of everything the media shows. The embedding is built from the image and this text together. Written by the caption stage." ;
    rdfs:domain pan:Media ;
    rdfs:range xsd:string .

# One value per object, a bag: `?s pan:sceneObjects "hand"` finds every
# image with a hand. In XMP it is an rdf:Bag; in the answer the caption
# stage reads, it is a JSON list.
pan:sceneObjects a owl:DatatypeProperty ;
    rdfs:label "scene objects" ;
    rdfs:comment "Every distinct physical thing visible, one value each (person, hand, hair, rock). The segmentation prompts. Repeat the value, not the key." ;
    rdfs:domain pan:Image ;
    rdfs:range xsd:string .

# The scene fields: one short phrase each, asked of the caption model in the
# same call as the descriptions. Image only.
pan:sceneCamera a owl:DatatypeProperty ;
    rdfs:label "scene camera" ;
    rdfs:comment "camera angle: low angle, eye level, overhead. One short phrase." ;
    rdfs:domain pan:Image ;
    rdfs:range xsd:string .

pan:sceneFraming a owl:DatatypeProperty ;
    rdfs:label "scene framing" ;
    rdfs:comment "shot framing: close-up, full body, wide. One short phrase." ;
    rdfs:domain pan:Image ;
    rdfs:range xsd:string .

pan:scenePosture a owl:DatatypeProperty ;
    rdfs:label "scene posture" ;
    rdfs:comment "body posture: standing, hunched, seated. One short phrase." ;
    rdfs:domain pan:Image ;
    rdfs:range xsd:string .

pan:sceneGaze a owl:DatatypeProperty ;
    rdfs:label "scene gaze" ;
    rdfs:comment "where the subject looks: at viewer, away, down. One short phrase." ;
    rdfs:domain pan:Image ;
    rdfs:range xsd:string .

pan:sceneExpression a owl:DatatypeProperty ;
    rdfs:label "scene expression" ;
    rdfs:comment "facial expression: calm, alert, sad. One short phrase." ;
    rdfs:domain pan:Image ;
    rdfs:range xsd:string .

pan:sceneAction a owl:DatatypeProperty ;
    rdfs:label "scene action" ;
    rdfs:comment "what is happening: reading, reaching, still. One short phrase." ;
    rdfs:domain pan:Image ;
    rdfs:range xsd:string .

pan:sceneEnergy a owl:DatatypeProperty ;
    rdfs:label "scene energy" ;
    rdfs:comment "energy level: quiet, tense, dynamic. One short phrase." ;
    rdfs:domain pan:Image ;
    rdfs:range xsd:string .

pan:sceneMood a owl:DatatypeProperty ;
    rdfs:label "scene mood" ;
    rdfs:comment "overall mood: serene, ominous, playful. One short phrase." ;
    rdfs:domain pan:Image ;
    rdfs:range xsd:string .

pan:sceneLighting a owl:DatatypeProperty ;
    rdfs:label "scene lighting" ;
    rdfs:comment "lighting: soft dusk, harsh, backlit. One short phrase." ;
    rdfs:domain pan:Image ;
    rdfs:range xsd:string .

pan:sceneStyle a owl:DatatypeProperty ;
    rdfs:label "scene style" ;
    rdfs:comment "visual style: painterly, photoreal, surreal. One short phrase." ;
    rdfs:domain pan:Image ;
    rdfs:range xsd:string .

pan:sceneMedium a owl:DatatypeProperty ;
    rdfs:label "scene medium" ;
    rdfs:comment "medium: photo, oil, charcoal, 3D render. One short phrase." ;
    rdfs:domain pan:Image ;
    rdfs:range xsd:string .

pan:sceneLocation a owl:DatatypeProperty ;
    rdfs:label "scene location" ;
    rdfs:comment "where: studio, cliffside, server room. One short phrase." ;
    rdfs:domain pan:Image ;
    rdfs:range xsd:string .

# Which way the body faces, not where the eyes go — sceneGaze is the eyes.
# The permitted values live in the caption prompt, not here: the prompt is
# what the model reads (goodlux, 2026-09-19).
pan:sceneSubjectOrientation a owl:DatatypeProperty ;
    rdfs:label "scene subject orientation" ;
    rdfs:comment "which way the main subject's body faces the camera: front, back, side, side-front, side-back. One of those five." ;
    rdfs:domain pan:Image ;
    rdfs:range xsd:string .

# --- identity and time, as spelled inside a media file -------------------------
# A media file carries only the pan and copia namespaces. The graph stores the
# pan: spelling, same as the file, pan:id being the universal via
# owl:equivalentProperty. owl:equivalentProperty says they are ONE property under
# two names: the sameAs ruling (goodlux, 2026-09-16) in the form OWL gives
# properties. pan:id is not media-only: every reference and record node
# (Enrichment, Caption, Embedding, Pose, Region, Thumbnail, Depth) carries its
# own (goodlux, 2026-09-16), so its domain is pan:Node.

pan:id a owl:ObjectProperty ;
    owl:equivalentProperty subtexture:id , git-lex:id ;
    rdfs:label "id" ;
    rdfs:comment "Which Thing this node IS, written <pan/Class/id>; the file spelling of git-lex:id. On media and on every record and reference. Never write the bare token." ;
    rdfs:domain pan:Node ;
    rdfs:range git-lex:Thing .

pan:createdDate a owl:DatatypeProperty ;
    owl:equivalentProperty subtexture:createdDate , git-lex:createdDate ;
    rdfs:label "created date" ;
    rdfs:comment "When Pan created this record, RFC3339 system local time; the file spelling of git-lex:createdDate. Written by pand, never by hand." ;
    rdfs:domain pan:Node ;
    rdfs:range xsd:dateTime .

# The file spelling of git-lex:description (same pattern as pan:id): a Pan
# file carries only pan and copia, so the universal is written under pan:.
pan:description a owl:DatatypeProperty ;
    owl:equivalentProperty git-lex:description ;
    rdfs:label "description" ;
    rdfs:comment "A short deliberate summary a person writes; the file spelling of git-lex:description. One value; overwrite it, do not repeat the key." ;
    rdfs:domain pan:Node ;
    rdfs:range xsd:string .

# The prompt that asked for an answer. On the media object (which prompt
# described this image) and on the Caption record beside it (which prompt
# produced THIS answer), so a second model or a second prompt is a second
# record naming its own. Two folders hold prompts: `default/`, rewritten from
# the pand binary at every start, and `custom/`, which Pan never writes.
pan:modelPromptPath a owl:DatatypeProperty ;
    rdfs:label "prompt path" ;
    rdfs:comment "The prompt file that asked for this answer, as full-caption.default.md, relative to the prompts directory. Written by pand; a changed prompt is a new file, never an edit." ;
    rdfs:domain pan:Node ;
    rdfs:range xsd:string .

# --- how good the image is, as the caption model sees it (goodlux, 2026-09-19) --
# Written by the caption stage in the same call as the captions and the scene
# fields. Three numbers and the sentence behind each: a score with no reason
# attached is a number nobody can argue with, and so a number nobody can
# trust. These are the MODEL's judgement; pan:rating is a person's.

pan:imageTechnicalScore a owl:DatatypeProperty ;
    rdfs:label "image technical score" ;
    rdfs:comment "How well made the image is, 0 to 100: focus, exposure, noise, artefacts. Written by the caption stage; do not set it by hand." ;
    rdfs:domain pan:Image ;
    rdfs:range pan:ScoreValue .

pan:imageTechnicalCritique a owl:DatatypeProperty ;
    rdfs:label "image technical critique" ;
    rdfs:comment "One sentence saying why the technical score is what it is, naming what it saw." ;
    rdfs:domain pan:Image ;
    rdfs:range xsd:string .

pan:imageAestheticScore a owl:DatatypeProperty ;
    rdfs:label "image aesthetic score" ;
    rdfs:comment "How good the image is to look at, 0 to 100: composition, light, whether it holds attention. Written by the caption stage; do not set it by hand." ;
    rdfs:domain pan:Image ;
    rdfs:range pan:ScoreValue .

pan:imageAestheticCritique a owl:DatatypeProperty ;
    rdfs:label "image aesthetic critique" ;
    rdfs:comment "One sentence saying why the aesthetic score is what it is, naming what it saw." ;
    rdfs:domain pan:Image ;
    rdfs:range xsd:string .

pan:imageAnatomyScore a owl:DatatypeProperty ;
    rdfs:label "image anatomy score" ;
    rdfs:comment "How anatomically correct the bodies are, 0 to 100, high is good: 100 has nothing wrong. Written by the caption stage; do not set it by hand." ;
    rdfs:domain pan:Image ;
    rdfs:range pan:ScoreValue .

pan:imageAnatomyCritique a owl:DatatypeProperty ;
    rdfs:label "image anatomy critique" ;
    rdfs:comment "One sentence naming the anatomical defect the anatomy score is for, or saying none was seen." ;
    rdfs:domain pan:Image ;
    rdfs:range xsd:string .

# --- curation: facts a person sets on the media (goodlux, 2026-09-16) ----------
# Written by `pan set`, never by a model. They ride in the image's XMP so a
# rating travels with the file; no second database. At most one value each.
# Deliberately absent: a tag field — machine tags belong to git-lex or
# subtexture, not to the media store.

# 0 to 100, the scale the caption model is asked for. Same shape as
# pan:RatingValue, different scale and different author: a score is the
# model's judgement, a rating is a person's.
pan:ScoreValue a rdfs:Datatype ;
    owl:onDatatype xsd:decimal ;
    owl:withRestrictions ( [ xsd:minInclusive 0 ] [ xsd:maxInclusive 100 ] ) .

pan:RatingValue a rdfs:Datatype ;
    owl:onDatatype xsd:integer ;
    owl:withRestrictions ( [ xsd:minInclusive 0 ] [ xsd:maxInclusive 5 ] ) .

pan:rating a owl:DatatypeProperty ;
    rdfs:label "rating" ;
    rdfs:comment "Star rating 0 to 5 set by a person. Overwrite the value; do not repeat the key." ;
    rdfs:domain pan:Media ;
    rdfs:range pan:RatingValue .

pan:isPicked a owl:DatatypeProperty ;
    rdfs:label "is picked" ;
    rdfs:comment "true when a person picked this media. Set or unset it; never write false to mean unknown." ;
    rdfs:domain pan:Media ;
    rdfs:range xsd:boolean .

pan:isRejected a owl:DatatypeProperty ;
    rdfs:label "is rejected" ;
    rdfs:comment "true when a person rejected this media. Set or unset it; never write false to mean unknown." ;
    rdfs:domain pan:Media ;
    rdfs:range xsd:boolean .

# -----------------------------------------------------------------------------
# MEDIASETS & IMAGESETS — sets of media a person curates (goodlux, 2026-09-17)
# -----------------------------------------------------------------------------
# A MediaSet is a subtexture:Set (base ontology 0.1.0) and pan:Node. It is an
# unordered set of Pan media of any kind (image, audio, video) a person curates;
# its own file on disk; no ordering. No instances yet; declared so media kinds get
# their own set classes (UI handlers differ per kind).
#
# An ImageSet is a pan:MediaSet holding only pan:Image. IRI <pan/ImageSet/<id>>.
# It is not a fact about any one image, so it has its own file on disk,
# imagesets/<id>.xml, beside the media; the graph is rebuilt from that file.
# Membership is written once, on the image, with the universal: each image
# carries pan:relatedToId <pan/ImageSet/id> in its XMP, one value per set, so
# the fact travels with the file; the set's members are read from the images.
# The code enforces the kind at add time, since membership is written on the
# member and a set-side restriction cannot reach it. The set node carries id,
# description and createdDate. No ordering: one image in two sets has two
# positions.
# -----------------------------------------------------------------------------

pan:MediaSet a owl:Class ;
    rdfs:subClassOf subtexture:Set , pan:Node ;
    rdfs:label "MediaSet" ;
    rdfs:comment "An unordered set of Pan media of any kind (image, audio, video) a person curates; own file on disk; no ordering." .

pan:ImageSet a owl:Class ;
    rdfs:subClassOf pan:MediaSet ;
    rdfs:label "ImageSet" ;
    rdfs:comment "An unordered set of Pan media holding only pan:Image; its own file imagesets/<id>.xml on disk, from which the graph is rebuilt. Do not put an ordering on it." .

# Stub: not yet implemented — no pan:Video media class exists yet and nothing writes one.
pan:VideoSet a owl:Class ;
    rdfs:subClassOf pan:MediaSet ;
    rdfs:label "VideoSet" ;
    rdfs:comment "Stub: an unordered set of Pan media holding only video; not yet implemented — no pan:Video media class exists yet and nothing writes one." .

# Stub: not yet implemented — no pan:Audio media class exists yet and nothing writes one.
pan:AudioSet a owl:Class ;
    rdfs:subClassOf pan:MediaSet ;
    rdfs:label "AudioSet" ;
    rdfs:comment "Stub: an unordered set of Pan media holding only audio; not yet implemented — no pan:Audio media class exists yet and nothing writes one." .

# -----------------------------------------------------------------------------
# THE INSTANCE (goodlux, 2026-09-16)
# -----------------------------------------------------------------------------
# One pand deployment as a node. A machine runs one daemon; the daemon serves
# many stores. The Instance says where its primary graph is answered, where
# a pan-ui cache graph sits if one exists, what root its files live under,
# whether it manages media (copies bytes in, PNG only) or references media
# in place, and which port it listens on. Declared ahead of code: pand will
# write one Instance node per daemon at start in a later release. How a
# pan:Store relates to its Instance is left open until ruled.
# -----------------------------------------------------------------------------

pan:Instance a owl:Class ;
    rdfs:subClassOf pan:Node ;
    rdfs:label "Instance" ;
    rdfs:comment "One pand deployment: its primary graph, an optional pan-ui cache graph, its storage root, its mode and its port. Written by pand at start; never authored." .

pan:primaryGraph a owl:DatatypeProperty ;
    rdfs:label "primary graph" ;
    rdfs:comment "Endpoint URI or named graph of the primary persistent store. Do not put a file path here; that is pan:fsRoot." ;
    rdfs:domain pan:Instance ;
    rdfs:range xsd:string .

pan:localGraph a owl:DatatypeProperty ;
    rdfs:label "local graph" ;
    rdfs:comment "The pan-ui local cache graph, in memory or ephemeral, when one exists. Absent means pan-ui reads the primary graph directly." ;
    rdfs:domain pan:Instance ;
    rdfs:range xsd:string .

pan:fsRoot a owl:DatatypeProperty ;
    rdfs:label "filesystem root" ;
    rdfs:comment "Absolute storage root backing this instance: the media root when managed, the derived root when referenced. Absolute, never relative." ;
    rdfs:domain pan:Instance ;
    rdfs:range xsd:string .

pan:sourceFormat a owl:DatatypeProperty ;
    rdfs:label "source format" ;
    rdfs:comment "MIME type of managed source media, image/png in the managed mode. Do not list several; a managed store has one." ;
    rdfs:domain pan:Instance ;
    rdfs:range xsd:string .

pan:instanceMode a owl:DatatypeProperty ;
    rdfs:label "instance mode" ;
    rdfs:comment "managed (media copied into the store, PNG only) or referenced (media indexed where it lives). One of the two words, nothing else." ;
    rdfs:domain pan:Instance ;
    rdfs:range xsd:string .

pan:listenPort a owl:DatatypeProperty ;
    rdfs:label "listen port" ;
    rdfs:comment "TCP port the daemon listens on. A number, not a URL." ;
    rdfs:domain pan:Instance ;
    rdfs:range xsd:integer .

# -----------------------------------------------------------------------------
# ENRICHMENT — what a model saw, where its output lives, when
# -----------------------------------------------------------------------------
# Every enrichment record is a first-class node with its own IRI
# <pan/Region/x7q2mf>. The producing model is DATA (pan:model), never part of
# a class or property name — a new model must never mean new vocabulary (the
# copia:qwen35vl9bCaption lesson: seven field names for one idea).
# -----------------------------------------------------------------------------

pan:Enrichment a owl:Class ;
    rdfs:subClassOf pan:Node ;
    rdfs:label "Enrichment" ;
    rdfs:comment "A reference to one stage's output file for one media object: which model, where the file is, when produced, and (pan:item) the records it holds. The image's own index of what exists for it." .

# The one reference that counts: a segmentation run yields one Region per
# thing found, all in one file, so its reference says how many. Caption and
# vector references name one file holding one answer; a pose file is counted
# by reading it. (goodlux, 2026-09-16)
pan:RegionData a owl:Class ;
    rdfs:subClassOf pan:Enrichment ;
    rdfs:label "Region data" ;
    rdfs:comment "The reference to one segmentation run's file of regions; the only reference that carries a count." .

pan:Region a owl:Class ;
    rdfs:subClassOf pan:Node ;
    rdfs:label "Region" ;
    rdfs:comment "One segmented region: an outline around one detected thing, with the model's own word for it in pan:descriptor." .

pan:Pose a owl:Class ;
    rdfs:subClassOf pan:Node ;
    rdfs:label "Pose" ;
    rdfs:comment "One detected body pose: a skeleton of keypoints for one figure." .

pan:Caption a owl:Class ;
    rdfs:subClassOf pan:Node ;
    rdfs:label "Caption" ;
    rdfs:comment "One model's whole answer about an image, verbatim (pan:text). Several per image is normal — one per model, all kept. Re-captioning with the same model replaces that model's record." .

pan:Embedding a owl:Class ;
    rdfs:subClassOf pan:Node ;
    rdfs:label "Embedding" ;
    rdfs:comment "A vector embedding: which model, what dimension, where the vector file is. The vector itself is never a graph literal." .

pan:Thumbnail a owl:Class ;
    rdfs:subClassOf pan:Node ;
    rdfs:label "Thumbnail" ;
    rdfs:comment "A reduced-size rendition Pan generated from the media." .

pan:Depth a owl:Class ;
    rdfs:subClassOf pan:Node ;
    rdfs:label "Depth" ;
    rdfs:comment "One monocular depth estimate: a normalized depth map for the whole image, with the range it was normalized from." .

# --- references: media → one stage's output file -----------------------------

pan:regionData a owl:ObjectProperty ;
    rdfs:label "region data" ;
    rdfs:comment "Reference to a segmentation output file holding this image's regions." ;
    rdfs:domain pan:Image ; rdfs:range pan:RegionData .

pan:poseData a owl:ObjectProperty ;
    rdfs:label "pose data" ;
    rdfs:comment "Reference to a pose-detection output file holding this image's poses." ;
    rdfs:domain pan:Image ; rdfs:range pan:Enrichment .

pan:depthData a owl:ObjectProperty ;
    rdfs:label "depth data" ;
    rdfs:comment "Reference to a depth-estimation output file holding this image's depth map." ;
    rdfs:domain pan:Image ; rdfs:range pan:Enrichment .

pan:captionData a owl:ObjectProperty ;
    rdfs:label "caption data" ;
    rdfs:comment "Reference to one captioning model's output file. One per model." ;
    rdfs:domain pan:Media ; rdfs:range pan:Enrichment .

pan:vectorData a owl:ObjectProperty ;
    rdfs:label "vector data" ;
    rdfs:comment "Reference to an embedding vector file (.npy). One per embedding model." ;
    rdfs:domain pan:Media ; rdfs:range pan:Enrichment .

# --- membership: reference → the records its file holds ------------------------
# The image never links a record directly (goodlux, 2026-09-16, option B):
# image → captionData/vectorData/poseData/regionData → pan:item → record.
# One property for every stage, the same shape the image's XMP already had.

pan:item a owl:ObjectProperty ;
    rdfs:label "item" ;
    rdfs:comment "One record this reference's file holds. Reach records through here; never link them from the image." ;
    rdfs:domain pan:Enrichment ; rdfs:range git-lex:Thing .

pan:thumbnail a owl:ObjectProperty ;
    rdfs:label "thumbnail" ;
    rdfs:comment "The Thumbnail Pan made of this media." ;
    rdfs:domain pan:Media ; rdfs:range pan:Thumbnail .

# The file spelling of git-lex:relatedToId — the same property (goodlux,
# 2026-09-16), see pan:id above. The producer writes it in the file; the graph
# may carry either spelling and a reader treats them as one.
pan:relatedToId a owl:ObjectProperty ;
    owl:equivalentProperty subtexture:relatedToId , git-lex:relatedToId ;
    rdfs:label "relatedToId" ;
    rdfs:comment "The Thing this media relates to (its Moment), written <ns/Class/id>; the file spelling of git-lex:relatedToId. Never the bare token." ;
    rdfs:domain pan:Media ; rdfs:range git-lex:Thing .

# --- shared fields -------------------------------------------------------------
# These apply to several pan classes (Enrichment, Region, Pose, Caption, …);
# their domain is the shared parent Node (pan:count excepted: RegionData only) and the cardinality block below says
# which class carries which.

pan:model a owl:DatatypeProperty ;
    rdfs:label "model" ;
    rdfs:comment "The model that produced this record, as configured for the endpoint that ran. Data, not identity." ;
    rdfs:domain pan:Node ;
    rdfs:range xsd:string .

pan:precision a owl:DatatypeProperty ;
    rdfs:label "precision" ;
    rdfs:comment "Numeric precision the model ran at (bf16-cuda, 8-bit), as the server reported it. Data; never part of the index name." ;
    rdfs:domain pan:Node ;
    rdfs:range xsd:string .

pan:provider a owl:DatatypeProperty ;
    rdfs:label "provider" ;
    rdfs:comment "Where the model ran (salad, phala, mac), as the server reported it. Data; never part of the index name." ;
    rdfs:domain pan:Node ;
    rdfs:range xsd:string .

pan:producedDate a owl:DatatypeProperty ;
    rdfs:label "produced date" ;
    rdfs:comment "When pand wrote this record, RFC3339 in system local time." ;
    rdfs:domain pan:Node ;
    rdfs:range xsd:dateTime .

pan:path a owl:DatatypeProperty ;
    rdfs:label "path" ;
    rdfs:comment "Location of the file this record refers to, relative to the store's pan:mediaRoot. Declared, never inferred." ;
    rdfs:domain pan:Node ;
    rdfs:range xsd:string .

# The server's own answer, kept whole beside the record. pan:path names the
# record; this names the answer the record was made from.
pan:modelReplyPath a owl:DatatypeProperty ;
    rdfs:label "model reply path" ;
    rdfs:comment "Path to the model server's own answer, saved whole beside the record, relative to pan:mediaRoot. Absent when the stage saves no such file." ;
    rdfs:domain pan:Enrichment ;
    rdfs:range xsd:string .

# --- what Pan asked a stage for (goodlux, 2026-09-19) --------------------------
# The segmentation node is told which nouns to ground and how strict to be.
# Without these a noun that found nothing left no trace, and an image with no
# person region could not say whether Pan asked for one.

pan:requestNouns a owl:DatatypeProperty ;
    rdfs:label "request nouns" ;
    rdfs:comment "The nouns Pan asked the segmenter to ground, comma-separated, exactly as sent. Includes the ones that found nothing." ;
    rdfs:domain pan:RegionData ; rdfs:range xsd:string .

pan:requestMinConfidence a owl:DatatypeProperty ;
    rdfs:label "request minimum confidence" ;
    rdfs:comment "The score a region had to beat to be returned. Regions below it were dropped by the node, not by Pan." ;
    rdfs:domain pan:RegionData ; rdfs:range xsd:decimal .

pan:requestPolygonVerts a owl:DatatypeProperty ;
    rdfs:label "request polygon vertices" ;
    rdfs:comment "How many vertices each outline was reduced to. A lower number is a coarser outline, not a different region." ;
    rdfs:domain pan:RegionData ; rdfs:range xsd:integer .

pan:count a owl:DatatypeProperty ;
    rdfs:label "count" ;
    rdfs:comment "How many regions the referenced segmentation file holds. Only on a RegionData reference; no other reference carries a count." ;
    rdfs:domain pan:RegionData ;
    rdfs:range xsd:integer .

# --- Region fields -------------------------------------------------------------

pan:descriptor a owl:DatatypeProperty ;
    rdfs:label "descriptor" ;
    rdfs:comment "What the model called the thing in this region (person, keyboard) — the prompt it answered. Description, never identity." ;
    rdfs:domain pan:Region ; rdfs:range xsd:string .

pan:polygon a owl:DatatypeProperty ;
    rdfs:label "polygon" ;
    rdfs:comment "Outline as 'x,y;x,y;…' in pixel coordinates." ;
    rdfs:domain pan:Region ; rdfs:range xsd:string .

pan:bbox a owl:DatatypeProperty ;
    rdfs:label "bounding box" ;
    rdfs:comment "'x1,y1,x2,y2' in pixel coordinates." ;
    rdfs:domain pan:Region ; rdfs:range xsd:string .

pan:score a owl:DatatypeProperty ;
    rdfs:label "score" ;
    rdfs:comment "Model confidence, 0..1." ;
    rdfs:domain pan:Region ; rdfs:range xsd:decimal .

pan:maskPath a owl:DatatypeProperty ;
    rdfs:label "mask path" ;
    rdfs:comment "Path to this region's mask image, relative to pan:mediaRoot, when the segmenter returned one." ;
    rdfs:domain pan:Region ; rdfs:range xsd:string .

# --- Pose fields ---------------------------------------------------------------

pan:keypoints a owl:DatatypeProperty ;
    rdfs:label "keypoints" ;
    rdfs:comment "'x,y,confidence;…' triples in the model's own keypoint order." ;
    rdfs:domain pan:Pose ; rdfs:range xsd:string .

pan:overlayPath a owl:DatatypeProperty ;
    rdfs:label "overlay path" ;
    rdfs:comment "Path to the rendered skeleton overlay image, relative to pan:mediaRoot, when the model returned one." ;
    rdfs:domain pan:Pose ; rdfs:range xsd:string .

# --- Caption / Embedding fields ------------------------------------------------

pan:text a owl:DatatypeProperty ;
    rdfs:label "text" ;
    rdfs:comment "The model's answer as it came back, unparsed." ;
    rdfs:domain pan:Caption ; rdfs:range xsd:string .

pan:dim a owl:DatatypeProperty ;
    rdfs:label "dimension" ;
    rdfs:comment "Length of the embedding vector." ;
    rdfs:domain pan:Embedding ; rdfs:range xsd:integer .

pan:vectorPath a owl:DatatypeProperty ;
    rdfs:label "vector path" ;
    rdfs:comment "Path to the .npy vector file, relative to pan:mediaRoot. The searchable copy lives in the index; this file is the rebuildable source." ;
    rdfs:domain pan:Embedding ; rdfs:range xsd:string .

# --- Depth fields --------------------------------------------------------------

pan:depthMapPath a owl:DatatypeProperty ;
    rdfs:label "depth map path" ;
    rdfs:comment "Path to the 8-bit normalized depth map PNG, relative to pan:mediaRoot. 255 = nearest, 0 = farthest." ;
    rdfs:domain pan:Depth ; rdfs:range xsd:string .

pan:depthMin a owl:DatatypeProperty ;
    rdfs:label "depth min" ;
    rdfs:comment "Lower bound of raw model depth the map was normalized from. Needed to compare maps." ;
    rdfs:domain pan:Depth ; rdfs:range xsd:decimal .

pan:depthMax a owl:DatatypeProperty ;
    rdfs:label "depth max" ;
    rdfs:comment "Upper bound of raw model depth the map was normalized from. Needed to compare maps." ;
    rdfs:domain pan:Depth ; rdfs:range xsd:decimal .

# -----------------------------------------------------------------------------
# CARDINALITY
# -----------------------------------------------------------------------------
# Every class requires exactly one identity. It is restricted as pan:id, the
# universal via owl:equivalentProperty; the graph stores the pan: spelling,
# same as the file (goodlux, 2026-09-17). The git-lex spelling was the 0.3.6
# SHACL-generator workaround, and the generator is being fixed to follow
# owl:equivalentProperty.

pan:Node rdfs:subClassOf
    [ a owl:Restriction ; owl:onProperty pan:id ; owl:cardinality 1 ] .

pan:Store rdfs:subClassOf
    [ a owl:Restriction ; owl:onProperty pan:id ; owl:cardinality 1 ] ,
    [ a owl:Restriction ; owl:onProperty pan:mediaRoot ; owl:cardinality 1 ] .

pan:Media rdfs:subClassOf
    [ a owl:Restriction ; owl:onProperty pan:id ; owl:cardinality 1 ] ,
    [ a owl:Restriction ; owl:onProperty pan:mediaPath  ; owl:cardinality 1 ] ,
    [ a owl:Restriction ; owl:onProperty pan:mediaType  ; owl:cardinality 1 ] ,
    [ a owl:Restriction ; owl:onProperty pan:sourceFile ; owl:cardinality 1 ] ,
    # When Pan created this record and its id (RFC3339, system local time):
    # pan:createdDate, the universal via owl:equivalentProperty.
    [ a owl:Restriction ; owl:onProperty pan:createdDate ; owl:cardinality 1 ] ,
    [ a owl:Restriction ; owl:onProperty pan:enrichmentCompleteDate ; owl:maxCardinality 1 ] ,
    [ a owl:Restriction ; owl:onProperty pan:width      ; owl:maxCardinality 1 ] ,
    [ a owl:Restriction ; owl:onProperty pan:height     ; owl:maxCardinality 1 ] ,
    [ a owl:Restriction ; owl:onProperty pan:thumbnail  ; owl:maxCardinality 1 ] ,
    [ a owl:Restriction ; owl:onProperty pan:modelPromptPath ; owl:maxCardinality 1 ] ,
    [ a owl:Restriction ; owl:onProperty pan:imageSha256Hash ; owl:maxCardinality 1 ] ,
    [ a owl:Restriction ; owl:onProperty pan:renderRequestInformation ; owl:maxCardinality 1 ] ,
    [ a owl:Restriction ; owl:onProperty pan:imageTechnicalScore ; owl:maxCardinality 1 ] ,
    [ a owl:Restriction ; owl:onProperty pan:imageTechnicalCritique ; owl:maxCardinality 1 ] ,
    [ a owl:Restriction ; owl:onProperty pan:imageAestheticScore ; owl:maxCardinality 1 ] ,
    [ a owl:Restriction ; owl:onProperty pan:imageAestheticCritique ; owl:maxCardinality 1 ] ,
    [ a owl:Restriction ; owl:onProperty pan:imageAnatomyScore ; owl:maxCardinality 1 ] ,
    [ a owl:Restriction ; owl:onProperty pan:imageAnatomyCritique ; owl:maxCardinality 1 ] ,
    [ a owl:Restriction ; owl:onProperty pan:rating     ; owl:maxCardinality 1 ] ,
    [ a owl:Restriction ; owl:onProperty pan:isPicked   ; owl:maxCardinality 1 ] ,
    [ a owl:Restriction ; owl:onProperty pan:isRejected ; owl:maxCardinality 1 ] .

# A MediaSet: one id, one creation time, at most one description. Spelled pan:
# as everywhere else in Pan. pan:ImageSet inherits this block.
pan:MediaSet rdfs:subClassOf
    [ a owl:Restriction ; owl:onProperty pan:id ; owl:cardinality 1 ] ,
    [ a owl:Restriction ; owl:onProperty pan:createdDate ; owl:cardinality 1 ] ,
    [ a owl:Restriction ; owl:onProperty pan:description ; owl:maxCardinality 1 ] .

# An Instance: one id, one root, one mode, one port; the rest at most once.
pan:Instance rdfs:subClassOf
    [ a owl:Restriction ; owl:onProperty pan:id ; owl:cardinality 1 ] ,
    [ a owl:Restriction ; owl:onProperty pan:fsRoot       ; owl:cardinality 1 ] ,
    [ a owl:Restriction ; owl:onProperty pan:instanceMode ; owl:cardinality 1 ] ,
    [ a owl:Restriction ; owl:onProperty pan:listenPort   ; owl:cardinality 1 ] ,
    [ a owl:Restriction ; owl:onProperty pan:primaryGraph ; owl:maxCardinality 1 ] ,
    [ a owl:Restriction ; owl:onProperty pan:localGraph   ; owl:maxCardinality 1 ] ,
    [ a owl:Restriction ; owl:onProperty pan:sourceFormat ; owl:maxCardinality 1 ] .

pan:Enrichment rdfs:subClassOf
    [ a owl:Restriction ; owl:onProperty pan:id ; owl:cardinality 1 ] ,
    [ a owl:Restriction ; owl:onProperty pan:model      ; owl:cardinality 1 ] ,
    [ a owl:Restriction ; owl:onProperty pan:path       ; owl:cardinality 1 ] ,
    [ a owl:Restriction ; owl:onProperty pan:modelReplyPath  ; owl:maxCardinality 1 ] ,
    [ a owl:Restriction ; owl:onProperty pan:producedDate ; owl:cardinality 1 ] .

# The segmentation reference alone carries a count (goodlux, 2026-09-16), and
# the request that produced it (goodlux, 2026-09-19).
pan:RegionData rdfs:subClassOf
    [ a owl:Restriction ; owl:onProperty pan:count      ; owl:cardinality 1 ] ,
    [ a owl:Restriction ; owl:onProperty pan:requestNouns ; owl:maxCardinality 1 ] ,
    [ a owl:Restriction ; owl:onProperty pan:requestMinConfidence ; owl:maxCardinality 1 ] ,
    [ a owl:Restriction ; owl:onProperty pan:requestPolygonVerts ; owl:maxCardinality 1 ] .

# One render request per image, with its prompt and the whole chunk it came
# from; every setting at most once, except the modules.

pan:Region rdfs:subClassOf
    [ a owl:Restriction ; owl:onProperty pan:id ; owl:cardinality 1 ] ,
    [ a owl:Restriction ; owl:onProperty pan:model      ; owl:cardinality 1 ] ,
    [ a owl:Restriction ; owl:onProperty pan:producedDate ; owl:cardinality 1 ] ,
    [ a owl:Restriction ; owl:onProperty pan:descriptor ; owl:cardinality 1 ] ,
    [ a owl:Restriction ; owl:onProperty pan:polygon    ; owl:maxCardinality 1 ] ,
    [ a owl:Restriction ; owl:onProperty pan:bbox       ; owl:maxCardinality 1 ] ,
    [ a owl:Restriction ; owl:onProperty pan:score      ; owl:maxCardinality 1 ] ,
    [ a owl:Restriction ; owl:onProperty pan:maskPath   ; owl:maxCardinality 1 ] .

pan:Pose rdfs:subClassOf
    [ a owl:Restriction ; owl:onProperty pan:id ; owl:cardinality 1 ] ,
    [ a owl:Restriction ; owl:onProperty pan:model       ; owl:cardinality 1 ] ,
    [ a owl:Restriction ; owl:onProperty pan:producedDate  ; owl:cardinality 1 ] ,
    [ a owl:Restriction ; owl:onProperty pan:keypoints   ; owl:cardinality 1 ] ,
    [ a owl:Restriction ; owl:onProperty pan:overlayPath ; owl:maxCardinality 1 ] .

pan:Caption rdfs:subClassOf
    [ a owl:Restriction ; owl:onProperty pan:id ; owl:cardinality 1 ] ,
    [ a owl:Restriction ; owl:onProperty pan:model      ; owl:cardinality 1 ] ,
    [ a owl:Restriction ; owl:onProperty pan:producedDate ; owl:cardinality 1 ] ,
    [ a owl:Restriction ; owl:onProperty pan:modelPromptPath ; owl:maxCardinality 1 ] ,
    [ a owl:Restriction ; owl:onProperty pan:text       ; owl:cardinality 1 ] .

pan:Embedding rdfs:subClassOf
    [ a owl:Restriction ; owl:onProperty pan:id ; owl:cardinality 1 ] ,
    [ a owl:Restriction ; owl:onProperty pan:model      ; owl:cardinality 1 ] ,
    [ a owl:Restriction ; owl:onProperty pan:producedDate ; owl:cardinality 1 ] ,
    [ a owl:Restriction ; owl:onProperty pan:dim        ; owl:cardinality 1 ] ,
    [ a owl:Restriction ; owl:onProperty pan:vectorPath ; owl:cardinality 1 ] .

pan:Thumbnail rdfs:subClassOf
    [ a owl:Restriction ; owl:onProperty pan:id ; owl:cardinality 1 ] ,
    [ a owl:Restriction ; owl:onProperty pan:path       ; owl:cardinality 1 ] ,
    [ a owl:Restriction ; owl:onProperty pan:producedDate ; owl:cardinality 1 ] ,
    [ a owl:Restriction ; owl:onProperty pan:width      ; owl:cardinality 1 ] ,
    [ a owl:Restriction ; owl:onProperty pan:height     ; owl:cardinality 1 ] .

pan:Depth rdfs:subClassOf
    [ a owl:Restriction ; owl:onProperty pan:id ; owl:cardinality 1 ] ,
    [ a owl:Restriction ; owl:onProperty pan:model        ; owl:cardinality 1 ] ,
    [ a owl:Restriction ; owl:onProperty pan:producedDate ; owl:cardinality 1 ] ,
    [ a owl:Restriction ; owl:onProperty pan:depthMapPath ; owl:cardinality 1 ] ,
    [ a owl:Restriction ; owl:onProperty pan:depthMin     ; owl:cardinality 1 ] ,
    [ a owl:Restriction ; owl:onProperty pan:depthMax     ; owl:cardinality 1 ] ,
    [ a owl:Restriction ; owl:onProperty pan:width        ; owl:maxCardinality 1 ] ,
    [ a owl:Restriction ; owl:onProperty pan:height       ; owl:maxCardinality 1 ] .
