@prefix pu:      <http://purl.org/stuff/plugin-universe/> .
@prefix trn:     <http://purl.org/stuff/transmissions/> .
@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#> .
@prefix dcterms: <http://purl.org/dc/terms/> .
@prefix prov:    <http://www.w3.org/ns/prov#> .
@prefix skos:    <http://www.w3.org/2004/02/skos/core#> .
@prefix units:   <http://lv2plug.in/ns/extensions/units#> .
@prefix foaf:    <http://xmlns.com/foaf/0.1/> .

pu: a owl:Ontology ;
    rdfs:label "Plugin Universe vocabulary" ;
    rdfs:comment "Terms for the Plugin Universe catalogue that have no home in an existing vocabulary. Plugin description proper uses trn: for curated behaviour and lv2:/units: for parameters; this covers measurement, packaging and catalogue administration." ;
    dcterms:license <https://creativecommons.org/publicdomain/zero/1.0/> .

## ── Catalogue administration ────────────────────────────────────────────────

pu:SourceGraph a owl:Class ; rdfs:label "Source Graph" ;
    rdfs:comment "A named graph holding data from one source, carrying its provenance and licence. Every triple in the catalogue lives in one of these." .

pu:graphKind      a owl:DatatypeProperty ; rdfs:domain pu:SourceGraph ; rdfs:range xsd:string .
pu:redistributable a owl:DatatypeProperty ; rdfs:domain pu:SourceGraph ; rdfs:range xsd:boolean ;
    rdfs:comment "Whether this graph's content may be published at all." .
pu:inCC0Dump      a owl:DatatypeProperty ; rdfs:domain pu:SourceGraph ; rdfs:range xsd:boolean ;
    rdfs:comment "Whether this graph may appear in the CC0 dataset dump without an attached notice." .
pu:precedence     a owl:DatatypeProperty ; rdfs:domain pu:SourceGraph ; rdfs:range xsd:integer ;
    rdfs:comment "Source precedence for conflict resolution. Higher wins." .
pu:harvestRun     a owl:DatatypeProperty ; rdfs:domain pu:SourceGraph ; rdfs:range xsd:string .

## ── Categories and tags ─────────────────────────────────────────────────────
## Categories are a SKOS concept scheme rather than a class hierarchy: plugin
## categories overlap, are contested, and differ between sources. Tags are the
## uncontrolled form — a source's own keywords, kept verbatim so that a mapping
## decided wrongly can be revisited without re-harvesting.

pu:category a owl:ObjectProperty ; rdfs:range skos:Concept ;
    rdfs:comment "A concept from the catalogue's category scheme. Source categories, LV2 plugin classes and user tags map in as skos:closeMatch." .

pu:tag a owl:DatatypeProperty ; rdfs:range xsd:string ;
    rdfs:comment "A source's own keyword, unmapped and unmodified. Feeds retrieval; carries no semantics." .

pu:unitLabel a owl:DatatypeProperty ; rdfs:range xsd:string ;
    rdfs:comment "A parameter's unit as the source wrote it, kept when it could not be mapped to a units: individual. Losing the label would be worse than failing to type it." .

## ── Availability ────────────────────────────────────────────────────────────
## The two questions asked most often about any plugin: can I see the source,
## and do I have to pay. Two axes rather than one enumeration, because they are
## independent — Ardour is GPL-3.0 and its official binaries are sold.

pu:SourceAvailability a owl:Class ; rdfs:label "Source Availability" .
pu:OpenSource      a pu:SourceAvailability ; rdfs:label "Open source" ;
    rdfs:comment "Released under an OSI-approved licence or a public-domain dedication. Derived from the stated licence, never guessed." .
pu:SourceAvailable a pu:SourceAvailability ; rdfs:label "Source available" ;
    rdfs:comment "Source can be read but the licence does not grant the freedoms of an open-source one." .
pu:Proprietary     a pu:SourceAvailability ; rdfs:label "Proprietary" ;
    rdfs:comment "No source. Asserted only where a source says so; an absent licence means unknown, not proprietary." .

pu:Pricing a owl:Class ; rdfs:label "Pricing" .
pu:Free         a pu:Pricing ; rdfs:label "Free" .
pu:Donationware a pu:Pricing ; rdfs:label "Donationware" .
pu:Freemium     a pu:Pricing ; rdfs:label "Freemium" ; rdfs:comment "A free tier alongside a paid one." .
pu:Paid         a pu:Pricing ; rdfs:label "Paid" .

pu:sourceAvailability a owl:ObjectProperty ; rdfs:range pu:SourceAvailability ;
    rdfs:comment "Derived from the plugin's own licence. Absent means unknown." .
pu:pricing a owl:ObjectProperty ; rdfs:range pu:Pricing ;
    rdfs:comment "Never derived from a licence: an open-source licence grants the right to the source, not a free binary. Asserted only by a harvester whose source states it, and carrying that source's provenance." .
pu:licenceId a owl:DatatypeProperty ; rdfs:range xsd:string ;
    rdfs:comment "The plugin's licence as an SPDX identifier where one is recognisable. dcterms:license keeps whatever the source actually said." .

## ── Accounts ────────────────────────────────────────────────────────────────
## Personal data, in <graph:system/accounts>, which carries the `personal-data`
## licence flag and is therefore excluded from every public dump by the same
## query that excludes any other non-redistributable source. What is recorded
## here is exactly what the contributor terms say is recorded: no email address,
## because the sign-in cannot read one, and no access token, because it is
## discarded after identification.

pu:githubId a owl:DatatypeProperty ; rdfs:range xsd:string ;
    rdfs:comment "The GitHub numeric id. Identity is minted from this rather than the login, because a login can be renamed and an account whose IRI moved would orphan every contribution attributed to it." .
pu:trustLevel a owl:DatatypeProperty ; rdfs:range xsd:string ;
    rdfs:comment "new, trusted or moderator. Earned, never asserted by the account holder: it decides whether a contribution is queued or goes live." .
pu:tier a owl:DatatypeProperty ; rdfs:range xsd:string ;
    rdfs:comment "registered, pro or admin." .
pu:suspended a owl:DatatypeProperty ; rdfs:range xsd:boolean ;
    rdfs:comment "Checked against the account on every request rather than carried in the session cookie, so suspension takes effect immediately even though sessions are stateless." .
pu:lastSeen a owl:DatatypeProperty ; rdfs:range xsd:dateTime .

## The paid part of an account. docs/architecture.md §7: payment is an external
## provider's job, and what is stored here is "only a customer reference and an
## entitlement with an expiry". Both halves of that sentence are load-bearing.

pu:stripeCustomer a owl:DatatypeProperty ; rdfs:range xsd:string ;
    rdfs:comment "The payment processor's customer id, and the whole of what this system knows about somebody's payment details. Opaque and useless without the processor, which is the point: no card, no name, no billing address is stored here, and the one thing that is stored is a handle rather than a fact about a person. Personal data nonetheless — it links an account to a payment relationship — so it lives in the accounts graph, which is flagged personal-data and never reaches a dump." .

pu:claimsVendor a owl:ObjectProperty ; rdfs:range pu:Vendor ;
    rdfs:comment "The vendor this account has been confirmed as speaking for. A pu:Vendor IRI, not a folded name: the fold is derived from the spelling, so it changes when two vendors are merged, and a claim keyed on it silently stopped matching exactly the plugins the merge had just gathered. Holding the identity instead means the claim follows whatever the identity layer decides — a merge repoints foaf:maker and the claim comes with it. Set only by a moderator, never by the account holder, because the entitlement it unlocks is the right to promote somebody's plugins for free and a self-asserted claim would be a way to promote anybody's." .

pu:claimConfirmedBy a owl:ObjectProperty ;
    rdfs:comment "The moderator who confirmed a vendor claim. A claim that cannot be traced to somebody who decided it is not an audit trail." .

pu:tierEndsAt a owl:DatatypeProperty ; rdfs:range xsd:dateTime ;
    rdfs:comment "When a paid tier lapses. The tier itself is pu:tier; this is what makes it an entitlement rather than a permanent grant. Read at the moment the tier is used, exactly as a promotion's pu:endsAt is, so an entitlement whose subscription stopped being paid for expires on its own — whether or not any webhook arrived to say so. A paid status that persists because a cancellation message went missing is the failure this avoids, and it is the expensive direction to fail in." .

## ── Contributions ───────────────────────────────────────────────────────────
## A person proposing a change to a fact. Corrections are *proposals* until
## accepted, and an accepted one takes effect through source precedence rather
## than by overwriting anything: the harvested statement stays where it is, in
## its own graph, and the contributor's graph outranks or is outranked by it
## according to the weights in config/preferences.js. Nothing is ever destroyed
## by a correction, which is what makes rollback a query.

pu:Correction a owl:Class ; rdfs:label "Correction" ;
    rdfs:comment "A proposed change to one statement about one plugin. Typed rather than free text, so it can be validated against the shapes before it is written and applied mechanically once accepted." .

pu:correctionSubject a owl:ObjectProperty ; rdfs:domain pu:Correction ;
    rdfs:comment "The plugin the correction is about." .
pu:correctionPredicate a owl:DatatypeProperty ; rdfs:domain pu:Correction ; rdfs:range xsd:string ;
    rdfs:comment "Which property is being corrected, as an IRI string. Restricted to a whitelist: a correction is not a way to write arbitrary triples." .
pu:proposedValue a owl:DatatypeProperty ; rdfs:domain pu:Correction ;
    rdfs:comment "What the contributor says it should be." .
pu:currentValue a owl:DatatypeProperty ; rdfs:domain pu:Correction ;
    rdfs:comment "What it said when the correction was made. Kept so a reviewer can see whether the catalogue changed underneath the proposal." .
pu:rationale a owl:DatatypeProperty ; rdfs:domain pu:Correction ; rdfs:range xsd:string ;
    rdfs:comment "Why. Free text, and the only part of a correction that is authored prose rather than a fact — but it is about the contribution rather than about the plugin, so it stays with the correction in the system graph." .
pu:correctionStatus a owl:DatatypeProperty ; rdfs:domain pu:Correction ; rdfs:range xsd:string ;
    rdfs:comment "pending, accepted or rejected. A correction from a trusted contributor is accepted on arrival; one from a new contributor waits." .
pu:reviewedBy a owl:ObjectProperty ; rdfs:domain pu:Correction .
pu:reviewedAt a owl:DatatypeProperty ; rdfs:domain pu:Correction ; rdfs:range xsd:dateTime .

## ── Vendors ─────────────────────────────────────────────────────────────────
## Who made a plugin, as a resource rather than as a string.
##
## `trn:vendor` is free text and stays free text: it is what a source said, and
## overwriting it would lose that. What it cannot do is be *pointed at*. A name
## has no description, no logo, no support address and no owning account; two
## spellings of one maker are two vendors for ever; and renaming orphans
## whatever was attached to the old spelling, because the key was computed from
## the name.
##
## So identity is asserted alongside the string. The IRI is minted by content
## hash over the folded name, which makes it stable and idempotent — re-deriving
## produces the same IRIs — and every spelling met is kept as a `skos:altLabel`
## so that merging two vendors is adding a label rather than rewriting history.
##
## **Derived, not harvested.** A harvester sees one source's graph, and the
## identity of a vendor is a fold *across* sources — "danja" appears in four of
## them. No harvester is in a position to compute it, so it is built after
## ingest by `bin/mint-vendors.js` into a graph of its own, and rebuilding is a
## DROP and a re-derive.

pu:Vendor a owl:Class ; rdfs:subClassOf foaf:Agent ; rdfs:label "Vendor" ;
    rdfs:comment "Whoever makes a plugin — a company, a project or one person. Deliberately a foaf:Agent rather than an Organization: most vendors in this catalogue are individuals, and the data never says which." .

pu:vendorKey a owl:DatatypeProperty ; rdfs:domain pu:Vendor ; rdfs:range xsd:string ;
    rdfs:label "vendor key" ;
    rdfs:comment "The folded name two spellings have in common: lower case, punctuation and spacing removed. What the grouping is computed from, kept so the derivation can be checked and so a lookup by key needs no string handling at query time." .

## The link from a plugin to its maker is foaf:maker, which already means this.
## A project-specific predicate here would be a bespoke term for a relation that
## two widely-used vocabularies already have a word for.

## ── Feedback ────────────────────────────────────────────────────────────────
## A message to the moderators, from somebody signed in.
##
## Deliberately *not* a contribution. A correction, a submission and a wiki
## revision are all offers of material for the catalogue, and each ends up in a
## graph with a publication licence on it. This is correspondence: it is about
## the site rather than about a plugin, nobody agreed to publish it, and it
## lives in a personal-data graph for the same reason the accounts do. The
## public dump excludes it by the query that already excludes anything
## non-redistributable, and erasing a person erases their messages with them.
##
## It carries no subject line and no reply address on purpose. The account says
## who wrote it and the moderators answer out of band, so the record holds the
## least it can while still being useful.

pu:Feedback a owl:Class ; rdfs:label "Feedback" ;
    rdfs:comment "A message sent to the moderators through the site by a signed-in account. Correspondence, not a contribution: it is never published, never enters a dump, and is deleted with the account that wrote it." .

pu:message a owl:DatatypeProperty ; rdfs:domain pu:Feedback ; rdfs:range xsd:string ;
    rdfs:label "message" ;
    rdfs:comment "What was written. Plain text, rendered escaped and never as markup — it is read by a moderator in a queue, and a message is the one place on this site where a stranger's words reach an administrator's screen." .
pu:feedbackStatus a owl:DatatypeProperty ; rdfs:domain pu:Feedback ; rdfs:range xsd:string ;
    rdfs:label "feedback status" ;
    rdfs:comment "new or read. A message is not accepted or rejected — there is nothing to apply — so the only question a moderator answers is whether it has been dealt with." .
pu:readBy a owl:ObjectProperty ; rdfs:domain pu:Feedback ;
    rdfs:comment "The moderator who marked it read." .
pu:readAt a owl:DatatypeProperty ; rdfs:domain pu:Feedback ; rdfs:range xsd:dateTime .

## A person proposing a plugin the catalogue does not have. Reviewed as one
## thing rather than as a set of corrections: a plugin needs a name to exist at
## all, so a half-accepted submission would be a record nothing could display.
## The proposed fields are held on the submission until it is accepted; only
## then are they minted into a plugin in the contributor's CC0 graph.

pu:Submission a owl:Class ; rdfs:label "Submission" ;
    rdfs:comment "A proposed plugin, from a person rather than a harvester. A proposal until reviewed, and its fields live here rather than in the catalogue until then." .
pu:submissionStatus a owl:DatatypeProperty ; rdfs:domain pu:Submission ; rdfs:range xsd:string ;
    rdfs:comment "pending, accepted or rejected — the same three a correction has, because the queue and the trust promotion are the same." .
pu:proposedPlugin a owl:ObjectProperty ; rdfs:domain pu:Submission ;
    rdfs:comment "The IRI the plugin will have if this is accepted. Minted at submission time from the identifying tuple, so that two people proposing the same plugin collide here rather than creating two records of one thing." .
pu:proposedField a owl:ObjectProperty ; rdfs:domain pu:Submission ;
    rdfs:comment "One proposed predicate and value, as a blank node. A submission is several of these and is accepted or rejected whole." .
pu:fieldPredicate a owl:DatatypeProperty ; rdfs:range xsd:string ;
    rdfs:comment "The predicate this field proposes, as a string — it is a proposal about the graph, not yet a statement in it." .
pu:fieldValue a owl:DatatypeProperty ;
    rdfs:comment "The proposed value, as given. Turned into a term of the right kind at acceptance, by the same table that validated it." .

## ── Wiki prose ──────────────────────────────────────────────────────────────
## The CC BY-SA half of the catalogue. Facts are CC0 and live in a contributor's
## -facts graph; prose is share-alike and lives in their -prose graph, so the
## licence boundary is a property of the store rather than a judgement made at
## publication time.

pu:WikiRevision a owl:Class ; rdfs:label "Wiki revision" ;
    rdfs:comment "One saved version of the prose about a plugin. Revisions are never edited and never deleted: saving writes a new one that supersedes the last through prov:wasRevisionOf, so the history is the data rather than a log kept beside it. That is also what makes a bad edit reversible without trusting anybody's backup." .

pu:wikiSubject a owl:ObjectProperty ; rdfs:domain pu:WikiRevision ;
    rdfs:comment "The plugin this revision is about. Not pu:subject, whose domain is pu:Measurement — a measurement and a paragraph are about a plugin in different senses and conflating the properties would make either query wrong." .

pu:wikiText a owl:DatatypeProperty ; rdfs:domain pu:WikiRevision ; rdfs:range xsd:string ;
    rdfs:comment "Markdown as the contributor typed it. The source is stored and never the rendered HTML: rendering is a decision made on the way out, so a change to how prose is sanitised applies to every revision ever written rather than only to the next one." .

pu:editSummary a owl:DatatypeProperty ; rdfs:domain pu:WikiRevision ; rdfs:range xsd:string ;
    rdfs:comment "What changed and why, in the contributor's words. Optional, and about the edit rather than about the plugin." .

## ── Packaging and distribution ──────────────────────────────────────────────
## Modelled on the Open Audio Stack registry manifest.

pu:Package a owl:Class ; rdfs:label "Package" ;
    rdfs:comment "A distributable release of a plugin, preset, project or app." .

pu:PackageFile a owl:Class ; rdfs:label "Package File" ;
    rdfs:comment "One downloadable artefact of a package version." .

pu:slug          a owl:DatatypeProperty ; rdfs:range xsd:string ;
    rdfs:comment "Registry identifier in organisation/package-name form." .
pu:package       a owl:ObjectProperty ; rdfs:range pu:Package ;
    rdfs:comment "A released package of this plugin." .
pu:packageVersion a owl:DatatypeProperty ; rdfs:domain pu:Package ; rdfs:range xsd:string ;
    rdfs:comment "Semantic version as the registry states it." .
pu:packageFile   a owl:ObjectProperty ; rdfs:domain pu:Package ; rdfs:range pu:PackageFile .
pu:downloadUrl   a owl:DatatypeProperty ; rdfs:domain pu:PackageFile ; rdfs:range xsd:anyURI .
pu:sha256        a owl:DatatypeProperty ; rdfs:domain pu:PackageFile ; rdfs:range xsd:string .
pu:fileSize      a owl:DatatypeProperty ; rdfs:domain pu:PackageFile ; rdfs:range xsd:integer .
pu:containsFormat a owl:ObjectProperty ; rdfs:domain pu:PackageFile ; rdfs:range trn:PluginFormat .
pu:containsArtefact a owl:DatatypeProperty ; rdfs:domain pu:PackageFile ; rdfs:range xsd:string ;
    rdfs:comment "A payload kind the registry names that is not a plugin format — a raw binary or a standalone executable. Kept verbatim rather than guessed at: a bare 'dll' does not say which plugin API it implements." .
pu:architecture  a owl:DatatypeProperty ; rdfs:domain pu:PackageFile ; rdfs:range xsd:string .
pu:operatingSystem a owl:DatatypeProperty ; rdfs:domain pu:PackageFile ; rdfs:range xsd:string .
pu:fileKind      a owl:DatatypeProperty ; rdfs:domain pu:PackageFile ; rdfs:range xsd:string ;
    rdfs:comment "archive or installer." .

pu:verified a owl:DatatypeProperty ; rdfs:range xsd:boolean ;
    rdfs:comment "The package slug's organisation matches the download URL's domain. Computed at harvest, never asserted by a submitter." .
pu:attested a owl:DatatypeProperty ; rdfs:range xsd:boolean ;
    rdfs:comment "A build artifact attestation exists for this file. Computed at harvest." .
pu:downloadCount a owl:DatatypeProperty ; rdfs:range xsd:integer ;
    rdfs:comment "Downloads reported by the source at harvest time. A snapshot, not a fact about the plugin." .
pu:audioPreview a owl:DatatypeProperty ; rdfs:range xsd:anyURI .
pu:donateUrl    a owl:DatatypeProperty ; rdfs:range xsd:anyURI .

## ── Measurement ─────────────────────────────────────────────────────────────
## Profiler output. A measurement is true of a plugin version on a machine at a
## time, not of the plugin, so it is an observation rather than a property.

pu:Measurement a owl:Class ; rdfs:label "Measurement" ;
    rdfs:comment "One observation produced by a profiler run." .

pu:Metric a owl:Class ; rdfs:label "Metric" .

pu:CpuLoad          a pu:Metric ; rdfs:label "CPU load" ; units:unit units:pc ;
    rdfs:comment "Percentage of one core consumed at the stated block size and sample rate." .
pu:Latency          a pu:Metric ; rdfs:label "Reports latency" ;
    rdfs:comment "Whether the plugin declares latency to its host. Boolean, because this is what a scan can establish: a static scan sees that a latency port exists, not what the plugin reports through it at run time." ;
    rdfs:seeAlso pu:LatencySamples .
pu:LatencySamples   a pu:Metric ; rdfs:label "Latency" ; units:unit units:frame ;
    rdfs:comment "Latency in samples, as reported by the plugin while running. Needs a host that actually instantiates it and asks, which is pluginval; pu:Latency is the scan-time counterpart that only says whether latency is declared at all." .
pu:ScanTime         a pu:Metric ; rdfs:label "Scan time" ; units:unit units:ms ;
    rdfs:comment "Time to enumerate the plugin." .
pu:ValidationResult a pu:Metric ; rdfs:label "Validation result" ;
    rdfs:comment "Verdict from a format validator. 'passed' and 'failed' are pluginval's own conclusions about the plugin; 'crashed' and 'timed-out' are what the sandbox saw; 'unloadable' means the profiler image could not load the binary at all and says nothing about the plugin. They are kept distinct on purpose — a plugin that fails a test and a plugin that killed the validator are different facts about it, and a plugin the profiler is too old to load is a fact about the profiler." .
pu:DenormalBehaviour a pu:Metric ; rdfs:label "Denormal behaviour" .
pu:DiscoveredControlPorts a pu:Metric ; rdfs:label "Discovered control ports" ;
    rdfs:comment "Control ports the built binary reports — the like-for-like counterpart of the parameter count in a harvested profile. Comparing the profile against the *total* port count instead makes every plugin look wrong, because a binary also has audio and atom ports." .
pu:DiscoveredPorts a pu:Metric ; rdfs:label "Discovered ports" ;
    rdfs:comment "Ports the built binary reports through lilv, which is what a host sees. Comparing it with the harvested profile's count is how a disagreement between a bundle's Turtle and its binary becomes visible." .
pu:signal a owl:DatatypeProperty ; rdfs:domain pu:Measurement ; rdfs:range xsd:string ;
    rdfs:comment "The signal a crashing scan died from, where it died from one. A plugin that takes the scanning tool with it is a recorded result." .
pu:StateIntegrity   a pu:Metric ; rdfs:label "State save/restore integrity" .
pu:FailedTests      a pu:Metric ; rdfs:label "Failed tests" ;
    rdfs:comment "How many of a validator's own tests reported a failure, out of those it ran. Meaningless without pu:strictness: the same plugin fails a different number of tests at level 5 and level 10, because they are not the same tests." .
pu:OpenTimeCold     a pu:Metric ; rdfs:label "Open time (cold)" ; units:unit units:ms ;
    rdfs:comment "Time to instantiate the plugin the first time, when nothing of it is cached. This is what a user waits for when a session loads." .
pu:OpenTimeWarm     a pu:Metric ; rdfs:label "Open time (warm)" ; units:unit units:ms ;
    rdfs:comment "Time to instantiate the plugin again immediately afterwards. Where it is far below the cold figure the cost was loading the binary; where it is not, the plugin does that work every time." .

pu:subject    a owl:ObjectProperty ; rdfs:domain pu:Measurement ;
    rdfs:comment "The plugin measured." .
pu:metric     a owl:ObjectProperty ; rdfs:domain pu:Measurement ; rdfs:range pu:Metric .
pu:value      a owl:DatatypeProperty ; rdfs:domain pu:Measurement .
pu:tool       a owl:DatatypeProperty ; rdfs:domain pu:Measurement ; rdfs:range xsd:string ;
    rdfs:comment "Tool and version, e.g. 'pluginval 1.0.3'." .
pu:platform   a owl:DatatypeProperty ; rdfs:domain pu:Measurement ; rdfs:range xsd:string .
pu:sampleRate a owl:DatatypeProperty ; rdfs:domain pu:Measurement ; rdfs:range xsd:integer .
pu:blockSize  a owl:DatatypeProperty ; rdfs:domain pu:Measurement ; rdfs:range xsd:integer .
pu:strictness a owl:DatatypeProperty ; rdfs:domain pu:Measurement ; rdfs:range xsd:integer ;
    rdfs:comment "The strictness level a validator was run at, 1-10 for pluginval. A condition of the measurement rather than a result of it, and recorded on every reading because \"passed pluginval\" is not a fact until it says at what level." .
pu:pluginVersion a owl:DatatypeProperty ; rdfs:domain pu:Measurement ; rdfs:range xsd:string .

## ── Embedding bookkeeping ───────────────────────────────────────────────────
## The vectors themselves live in the index, not here. The graph records only
## enough to know whether an embedding is current.

pu:embeddingModel     a owl:DatatypeProperty ; rdfs:range xsd:string .
pu:embeddingDimension a owl:DatatypeProperty ; rdfs:range xsd:integer .
pu:embeddedAtTime     a owl:DatatypeProperty ; rdfs:range xsd:dateTime .
pu:composedTextHash   a owl:DatatypeProperty ; rdfs:range xsd:string ;
    rdfs:comment "Hash of the text that was embedded, so a stale embedding is detectable without re-embedding." .

## ── Promotion ───────────────────────────────────────────────────────────────
## Promotion records are ordinary graph resources, which is what makes a public
## ad repository a query rather than a feature.

pu:Promotion a owl:Class ; rdfs:label "Promotion" ;
    rdfs:comment "A paid placement. Disclosed publicly: who paid, for what, and when." .

pu:promotes    a owl:ObjectProperty ; rdfs:domain pu:Promotion ;
    rdfs:comment "The plugin whose placement was paid for." .
pu:paidBy      a owl:ObjectProperty ; rdfs:domain pu:Promotion ;
    rdfs:comment "Who paid. Absent where a moderator placed it directly and no money changed hands — which is the honest record of that, not an omission. The DSA disclosure of whose behalf it is on then falls to the plugin's own vendor, which the plugin already states." .
pu:startedAt   a owl:DatatypeProperty ; rdfs:domain pu:Promotion ; rdfs:range xsd:dateTime .
pu:endsAt      a owl:DatatypeProperty ; rdfs:domain pu:Promotion ; rdfs:range xsd:dateTime ;
    rdfs:comment "When the placement stops. The single thing that decides whether a promotion is live: expiry and early revocation are the same fact, so ending one early is a new value here rather than a second state to keep in step. Read at query time, so a lapsed promotion stops being applied whether or not anything swept it up." .
pu:revokedBy   a owl:ObjectProperty ; rdfs:domain pu:Promotion ;
    rdfs:comment "The moderator who ended a placement before its term. Distinguishes a decision from an expiry, which pu:endsAt alone cannot." .
pu:paymentReference a owl:DatatypeProperty ; rdfs:domain pu:Promotion ; rdfs:range xsd:string ;
    rdfs:comment "The payment processor's own identifier for the transaction that bought this placement — a Stripe Checkout Session id. Opaque here and meaningless without the processor, which is the point: it is the thread back to a payment this system deliberately knows nothing else about. It is also what makes fulfilment idempotent, since a webhook may be delivered more than once and the second delivery carries the same id as the first. Never a card number, a name or an address: none of those reaches this system." .

## A promotion is never deleted. A record of a paid placement that has been
## taken down is exactly what an ad repository is asked for, and the audit of
## who placed it is worth more than the row it occupies.
