50 KiB
Pernod Ricard Data MetaModel — T-Box Rulebook
Version 1.2 — Draft for review — generated 2026-08-03
Generated from
rules.yaml. Do not edit this document: edit the source and regenerate (EV-007, EV-008).
Scope
Governs the vocabulary of the model itself: the IRIs, labels and declarative axioms of every class, property and enumeration individual. Does not govern instances, which remain under the A-Box rulebook.
Written to be read and applied directly, by a person or by a language model, without further context. Every rule states what must hold, why, and what enforces it.
Categories
| Category | Subject | Rules |
|---|---|---|
| Identifier | Form of the IRI | 17 |
| Label | Derivation and display | 6 |
| Declaration | Declarative completeness | 5 |
| Evolution | Change and propagation | 15 |
Severity
- BLOCKING — The commit does not pass. Enforced pre-commit and in continuous integration by the named executor.
- MAJOR — Reviewed and answered for, but not machine-enforceable. Reserved for rules that require human judgement, per EV-011.
- GUIDELINE — Recommended practice with no gate.
Identifier — Form of the IRI
The IRI is identity. It is immutable in the RDF sense — changing one is a migration, never an edit — so it must be unambiguous, searchable and free of local convention.
TN-001 — CamelCase IRI with no separator
Statement. The local name of an IRI is strict CamelCase with no separator of any kind: no hyphen, no underscore, no dot, no space. Initial capital for a class or an enumeration individual; initial lower case for a property, whether object, datatype or annotation. Consecutive capitals are admitted only where they carry an acronym reproduced from a target class short label under TN-011, and nowhere else.
| Severity | BLOCKING |
| Applies to | class, object_property, datatype_property, annotation_property, individual |
| Enforced at | script |
| Executor | check_tbox_naming.py |
| Procedure | pending |
Why. A hyphen forces escaping in certain Turtle and SPARQL contexts and makes decamelisation ambiguous: Sub-Domain has no single correct expansion. Typographic convention is a display concern and belongs to the short label, never to identity.
| From | To | Note |
|---|---|---|
| pr:data_domain | pr:DataDomain | no underscore |
| pr:Sub-Domain | pr:DataSubDomain | no hyphen |
| pr:BelongsTo | pr:belongsTo | a property starts lower case |
TN-002 — No acronym in the IRI
Statement. The local name of an IRI contains no acronym, initialism or abbreviation. Every short form is expanded in full. One exception, and one only: a relation reproducing the short label of the class it targets carries that short label as it stands, acronym included, under TN-011. No other term may carry an acronym, and no exception list is maintained.
| Severity | BLOCKING |
| Applies to | class, object_property, datatype_property, annotation_property, individual |
| Enforced at | script |
| Executor | check_tbox_naming.py |
| Procedure | pending |
| Related | A-Box NR-004 |
Why. An acronym in an identifier is a local convention pretending to be a name. It is unsearchable by anyone who does not already know it, and it collides across domains: PO is a Product Owner in one team and a Purchase Order in another. Length is not a counter-argument — an IRI is written by tooling and read in context, and the short form remains available through the short label and the acronym annotation. The single exception exists because a relation name is read on every edge of the graph and in every query, where the spoken form is what makes it legible; reproducing the target's short label verbatim also keeps the relation name derivable from the class rather than invented, which a truncated form would not.
| From | To | Note |
|---|---|---|
| pr:KPI | pr:KeyPerformanceIndicator | short form moves to pr:acronym |
| pr:BIDataSource | pr:BusinessIntelligenceDataSource | |
| pr:definitionUri | pr:definitionAddress | URI is itself an acronym |
| a relation targeting a class whose short label is BI Field | pr:containsBIField | the one exception — see TN-011 |
TN-003 — Display properties are annotations
Statement. pr:acronym and pr:shortLabel are declared owl:AnnotationProperty, never owl:DatatypeProperty.
| Severity | BLOCKING |
| Applies to | annotation_property |
| Enforced at | script |
| Executor | check_tbox_naming.py |
| Procedure | pending |
Why. A datatype property applies to an individual. Annotating a term of the vocabulary with one takes the ontology out of OWL DL into OWL Full, which reasoners reject and strict validators flag. An annotation property applies to anything — class, property or individual. Nothing is lost on the control side: SHACL validates triples and does not read OWL declarations, so every shape targeting these properties keeps working unchanged.
| From | To | Note |
|---|---|---|
| pr:shortLabel a owl:DatatypeProperty | pr:shortLabel a owl:AnnotationProperty | |
| pr:acronym a owl:DatatypeProperty | pr:acronym a owl:AnnotationProperty |
TN-004 — No digit in the IRI
Statement. The local name of a vocabulary term contains no digit. A numeric parameter — a window, a threshold, a version — is a value, never an identity.
| Severity | BLOCKING |
| Applies to | class, object_property, datatype_property, annotation_property |
| Enforced at | script |
| Executor | check_tbox_naming.py |
| Procedure | pending |
| Related | A-Box NR-014 (deliberate opposite) |
Why. An instance identifier does encode numbers, and that is its purpose: it carries a position in the domain tree. A vocabulary term encodes nothing but its own meaning. The opposition is deliberate and is stated in the ontology header so that no reader takes one for a violation of the other. A term named after a thirty-day window starts lying the day the window becomes ninety, and nothing detects it.
| From | To | Note |
|---|---|---|
| pr:queryCount30d | pr:queryCount | the window belongs to the harvesting specification |
TN-005 — A class is a singular common noun
Statement. The local name of a class is a singular noun phrase. No plural, no verb. No type word such as Object, Entity or Item unless it names a genuine abstraction of the model. No suffix rule is imposed on abstract classes.
| Severity | BLOCKING |
| Applies to | class |
| Enforced at | script, human |
| Executor | check_tbox_naming.py |
| Procedure | pending |
| Related | A-Box NR-008, NR-013 |
Why. Abstractness is not inferable from a name and must not be encoded in one. The correspondence fails in both directions: classes can be abstract without any suffix, and classes ending in Object can be among the most heavily instantiated in the model. Renaming a central business term to satisfy a naming rule would trade meaning for symmetry. Abstractness is declared instead, by TN-007.
| From | To | Note |
|---|---|---|
| pr:DataDomains | pr:DataDomain | singular |
| pr:ActorObject | pr:Actor | no type word added for its own sake |
TN-006 — Every layer root is abstract
Statement. A class that serves as the root of a layer carries pr:isAbstract true.
| Severity | BLOCKING |
| Applies to | class |
| Enforced at | script |
| Executor | check_tbox_naming.py |
| Procedure | pending |
Why. A non-abstract layer root allows an instance to be typed as an object of that layer without saying which kind of object it is. That statement carries no information and no downstream control can repair it.
| From | To | Note |
|---|---|---|
| pr:PhysicalLayerObject with no declaration | pr:isAbstract true |
TN-007 — Every class declares whether it is abstract
Statement. Every owl:Class carries pr:isAbstract with an explicit boolean value. Absence of the property is a violation, not a default.
| Severity | BLOCKING |
| Applies to | class |
| Enforced at | script |
| Executor | check_tbox_naming.py |
| Procedure | pending |
Why. Where declaration is optional, nothing distinguishes a class deliberately left concrete from one where the question was never asked. Two situations make the compulsion necessary. A class with subclasses may be perfectly instantiable, and has been wrongly marked abstract by inference. And a class used as a classification axis may have no declared subclass at all, its members being typed by multiple typing on instances — its abstractness is then structurally unverifiable and only the explicit declaration carries it.
| From | To | Note |
|---|---|---|
| a class with no declaration | pr:isAbstract true or false | silence is a violation |
| pr:Metric inferred abstract because it has a subclass | pr:isAbstract false | concrete despite having a subclass |
TN-008 — Abstractness is declared, never inferred
Statement. Abstractness is read from pr:isAbstract and from nothing else. No consumer of the model — viewer, export, script or documentation generator — may derive it from the presence of subclasses, from the absence of instances, or from any other structural signal.
| Severity | BLOCKING |
| Applies to | class, consumer |
| Enforced at | human |
| Executor | consumer contract review |
| Procedure | pending |
Why. A declaration rule holds in the source only if it also binds every consumer. Otherwise each one re-invents its own inference and the model acquires as many answers as it has readers. This is the same failure mode that justified a governed short label rather than a display dictionary per viewer.
| From | To | Note |
|---|---|---|
| isAbstract = term.subclasses.length > 0 | isAbstract = term.isAbstract === true | consumer contract |
TN-009 — A relation begins with a verb
Statement. The local name of an object property begins with a verb, in lower case. Both the active form and the passive or participial form are admitted: a past participle is a verb in first position. No relation begins with a preposition or with a noun.
| Severity | BLOCKING |
| Applies to | object_property |
| Enforced at | script |
| Executor | check_tbox_naming.py |
| Procedure | pending |
Why. A relation reads as a verb and an attribute reads as a noun. The distinction is what lets a reader tell an edge from a field without opening the declaration. The explicit clause on participial forms is required: read literally, a verb-first rule would condemn a whole family of sound relations such as computedBy, storedIn and derivedFrom.
| From | To | Note |
|---|---|---|
| pr:inDatabase | pr:isInDatabase | a preposition is not a verb |
| pr:primaryLocation | pr:primarilyStoredIn | a noun is not a verb |
| pr:computedBy | pr:computedBy | participial form is conforming |
TN-010 — The is prefix is either a copula or a predicate
Statement. On an owl:ObjectProperty, is acts as a COPULA — a state verb before a preposition or adjective, whose target is a node of the graph. On a boolean datatype or annotation property, is acts as a PREDICATE — it introduces an adjective or participle and the value is true or false. No other use is admitted.
| Severity | BLOCKING |
| Applies to | object_property, datatype_property, annotation_property |
| Enforced at | script |
| Executor | check_tbox_naming.py |
| Procedure | pending |
Why. Conformance of a name beginning with is is never checkable on the name alone. A control asserting that every such term is a boolean produces false positives on every copula; the reverse control lets real problems through. The correct check reads the declared type of the property first, then applies the matching clause.
| From | To | Note |
|---|---|---|
| pr:isInSchema | copula | object property |
| pr:isNullable | predicate | datatype property of range xsd:boolean |
TN-011 — A relation names its target by its short label
Statement. A relation need not mention its target class at all. When it does, it uses the target class's pr:shortLabel AS IT STANDS — not the full label, not a truncation of the short label, and not a form invented for the occasion. Where the short label carries an acronym, the acronym is reproduced with it: this is the single exception admitted by TN-002, and TN-001 admits the consecutive capitals it produces.
| Severity | BLOCKING |
| Applies to | object_property |
| Enforced at | script |
| Executor | check_tbox_naming.py |
| Procedure | pending |
Why. Relation names are what is read on the edges of the graph, constantly, in every viewer and every query. Full labels make them unreadable on an arrow. The short label is the form the organisation actually speaks, and reusing it makes a relation name predictable from the class it points at instead of arbitrary. Reproducing it whole is what preserves that property: a truncation would be a form the model invented, unpredictable from the class and unverifiable against it. A corollary: relations do not normally carry a short label of their own, since this rule already keeps their IRIs short.
| From | To | Note |
|---|---|---|
| pr:hasDomainOwner | pr:hasDomainOwner | target DataDomainOwner, short label Domain Owner — conforming |
| pr:hasDGL | pr:hasGovernanceLead | target short label is Governance Lead |
| pr:inBIDataSource | pr:isInBIDataSource | short label BI Data Source reproduced whole, acronym included |
| pr:containsField | pr:containsBIField | a truncated short label is not admitted |
TN-012 — An attribute is a noun
Statement. The local name of a datatype property is a noun phrase. It begins with no verb, and in particular with no has.
| Severity | BLOCKING |
| Applies to | datatype_property |
| Enforced at | script |
| Executor | check_tbox_naming.py |
| Procedure | pending |
Why. A has prefix carries no information — rdfs:domain already says who has the attribute — and it costs the reader the distinction TN-009 exists to make visible. Under TN-018 the derived label would read as a verb phrase on a field, so a relation and an attribute would become indistinguishable in every display.
| From | To | Note |
|---|---|---|
| pr:hasFormula | pr:formula | |
| pr:hasName | pr:canonicalName | name alone is too generic to stand as an identity |
TN-013 — A date attribute is a noun, not a participle
Statement. An attribute holding a date or timestamp is named as a noun.
| Severity | BLOCKING |
| Applies to | datatype_property |
| Enforced at | script |
| Executor | check_tbox_naming.py |
| Procedure | pending |
Why. The participial form is idiomatic English but creates a standing family of exceptions to TN-012 that must be defended at every review. The nominal form is consistent with the other attributes of the model and needs no defence.
| From | To | Note |
|---|---|---|
| pr:createdOn | pr:creationDate | |
| pr:lastQueriedOn | pr:lastQueryDate |
TN-014 — An attribute never takes a relational suffix
Statement. A datatype property never takes a form ending in By, In, From or On. Those forms are reserved for object properties. The only exception is the predicative is of booleans, covered by TN-015.
| Severity | BLOCKING |
| Applies to | datatype_property |
| Enforced at | script |
| Executor | check_tbox_naming.py |
| Procedure | pending |
Why. An attribute named like a relation will be read as one. A reader encountering a term in the shape of computedBy or ownedBy expects a node at the other end, not a string. The tell is usually visible in the label: a parenthetical gloss added to explain what the value actually holds is evidence that the name itself is wrong.
| From | To | Note |
|---|---|---|
| pr:definedBy holding a string | pr:definitionAddress | address, not location — location is taken by the storage side |
TN-015 — A boolean attribute begins with is
Statement. A property of range xsd:boolean begins with is, followed by an adjective or participle. This is the single explicit exception to TN-012.
| Severity | BLOCKING |
| Applies to | datatype_property, annotation_property |
| Enforced at | script |
| Executor | check_tbox_naming.py |
| Procedure | pending |
Why. A bare adjective does not read as a statement, and some of them collide with reserved words in the languages that consume the model.
| From | To | Note |
|---|---|---|
| pr:nullable | pr:isNullable | |
| pr:abstract | pr:isAbstract | abstract is a reserved word in several target languages |
TN-016 — A controlled value is an individual or a literal
Statement. A value becomes a TYPED INDIVIDUAL if it needs to be defined, owned, or extended by a domain without modifying the model. It stays a LITERAL if it is a closed technical state whose list is the property of governance and must precisely not be extended. The model chooses one form per kind of value and states the choice.
| Severity | BLOCKING |
| Applies to | class, individual, datatype_property |
| Enforced at | shacl, human |
| Executor | pr_metamodel_shapes.ttl |
| Procedure | pending |
| Related | A-Box NR-016 |
Why. A literal cannot carry a definition, an owner or a link, and extending its list means editing the shapes and therefore the frozen vocabulary. A typed individual can be added by a domain without touching the model, appears as a node in every viewer, and can be reached by following an edge. The test is not how the value looks but who is allowed to add one.
| From | To | Note |
|---|---|---|
| an activation level defined in a playbook, not in the graph | literal constrained by sh:in | not domain-extensible |
| a kind of system a domain may need to add | typed individual | extensible and worth defining |
TN-017 — Controlled literals are UPPER_SNAKE_CASE
Statement. Controlled literal values are written UPPER_SNAKE_CASE. This is the only place in the model where that casing is used.
| Severity | BLOCKING |
| Applies to | datatype_property |
| Enforced at | shacl |
| Executor | pr_metamodel_shapes.ttl |
| Procedure | pending |
Why. The casing exists so that a value drawn from a closed list is distinguishable from free text at a glance, in the source and in any export.
| From | To | Note |
|---|---|---|
| "Full activation" | "FULL_ACTIVATION" | |
| "Published" | "PUBLISHED" |
Label — Derivation and display
The label is derived, not authored. Anything a reader needs that the derivation cannot produce lives in a dedicated channel: the short label for the spoken form, the comment for meaning.
TN-018 — The label is derived from the IRI
Statement. rdfs:label is obtained from the local name of the IRI by inserting a space at every case boundary. No word added, removed, reordered or substituted. The words of a class label keep their initial capital; the words of a property label are entirely lower case.
| Severity | BLOCKING |
| Applies to | class, object_property, datatype_property, annotation_property, individual |
| Enforced at | script |
| Executor | check_tbox_naming.py |
| Procedure | pending |
| Related | A-Box NR-005 (deliberate opposite) |
Why. A label carrying meaning the IRI does not is the symptom of a badly named IRI or of an incomplete comment — never of a legitimate exception to the label. Where a label has been quietly enriched to explain a term, the explanation belongs in rdfs:comment and the short form, if the organisation speaks one, in pr:shortLabel.
| From | To | Note |
|---|---|---|
| pr:BusinessIntelligenceDataSource | Business Intelligence Data Source | a class label keeps its capitals |
| pr:primarilyStoredIn | primarily stored in | a property label is lower case |
| "Ownership and Categorization Layer Object" | "Ownership Layer Object" | the added meaning moves to rdfs:comment |
TN-019 — No acronym in the label
Statement. The label introduces no acronym or abbreviation that the IRI does not already carry, including in a recapitalised form that the derivation would not produce. Where the IRI legitimately carries an acronym under TN-011, the derived label carries it too and nothing further is added.
| Severity | BLOCKING |
| Applies to | class, object_property, datatype_property, annotation_property, individual |
| Enforced at | script |
| Executor | check_tbox_naming.py |
| Procedure | pending |
Why. A direct corollary of TN-018, kept as a separate rule because it catches labels whose IRI is innocent: a term correctly named in full can still carry a hand-written label that reintroduces the short form. Stated as an introduction rather than a prohibition, it stays a pure corollary — whatever the IRI legitimately holds, the derivation reproduces, and nothing else.
| From | To | Note |
|---|---|---|
| "BI expression" | "expression" | the IRI was already conforming |
TN-020 — No state in the label
Statement. The label mentions no status, version or deprecation. State lives in an axiom — owl:deprecated, pr:status — never in text. Deprecated and active terms are never mixed in a single view: a consumer filters on the axiom at query time and presents deprecated terms separately.
| Severity | BLOCKING |
| Applies to | class, object_property, datatype_property, annotation_property, individual, consumer |
| Enforced at | script |
| Executor | check_tbox_naming.py |
| Procedure | pending |
Why. A term whose label repeats its state carries that state twice. The day one is changed without the other, the two contradict each other and nothing detects it. With the rule in force, the question of whether a term is deprecated has exactly one possible answer.
| From | To | Note |
|---|---|---|
| "belongs to domain (deprecated)" | "belongs to domain" | owl:deprecated carries the state |
TN-021 — No gloss or parenthesis in the label
Statement. No parenthesis, qualification or example appears in the label. They belong in rdfs:comment.
| Severity | BLOCKING |
| Applies to | class, object_property, datatype_property, annotation_property |
| Enforced at | script |
| Executor | check_tbox_naming.py |
| Procedure | pending |
Why. A parenthetical gloss is almost always the symptom of an IRI that does not say what it holds. The right response is usually to rename the term, not to annotate the label.
| From | To | Note |
|---|---|---|
| "defined by (code URI)" | "definition address" | rename the term rather than gloss the label |
| "query count (30 days)" | "query count" |
TN-022 — The short label carries the spoken form
Statement. A term carries pr:shortLabel ONLY where the form derived under TN-018 is not what the organisation writes or says. Where the two coincide, no short label is declared: a duplicate of the label carries no information and is one more thing to keep in step. The short label follows the casing of the label it replaces — title case for a class — and admits what the label cannot: acronyms, abbreviations, dropped words.
| Severity | MAJOR |
| Applies to | class, individual |
| Enforced at | human |
| Executor | review checklist |
| Procedure | pending |
Why. The short label is what makes the derivation rule bearable. Without it, a mechanical label impoverishes every screen; with it, display becomes explicit and centrally governed instead of being improvised by each consumer. Restricting it to the cases where it actually differs is what keeps it meaningful: a short label present on every term degenerates into a second copy of the label, drifting from it one entry at a time. It is MAJOR rather than BLOCKING because judging whether a derived form matches what people actually say is human work, and EV-011 forbids declaring a rule blocking with no executor.
| From | To | Note |
|---|---|---|
| "Data Sub Domain Owner" | short label Sub Domain Owner | the word Data is dropped in the spoken form |
| "Key Performance Indicator" | short label KPI, acronym KPI | the acronym channel is what makes TN-002 bearable |
| a short label reading Metric on a class labelled Metric | no short label | a duplicate of the label carries nothing |
TN-023 — The label is generated, never typed
Statement. rdfs:label is never written by hand. It is produced by the derivation function from the IRI and stored in the source. A hand-typed label is a violation even when its value happens to be correct.
| Severity | BLOCKING |
| Applies to | class, object_property, datatype_property, annotation_property, individual |
| Enforced at | script |
| Executor | check_tbox_naming.py |
| Procedure | pending |
Why. Generate and store, rather than store only or compute at read time. The label stays in the source so SPARQL and third-party tooling behave normally, while a script regenerates it at every commit and the gate fails if a stored label differs from the derived one. Redundancy becomes harmless because it is checked: rather than hoping two facts stay consistent, the model makes inconsistency detectable.
| From | To | Note |
|---|---|---|
| label absent | violation | |
| label differing from the derivation | violation — drift |
Declaration — Declarative completeness
Nothing is left to be guessed. What a term is, what it applies to, where it sits and where it came from are all stated, never inferred from structure or from silence.
TN-024 — Every active term carries a comment
Statement. rdfs:comment is mandatory on every active term. On a property the comment is structured: the definition, plus the scope constraint or the confusion to be avoided. On a class the form is free.
| Severity | BLOCKING |
| Applies to | class, object_property, datatype_property, annotation_property |
| Enforced at | script, human |
| Executor | check_tbox_naming.py |
| Procedure | pending |
Why. A comment that paraphrases the name prevents nothing. A good one prevents a specific error: it says where the property may and may not be declared, or which neighbouring term it must not be confused with. The structure is imposed on properties because they are where ambiguity costs most — they are asserted thousands of times by people who will not read the model. The script checks presence; only a reviewer checks value, which is why the rule names two tiers.
| From | To | Note |
|---|---|---|
| "the owner" | definition plus where it may be declared and what it excludes |
TN-025 — Every property declares its domain and range
Statement. rdfs:domain and rdfs:range are mandatory on every property, EXCEPT where the property is deliberately polymorphic. A polymorphic property states so in its comment and has its scope declared in SHACL. Silent absence is a violation; documented absence is not.
| Severity | BLOCKING |
| Applies to | object_property, datatype_property |
| Enforced at | script, human |
| Executor | check_tbox_naming.py |
| Procedure | pending |
Why. A property with no domain cannot be targeted by any control. But some properties are polymorphic by design, their scope controlled class by class in SHACL rather than by twin properties; giving those an rdfs:domain would trigger the RDFS retyping described in TN-028. The rule therefore separates the two cases rather than demanding a domain everywhere.
| From | To | Note |
|---|---|---|
| a polymorphic property with no domain and no comment | violation | silence is indistinguishable from omission |
| a polymorphic property with no domain | conforming | scope declared in SHACL |
TN-026 — Every term declares how it was authored
Statement. pr:authoringMode is mandatory on every term. pr:harvestSource is mandatory if and only if the mode is HARVESTED.
| Severity | BLOCKING |
| Applies to | class, object_property, datatype_property, annotation_property |
| Enforced at | script |
| Executor | check_tbox_naming.py |
| Procedure | pending |
Why. Provenance decides who may edit a term and what a divergence means. A term stating where its data comes from without stating that it is harvested, or declaring itself harvested without naming a source, is half-declared in a way no control can catch. Declared symmetrically, provenance also makes a harvester specifiable from the model itself rather than from a side document.
| From | To | Note |
|---|---|---|
| harvestSource present | authoringMode HARVESTED | |
| authoringMode HARVESTED, harvestSource absent | harvestSource declared |
TN-027 — A concrete class has one layer and one provenance
Statement. Every concrete class descends from exactly one layer root and from exactly one provenance axis. Controlled-vocabulary classes are outside the layers by nature — a named and closed exception.
| Severity | BLOCKING |
| Applies to | class |
| Enforced at | script |
| Executor | check_tbox_naming.py |
| Procedure | pending |
Why. A class with no layer is invisible to every layer-scoped control and to every view organised by layer; a class with two is ambiguous in both. Where a family of classes genuinely sits outside the data layers — actors, governance objects — the answer is an explicit root of its own, not silence. Adding such a root by INSERTION above an existing abstraction, rather than by renaming it, keeps the ranges that point at that abstraction readable: a range must state the nature of its target, not its position in the model.
| From | To | Note |
|---|---|---|
| a class with no parent | attached to its layer root | |
| renaming an abstraction into a layer root | inserting the layer root above it | preserves the meaning of every range that points at it |
TN-028 — A sub-property inherits the domain of its parent
Statement. Declaring rdfs:subPropertyOf does not shield a property from the rdfs:domain of its parent. Every assertion of a sub-property is also an assertion of the parent, so the parent's domain applies and its RDFS retyping takes effect.
| Severity | BLOCKING |
| Applies to | object_property, datatype_property |
| Enforced at | script, human |
| Executor | check_tbox_naming.py |
| Procedure | pending |
Why. In RDFS a domain is not a rejecting constraint but a retyping inference. Attaching a sub-property to a parent whose domain does not fit will silently retype its subjects — an object of one layer quietly becomes an object of another, entering counts, controls and traversals it was never part of. Where two relations share a verb but not a domain, they must not be merged and must not be linked by subPropertyOf; the unification, if one is needed, belongs on a shared upper property with no domain of its own.
| From | To | Note |
|---|---|---|
| a dashboard field asserted through a property whose domain is a warehouse column | the dashboard field is inferred to BE a warehouse column | silent retyping |
| linking the two by rdfs:subPropertyOf | no protection — the parent's domain still applies | |
| a shared upper property with no domain | end-to-end traversal without merging IRIs | the correct unification |
Evolution — Change and propagation
Every rule here is anchored on a real failure. They govern how the model changes without breaking what depends on it.
EV-001 — A rename is never an edit in place
Statement. A rename creates a new term and keeps the old one with owl:deprecated true and dcterms:isReplacedBy. No IRI is modified in place. The deprecated stub carries no edge: no subClassOf, no label, no comment — only its type and the two axioms.
| Severity | BLOCKING |
| Applies to | procedure |
| Enforced at | human |
| Executor | commit review |
| Procedure | pending |
| Related | A-Box LC-002 |
Why. Nothing propagates in RDF. An IRI is a string copied into every triple, so renaming means rewriting every triple where it appears as subject, predicate or object — declaration, subclass axioms, domain, range, instance typing, shape targets and paths, queries, viewers, documentation. Edges are moved, not duplicated. The stub exists so that a consumer holding an older export can still be told what replaced the term.
| From | To | Note |
|---|---|---|
| editing an IRI in place | new term plus deprecated stub |
EV-002 — A withdrawal requires proof of non-instantiation
Statement. A term is removed only after a count proving it was never instantiated, the count being attached to the commit. Without proof, deprecation only.
| Severity | BLOCKING |
| Applies to | procedure |
| Enforced at | script |
| Executor | count query attached to the commit |
| Procedure | pending |
Why. Removal is the one irreversible operation on a vocabulary. The proof is cheap — a single counting query — and it is what separates a safe withdrawal from a silent data loss.
| From | To | Note |
|---|---|---|
| deleting a term | SELECT (COUNT(?s) AS ?n) WHERE { ?s a pr:Term } | must return zero |
EV-003 — A deletion removes every reference to the subject
Statement. A subject removed while its IRI is still cited elsewhere is recreated by RDFS inference. Every reference is removed in the same operation.
| Severity | BLOCKING |
| Applies to | procedure |
| Enforced at | script |
| Executor | dangling-reference check in the migration script |
| Procedure | pending |
Why. A graph has no foreign keys. Deleting the block that declares a subject leaves every triple that names it intact, and a reasoner will reconstruct a hollow node from them — present in traversals, absent from every control.
| From | To | Note |
|---|---|---|
| deleting a subject block | deleting the block and every triple naming it |
EV-004 — A change is validated before it is applied
Statement. Every modification of the model is agreed before any artifact is written. Dry run first, apply second.
| Severity | BLOCKING |
| Applies to | procedure |
| Enforced at | human |
| Executor | commit review |
| Procedure | pending |
Why. The damage from a bad change is rarely in the change itself but in everything that silently depended on the old state. A dry run that reports what it would touch is what makes that dependency visible while it can still be discussed.
| From | To | Note |
|---|---|---|
| migrate.py --apply as the first run | migrate.py, then --apply | dry run is the default |
EV-005 — Graph edits go through an RDF parser
Statement. Any transformation of the graph is performed with an RDF parser. No regular expression applied line by line to Turtle.
| Severity | BLOCKING |
| Applies to | procedure, script |
| Enforced at | human |
| Executor | script review |
| Procedure | pending |
Why. Turtle is a structured syntax and a line is not a unit of meaning. A literal may contain the very characters a regex splits on, so a text-level edit that looks correct on every example can truncate a value on the one that matters. A parser knows the difference between a delimiter and a character inside a string; a regex does not, and every workaround for that is a partial reimplementation of the parser.
| From | To | Note |
|---|---|---|
| re.sub on the file text | remove and add on parsed triples | |
| hand-written clause splitting | parser | a workaround is evidence the wrong tool is in use |
EV-006 — Every script is idempotent
Statement. A script may be replayed without changing the result. Its guard tests the TARGET state, not the source state, and it counts its own result. Acceptance test: two consecutive runs, the second reporting zero modifications.
| Severity | BLOCKING |
| Applies to | script |
| Enforced at | script |
| Executor | double run in continuous integration |
| Procedure | pending |
Why. A chain of transformations is only safe if any step can be replayed. A guard that tests whether work remains to be done breaks as soon as another script has already changed the source; a guard that tests whether the target state exists survives it. The self-count is what turns the property into a verifiable one.
| From | To | Note |
|---|---|---|
| appending unconditionally | appending only if the target is absent | guard on the target state |
EV-007 — The replayable source is the start of the chain
Statement. The replayable source is the starting file plus the ordered sequence of scripts, never an intermediate committed state. Hand-editing a file produced by a script is forbidden, including for a typo: the script is corrected and the chain replayed.
| Severity | BLOCKING |
| Applies to | procedure |
| Enforced at | human |
| Executor | commit review |
| Procedure | pending |
Why. A committed intermediate file is a convenience, not a source. As soon as one hand edit exists in it that no script performs, replaying the chain no longer reproduces the committed state and nobody can say which of the two is right. Keeping the chain authoritative is also what makes a failed migration recoverable: restart from the last true source with a corrected script, rather than repair a half-migrated file by hand.
| From | To | Note |
|---|---|---|
| fixing a typo in a generated file | fixing the script and replaying |
EV-008 — Every artifact has a producer script
Statement. No artifact contains hard-coded data. Every deliverable — viewer, documentation, workbook, diagram — is regenerated from a versioned source by a script.
| Severity | BLOCKING |
| Applies to | script, artifact |
| Enforced at | human |
| Executor | artifact review |
| Procedure | pending |
Why. An artifact that cannot be regenerated becomes stale the moment the model moves, and there is no way to tell whether it is stale or merely different. This rulebook applies the rule to itself: the source is one file, the document and the workbook are generated from it.
| From | To | Note |
|---|---|---|
| a hand-maintained workbook | a source file plus a generator |
EV-009 — A change propagates in a single merge
Statement. A change is propagated to the vocabulary, the instances, the shapes, the consumers and the documentation in one branch, merged as a block. The main line never sees a state where one has moved and another has not.
| Severity | BLOCKING |
| Applies to | procedure |
| Enforced at | human |
| Executor | merge review |
| Procedure | pending |
Why. Partial propagation is not a smaller version of a change, it is a different and invalid state. Its worst form is silent: a gate that validates the stale half and reports success. A single merge makes the intermediate state unreachable rather than merely discouraged.
| From | To | Note |
|---|---|---|
| committing the vocabulary and updating the shapes later | one branch |
EV-010 — Shapes declare the ontology version they target
Statement. The shapes graph declares dcterms:conformsTo equal to the owl:versionIRI of the ontology. Any divergence aborts validation immediately.
| Severity | BLOCKING |
| Applies to | shape, script |
| Enforced at | script |
| Executor | run_shacl_validation.py |
| Procedure | pending |
Why. Validating against the wrong shapes does not fail loudly. It produces a long list of violations that reads exactly like a regression of the model, and the time goes into interpreting them rather than noticing the mismatch. One declared triple and one check turn that into a single line naming the real problem.
| From | To | Note |
|---|---|---|
| hundreds of violations to interpret | ABORT — shapes target a different ontology version |
EV-011 — No blocking rule without an executor
Statement. A rule may be declared BLOCKING only if a named script or shape enforces it. A blocking rule with no control is a statement of intent.
| Severity | BLOCKING |
| Applies to | rule |
| Enforced at | script |
| Executor | rulebook source self-check |
| Procedure | pending |
Why. An unenforced blocking rule is worse than an absent one: it is cited as though it held, and the gap only surfaces when something it should have caught reaches production. Making the executor a mandatory field turns the omission into something a script can detect on the rulebook itself.
| From | To | Note |
|---|---|---|
| severity BLOCKING with no executor | violation | detected by the generators before they write anything |
EV-012 — Every rule names the tier that enforces it
Statement. Controls exist at three tiers — SHACL for the shape of the graph, scripts for the file, the lexicon and naming, human review for semantics. Every rule names its tier. A rule with no assigned tier is a violation.
| Severity | BLOCKING |
| Applies to | rule |
| Enforced at | script |
| Executor | rulebook source self-check |
| Procedure | pending |
Why. SHACL validates a graph, not a file, and not meaning. Two IRIs standing for the same notion produce two individually valid graphs; a duplicated block of text produces identical triples and no complaint. Naming the tier forces the question of what actually catches each rule, and pushes the ones SHACL cannot see to a control that can: a normalised-label index reviewed by a person, a file-level hash, a naming script, a review checklist.
| From | To | Note |
|---|---|---|
| two IRIs for one notion | normalised label index plus human review | SHACL cannot see it |
| a duplicated text block | file-level block hashing | identical triples |
EV-013 — No sh:pattern outside the SHACL specification
Statement. sh:pattern uses XPath 2.0 regular expressions, which support neither lookbehind nor lookahead. A constraint is expressed positively — what the string must be — rather than negatively. Where that is impossible the rule leaves SHACL and moves to the script tier under EV-012.
| Severity | BLOCKING |
| Applies to | shape |
| Enforced at | script |
| Executor | run_shacl_validation.py |
| Procedure | pending |
| Related | A-Box NR-001 |
Why. A pattern written in a richer regex dialect does not degrade gracefully: the validator fails to compile it, and the rule that depended on it silently stops being enforced while still being declared blocking.
| From | To | Note |
|---|---|---|
| a pattern using (?<! ... ) | a pattern describing the whole accepted string | express positively |
EV-014 — The term namespace is never versioned
Statement. The namespace of terms is not versioned. The version lives in owl:versionIRI alone.
| Severity | BLOCKING |
| Applies to | ontology |
| Enforced at | script |
| Executor | check_tbox_naming.py |
| Procedure | pending |
Why. A versioned term namespace changes every IRI at every release, breaking every consumer for no benefit. It also fails silently in the other direction: a file bound to an outdated namespace targets IRIs that no longer exist, so every shape matches nothing and validation passes while testing nothing at all.
| From | To | Note |
|---|---|---|
| a prefix bound to a versioned namespace | a prefix bound to the stable namespace | the version lives in owl:versionIRI |
EV-015 — Every procedure that writes produces an execution log
Statement. Any procedure that modifies the graph writes a versioned execution log, committed alongside the change. The log records the attempt number and timestamp, a checksum of every input consumed, the ontology version and the version the shapes declare they target, a count per step, the full validation report, and a difference against the previous attempt.
| Severity | BLOCKING |
| Applies to | procedure, script |
| Enforced at | script |
| Executor | the procedure's own runner |
| Procedure | pending |
Why. A counter printed to the terminal disappears with the terminal. When a validation run fails, the question is never only what failed but whether it is the same failure as the previous attempt: a violation that persists unchanged after a correction says the cause lies elsewhere, while one that moves every time says the change is being patched rather than fixed. Those are opposite diagnoses and neither is available without a record of the previous run. The input checksums serve the same purpose one level down, separating a wrong model from a stale file — the ambiguity that made a past incident expensive to explain. The log is also the natural carrier of the proof of non-instantiation required by EV-002, which otherwise has no defined format.
| From | To | Note |
|---|---|---|
| a counter printed to standard output | a versioned log committed with the change | |
| a failed run with no record of the previous one | a difference against the previous attempt | what makes correct-or-abandon decidable |
Annex A — Abstractness
Applies TN-006 and TN-007 to the model. 44 classes, of which 13 abstract and 31 concrete. Maintained with the model, not after it.
| Class | isAbstract | Note |
|---|---|---|
MetaModelObject |
true | root of the model |
DefinedObject |
true | provenance axis |
CapturedObject |
true | provenance axis |
OwnershipLayerObject |
true | layer root |
BusinessLayerObject |
true | layer root |
LogicalLayerObject |
true | layer root |
PhysicalLayerObject |
true | NEW — omission in v1.6 |
DeliveryLayerObject |
true | layer root |
ConsumptionLayerObject |
true | layer root |
GovernanceLayerObject |
true | NEW — seventh layer root |
DataStructure |
true | harvesting types the concrete sort |
KeyConstraint |
true | only PrimaryKey and ForeignKey instantiate |
Actor |
true | renaming would degrade the range of ownedBy and hasPublisher |
DataDomain |
false | |
DataSubDomain |
false | |
BusinessObject |
false | ends in Object and is heavily instantiated - refutes any suffix rule |
BusinessConcept |
false | |
Metric |
false | concrete despite having KeyPerformanceIndicator beneath it |
KeyPerformanceIndicator |
false | |
DataObject |
false | |
DataElement |
false | |
Database |
false | |
Schema |
false | |
Field |
false | |
System |
false | |
Transformation |
false | |
BaseTable |
false | |
ExternalTable |
false | |
View |
false | |
PrimaryKey |
false | |
ForeignKey |
false | |
DataProduct |
false | |
DataContract |
false | |
DataInterface |
false | |
BusinessIntelligenceWorkspace |
false | |
BusinessIntelligenceDataSource |
false | |
BusinessIntelligenceField |
false | |
BusinessIntelligenceReport |
false | |
DataDomainOwner |
false | |
DataSubDomainOwner |
false | |
DataProductOwner |
false | |
DataSteward |
false | |
DataGovernanceLead |
false | |
SystemType |
false | must stay concrete — it types five individuals |
Annex B — Display
Applies TN-011 and TN-022. The short label is what screens display and what relation names reuse; the acronym is a search key, never an identity.
| IRI | rdfs:label | shortLabel | acronym | Note |
|---|---|---|---|---|
MetaModelObject |
Meta Model Object | - | - | |
DefinedObject |
Defined Object | - | - | |
CapturedObject |
Captured Object | - | - | |
OwnershipLayerObject |
Ownership Layer Object | Ownership Layer | - | the categorisation role moves into rdfs:comment |
BusinessLayerObject |
Business Layer Object | Business Layer | - | |
LogicalLayerObject |
Logical Layer Object | Logical Layer | - | |
PhysicalLayerObject |
Physical Layer Object | Physical Layer | - | |
DeliveryLayerObject |
Delivery Layer Object | Delivery Layer | - | |
ConsumptionLayerObject |
Consumption Layer Object | Consumption Layer | - | |
GovernanceLayerObject |
Governance Layer Object | Governance Layer | - | |
DataStructure |
Data Structure | - | - | the former short label was a gloss and belongs in rdfs:comment |
KeyConstraint |
Key Constraint | - | - | |
Actor |
Actor | - | - | |
DataDomain |
Data Domain | Domain | DD | DD is already the short form used inside instance identifiers |
DataSubDomain |
Data Sub Domain | Sub Domain | SD | no hyphen, for consistency |
BusinessObject |
Business Object | - | BO | |
BusinessConcept |
Business Concept | Concept | - | drives the name of usesConcept |
Metric |
Metric | - | - | |
KeyPerformanceIndicator |
Key Performance Indicator | KPI | KPI | the acronym channel is what makes TN-002 bearable |
DataObject |
Data Object | - | - | |
DataElement |
Data Element | Element | - | drives the name of hasElement and hasGrainElement |
Database |
Database | - | - | |
Schema |
Schema | - | - | |
Field |
Field | - | - | |
System |
System | - | - | |
SystemType |
System Type | - | - | |
Transformation |
Transformation | - | - | |
BaseTable |
Base Table | - | - | |
ExternalTable |
External Table | - | - | |
View |
View | - | - | |
PrimaryKey |
Primary Key | - | - | |
ForeignKey |
Foreign Key | - | - | |
DataProduct |
Data Product | - | - | |
DataContract |
Data Contract | - | - | |
DataInterface |
Data Interface | - | - | |
BusinessIntelligenceWorkspace |
Business Intelligence Workspace | BI Workspace | - | |
BusinessIntelligenceDataSource |
Business Intelligence Data Source | BI Data Source | - | |
BusinessIntelligenceField |
Business Intelligence Field | BI Field | - | reproduced whole in containsBIField under TN-011 |
BusinessIntelligenceReport |
Business Intelligence Report | BI Report | - | |
DataDomainOwner |
Data Domain Owner | Domain Owner | DDO | |
DataSubDomainOwner |
Data Sub Domain Owner | Sub Domain Owner | SDO | |
DataProductOwner |
Data Product Owner | Product Owner | PO | not DPO - the initialism is taken by Data Protection Officer, a real GDPR role |
DataSteward |
Data Steward | Steward | - | |
DataGovernanceLead |
Data Governance Lead | Governance Lead | DGL |