Files

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