# PERNOD RICARD DATA METAMODEL — T-BOX RULEBOOK # ============================================================================= # SINGLE SOURCE OF TRUTH. The .md and .xlsx deliverables are GENERATED from this # file and must never be edited by hand (EV-007, EV-008). # # python3 generate_rulebook_md.py # python3 generate_rulebook_xlsx.py # # severity : BLOCKING | MAJOR | GUIDELINE — one per rule. The rule is the # rule; transitional tolerance belongs to a migration process, # not to a normative document. # control.tier : shacl | script | human (EV-012 — where the rule is enforced) # control.executor : the script or shape that enforces it (EV-011 — no blocking # rule without an executor) # control.procedure: filled in by the processbook. null until then. # ============================================================================= meta: title: Pernod Ricard Data MetaModel — T-Box Rulebook version: "1.2" status: Draft for review date: "2026-08-03" 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. audience: > 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: Identifier: title: Form of the IRI intent: > 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. Label: title: Derivation and display intent: > 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. Declaration: title: Declarative completeness intent: > 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. Evolution: title: Change and propagation intent: > Every rule here is anchored on a real failure. They govern how the model changes without breaking what depends on it. severity_model: 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. rules: # ---------------------------------------------------------------- IDENTIFIER - id: TN-001 category: Identifier title: 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. scope: [class, object_property, datatype_property, annotation_property, individual] severity: BLOCKING control: {tier: script, executor: check_tbox_naming.py, procedure: null} filiation: null rationale: > 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. examples: - {from: "pr:data_domain", to: "pr:DataDomain", note: no underscore} - {from: "pr:Sub-Domain", to: "pr:DataSubDomain", note: no hyphen} - {from: "pr:BelongsTo", to: "pr:belongsTo", note: a property starts lower case} - id: TN-002 category: Identifier title: 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. scope: [class, object_property, datatype_property, annotation_property, individual] severity: BLOCKING control: {tier: script, executor: check_tbox_naming.py, procedure: null} filiation: A-Box NR-004 rationale: > 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. examples: - {from: "pr:KPI", to: "pr:KeyPerformanceIndicator", note: "short form moves to pr:acronym"} - {from: "pr:BIDataSource", to: "pr:BusinessIntelligenceDataSource", note: null} - {from: "pr:definitionUri", to: "pr:definitionAddress", note: "URI is itself an acronym"} - {from: "a relation targeting a class whose short label is BI Field", to: "pr:containsBIField", note: "the one exception — see TN-011"} - id: TN-003 category: Identifier title: Display properties are annotations statement: > pr:acronym and pr:shortLabel are declared owl:AnnotationProperty, never owl:DatatypeProperty. scope: [annotation_property] severity: BLOCKING control: {tier: script, executor: check_tbox_naming.py, procedure: null} filiation: null rationale: > 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. examples: - {from: "pr:shortLabel a owl:DatatypeProperty", to: "pr:shortLabel a owl:AnnotationProperty", note: null} - {from: "pr:acronym a owl:DatatypeProperty", to: "pr:acronym a owl:AnnotationProperty", note: null} - id: TN-004 category: Identifier title: 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. scope: [class, object_property, datatype_property, annotation_property] severity: BLOCKING control: {tier: script, executor: check_tbox_naming.py, procedure: null} filiation: A-Box NR-014 (deliberate opposite) rationale: > 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. examples: - {from: "pr:queryCount30d", to: "pr:queryCount", note: the window belongs to the harvesting specification} - id: TN-005 category: Identifier title: 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. scope: [class] severity: BLOCKING control: {tier: [script, human], executor: check_tbox_naming.py, procedure: null} filiation: A-Box NR-008, NR-013 rationale: > 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. examples: - {from: "pr:DataDomains", to: "pr:DataDomain", note: singular} - {from: "pr:ActorObject", to: "pr:Actor", note: no type word added for its own sake} - id: TN-006 category: Identifier title: Every layer root is abstract statement: > A class that serves as the root of a layer carries pr:isAbstract true. scope: [class] severity: BLOCKING control: {tier: script, executor: check_tbox_naming.py, procedure: null} filiation: null rationale: > 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. examples: - {from: "pr:PhysicalLayerObject with no declaration", to: "pr:isAbstract true", note: null} - id: TN-007 category: Identifier title: 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. scope: [class] severity: BLOCKING control: {tier: script, executor: check_tbox_naming.py, procedure: null} filiation: null rationale: > 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. examples: - {from: "a class with no declaration", to: "pr:isAbstract true or false", note: silence is a violation} - {from: "pr:Metric inferred abstract because it has a subclass", to: "pr:isAbstract false", note: concrete despite having a subclass} - id: TN-008 category: Identifier title: 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. scope: [class, consumer] severity: BLOCKING control: {tier: human, executor: consumer contract review, procedure: null} filiation: null rationale: > 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. examples: - {from: "isAbstract = term.subclasses.length > 0", to: "isAbstract = term.isAbstract === true", note: consumer contract} - id: TN-009 category: Identifier title: 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. scope: [object_property] severity: BLOCKING control: {tier: script, executor: check_tbox_naming.py, procedure: null} filiation: null rationale: > 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. examples: - {from: "pr:inDatabase", to: "pr:isInDatabase", note: a preposition is not a verb} - {from: "pr:primaryLocation", to: "pr:primarilyStoredIn", note: a noun is not a verb} - {from: "pr:computedBy", to: "pr:computedBy", note: participial form is conforming} - id: TN-010 category: Identifier title: 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. scope: [object_property, datatype_property, annotation_property] severity: BLOCKING control: {tier: script, executor: check_tbox_naming.py, procedure: null} filiation: null rationale: > 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. examples: - {from: "pr:isInSchema", to: copula, note: object property, target is a node} - {from: "pr:isNullable", to: predicate, note: datatype property of range xsd:boolean} - id: TN-011 category: Identifier title: 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. scope: [object_property] severity: BLOCKING control: {tier: script, executor: check_tbox_naming.py, procedure: null} filiation: null rationale: > 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. examples: - {from: "pr:hasDomainOwner", to: "pr:hasDomainOwner", note: "target DataDomainOwner, short label Domain Owner — conforming"} - {from: "pr:hasDGL", to: "pr:hasGovernanceLead", note: "target short label is Governance Lead"} - {from: "pr:inBIDataSource", to: "pr:isInBIDataSource", note: "short label BI Data Source reproduced whole, acronym included"} - {from: "pr:containsField", to: "pr:containsBIField", note: "a truncated short label is not admitted"} - id: TN-012 category: Identifier title: 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. scope: [datatype_property] severity: BLOCKING control: {tier: script, executor: check_tbox_naming.py, procedure: null} filiation: null rationale: > 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. examples: - {from: "pr:hasFormula", to: "pr:formula", note: null} - {from: "pr:hasName", to: "pr:canonicalName", note: name alone is too generic to stand as an identity} - id: TN-013 category: Identifier title: A date attribute is a noun, not a participle statement: > An attribute holding a date or timestamp is named as a noun. scope: [datatype_property] severity: BLOCKING control: {tier: script, executor: check_tbox_naming.py, procedure: null} filiation: null rationale: > 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. examples: - {from: "pr:createdOn", to: "pr:creationDate", note: null} - {from: "pr:lastQueriedOn", to: "pr:lastQueryDate", note: null} - id: TN-014 category: Identifier title: 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. scope: [datatype_property] severity: BLOCKING control: {tier: script, executor: check_tbox_naming.py, procedure: null} filiation: null rationale: > 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. examples: - {from: "pr:definedBy holding a string", to: "pr:definitionAddress", note: "address, not location — location is taken by the storage side"} - id: TN-015 category: Identifier title: 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. scope: [datatype_property, annotation_property] severity: BLOCKING control: {tier: script, executor: check_tbox_naming.py, procedure: null} filiation: null rationale: > A bare adjective does not read as a statement, and some of them collide with reserved words in the languages that consume the model. examples: - {from: "pr:nullable", to: "pr:isNullable", note: null} - {from: "pr:abstract", to: "pr:isAbstract", note: "abstract is a reserved word in several target languages"} - id: TN-016 category: Identifier title: 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. scope: [class, individual, datatype_property] severity: BLOCKING control: {tier: [shacl, human], executor: pr_metamodel_shapes.ttl, procedure: null} filiation: A-Box NR-016 rationale: > 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. examples: - {from: "an activation level defined in a playbook, not in the graph", to: "literal constrained by sh:in", note: not domain-extensible} - {from: "a kind of system a domain may need to add", to: typed individual, note: extensible and worth defining} - id: TN-017 category: Identifier title: 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. scope: [datatype_property] severity: BLOCKING control: {tier: shacl, executor: pr_metamodel_shapes.ttl, procedure: null} filiation: null rationale: > 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. examples: - {from: "\"Full activation\"", to: "\"FULL_ACTIVATION\"", note: null} - {from: "\"Published\"", to: "\"PUBLISHED\"", note: null} # --------------------------------------------------------------------- LABEL - id: TN-018 category: Label title: 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. scope: [class, object_property, datatype_property, annotation_property, individual] severity: BLOCKING control: {tier: script, executor: check_tbox_naming.py, procedure: null} filiation: A-Box NR-005 (deliberate opposite) rationale: > 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. examples: - {from: "pr:BusinessIntelligenceDataSource", to: "Business Intelligence Data Source", note: a class label keeps its capitals} - {from: "pr:primarilyStoredIn", to: "primarily stored in", note: a property label is lower case} - {from: "\"Ownership and Categorization Layer Object\"", to: "\"Ownership Layer Object\"", note: the added meaning moves to rdfs:comment} - id: TN-019 category: Label title: 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. scope: [class, object_property, datatype_property, annotation_property, individual] severity: BLOCKING control: {tier: script, executor: check_tbox_naming.py, procedure: null} filiation: null rationale: > 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. examples: - {from: "\"BI expression\"", to: "\"expression\"", note: the IRI was already conforming} - id: TN-020 category: Label title: 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. scope: [class, object_property, datatype_property, annotation_property, individual, consumer] severity: BLOCKING control: {tier: script, executor: check_tbox_naming.py, procedure: null} filiation: null rationale: > 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. examples: - {from: "\"belongs to domain (deprecated)\"", to: "\"belongs to domain\"", note: owl:deprecated carries the state} - id: TN-021 category: Label title: No gloss or parenthesis in the label statement: > No parenthesis, qualification or example appears in the label. They belong in rdfs:comment. scope: [class, object_property, datatype_property, annotation_property] severity: BLOCKING control: {tier: script, executor: check_tbox_naming.py, procedure: null} filiation: null rationale: > 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. examples: - {from: "\"defined by (code URI)\"", to: "\"definition address\"", note: rename the term rather than gloss the label} - {from: "\"query count (30 days)\"", to: "\"query count\"", note: null} - id: TN-022 category: Label title: 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. scope: [class, individual] severity: MAJOR control: {tier: human, executor: review checklist, procedure: null} filiation: null rationale: > 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. examples: - {from: "\"Data Sub Domain Owner\"", to: "short label Sub Domain Owner", note: the word Data is dropped in the spoken form} - {from: "\"Key Performance Indicator\"", to: "short label KPI, acronym KPI", note: the acronym channel is what makes TN-002 bearable} - {from: "a short label reading Metric on a class labelled Metric", to: no short label, note: a duplicate of the label carries nothing} - id: TN-023 category: Label title: 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. scope: [class, object_property, datatype_property, annotation_property, individual] severity: BLOCKING control: {tier: script, executor: check_tbox_naming.py, procedure: null} filiation: null rationale: > 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. examples: - {from: label absent, to: violation, note: null} - {from: label differing from the derivation, to: "violation — drift", note: null} # --------------------------------------------------------------- DECLARATION - id: TN-024 category: Declaration title: 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. scope: [class, object_property, datatype_property, annotation_property] severity: BLOCKING control: {tier: [script, human], executor: check_tbox_naming.py, procedure: null} filiation: null rationale: > 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. examples: - {from: "\"the owner\"", to: "definition plus where it may be declared and what it excludes", note: null} - id: TN-025 category: Declaration title: 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. scope: [object_property, datatype_property] severity: BLOCKING control: {tier: [script, human], executor: check_tbox_naming.py, procedure: null} filiation: null rationale: > 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. examples: - {from: a polymorphic property with no domain and no comment, to: violation, note: silence is indistinguishable from omission} - {from: a polymorphic property with no domain, documented, to: conforming, note: scope declared in SHACL} - id: TN-026 category: Declaration title: 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. scope: [class, object_property, datatype_property, annotation_property] severity: BLOCKING control: {tier: script, executor: check_tbox_naming.py, procedure: null} filiation: null rationale: > 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. examples: - {from: harvestSource present, authoringMode absent, to: "authoringMode HARVESTED", note: null} - {from: "authoringMode HARVESTED, harvestSource absent", to: harvestSource declared, note: null} - id: TN-027 category: Declaration title: 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. scope: [class] severity: BLOCKING control: {tier: script, executor: check_tbox_naming.py, procedure: null} filiation: null rationale: > 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. examples: - {from: a class with no parent, to: attached to its layer root, note: null} - {from: renaming an abstraction into a layer root, to: inserting the layer root above it, note: preserves the meaning of every range that points at it} - id: TN-028 category: Declaration title: 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. scope: [object_property, datatype_property] severity: BLOCKING control: {tier: [script, human], executor: check_tbox_naming.py, procedure: null} filiation: null rationale: > 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. examples: - {from: "a dashboard field asserted through a property whose domain is a warehouse column", to: "the dashboard field is inferred to BE a warehouse column", note: silent retyping} - {from: "linking the two by rdfs:subPropertyOf", to: "no protection — the parent's domain still applies", note: null} - {from: "a shared upper property with no domain", to: "end-to-end traversal without merging IRIs", note: the correct unification} # ----------------------------------------------------------------- EVOLUTION - id: EV-001 category: Evolution title: 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. scope: [procedure] severity: BLOCKING control: {tier: human, executor: commit review, procedure: null} filiation: A-Box LC-002 rationale: > 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. examples: - {from: editing an IRI in place, to: new term plus deprecated stub, note: null} - id: EV-002 category: Evolution title: 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. scope: [procedure] severity: BLOCKING control: {tier: script, executor: count query attached to the commit, procedure: null} filiation: null rationale: > 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. examples: - {from: deleting a term, to: "SELECT (COUNT(?s) AS ?n) WHERE { ?s a pr:Term }", note: must return zero} - id: EV-003 category: Evolution title: 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. scope: [procedure] severity: BLOCKING control: {tier: script, executor: dangling-reference check in the migration script, procedure: null} filiation: null rationale: > 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. examples: - {from: deleting a subject block, to: deleting the block and every triple naming it, note: null} - id: EV-004 category: Evolution title: 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. scope: [procedure] severity: BLOCKING control: {tier: human, executor: commit review, procedure: null} filiation: null rationale: > 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. examples: - {from: "migrate.py --apply as the first run", to: "migrate.py, then --apply", note: dry run is the default} - id: EV-005 category: Evolution title: 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. scope: [procedure, script] severity: BLOCKING control: {tier: human, executor: script review, procedure: null} filiation: null rationale: > 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. examples: - {from: "re.sub on the file text", to: remove and add on parsed triples, note: null} - {from: hand-written clause splitting, to: parser, note: a workaround is evidence the wrong tool is in use} - id: EV-006 category: Evolution title: 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. scope: [script] severity: BLOCKING control: {tier: script, executor: double run in continuous integration, procedure: null} filiation: null rationale: > 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. examples: - {from: appending unconditionally, to: appending only if the target is absent, note: guard on the target state} - id: EV-007 category: Evolution title: 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. scope: [procedure] severity: BLOCKING control: {tier: human, executor: commit review, procedure: null} filiation: null rationale: > 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. examples: - {from: fixing a typo in a generated file, to: fixing the script and replaying, note: null} - id: EV-008 category: Evolution title: 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. scope: [script, artifact] severity: BLOCKING control: {tier: human, executor: artifact review, procedure: null} filiation: null rationale: > 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. examples: - {from: a hand-maintained workbook, to: a source file plus a generator, note: null} - id: EV-009 category: Evolution title: 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. scope: [procedure] severity: BLOCKING control: {tier: human, executor: merge review, procedure: null} filiation: null rationale: > 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. examples: - {from: committing the vocabulary and updating the shapes later, to: one branch, one merge, one tag, note: null} - id: EV-010 category: Evolution title: 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. scope: [shape, script] severity: BLOCKING control: {tier: script, executor: run_shacl_validation.py, procedure: null} filiation: null rationale: > 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. examples: - {from: hundreds of violations to interpret, to: "ABORT — shapes target a different ontology version", note: null} - id: EV-011 category: Evolution title: 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. scope: [rule] severity: BLOCKING control: {tier: script, executor: rulebook source self-check, procedure: null} filiation: null rationale: > 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. examples: - {from: "severity BLOCKING with no executor", to: violation, note: detected by the generators before they write anything} - id: EV-012 category: Evolution title: 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. scope: [rule] severity: BLOCKING control: {tier: script, executor: rulebook source self-check, procedure: null} filiation: null rationale: > 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. examples: - {from: two IRIs for one notion, to: normalised label index plus human review, note: SHACL cannot see it} - {from: a duplicated text block, to: file-level block hashing, note: identical triples, no violation} - id: EV-013 category: Evolution title: 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. scope: [shape] severity: BLOCKING control: {tier: script, executor: run_shacl_validation.py, procedure: null} filiation: A-Box NR-001 rationale: > 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. examples: - {from: "a pattern using (? The namespace of terms is not versioned. The version lives in owl:versionIRI alone. scope: [ontology] severity: BLOCKING control: {tier: script, executor: check_tbox_naming.py, procedure: null} filiation: null rationale: > 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. examples: - {from: "a prefix bound to a versioned namespace", to: "a prefix bound to the stable namespace", note: the version lives in owl:versionIRI} - id: EV-015 category: Evolution title: 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. scope: [procedure, script] severity: BLOCKING control: {tier: script, executor: the procedure's own runner, procedure: null} filiation: null rationale: > 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. examples: - {from: a counter printed to standard output, to: a versioned log committed with the change, note: null} - {from: "a failed run with no record of the previous one", to: "a difference against the previous attempt", note: what makes correct-or-abandon decidable} # ====== ANNEX: ABSTRACTNESS (TN-006, TN-007) ====== abstractness: - {term: MetaModelObject, is_abstract: true, note: "root of the model"} - {term: DefinedObject, is_abstract: true, note: "provenance axis"} - {term: CapturedObject, is_abstract: true, note: "provenance axis"} - {term: OwnershipLayerObject, is_abstract: true, note: "layer root"} - {term: BusinessLayerObject, is_abstract: true, note: "layer root"} - {term: LogicalLayerObject, is_abstract: true, note: "layer root"} - {term: PhysicalLayerObject, is_abstract: true, note: "NEW — omission in v1.6"} - {term: DeliveryLayerObject, is_abstract: true, note: "layer root"} - {term: ConsumptionLayerObject, is_abstract: true, note: "layer root"} - {term: GovernanceLayerObject, is_abstract: true, note: "NEW — seventh layer root"} - {term: DataStructure, is_abstract: true, note: "harvesting types the concrete sort"} - {term: KeyConstraint, is_abstract: true, note: "only PrimaryKey and ForeignKey instantiate"} - {term: Actor, is_abstract: true, note: "renaming would degrade the range of ownedBy and hasPublisher"} - {term: DataDomain, is_abstract: false, note: null} - {term: DataSubDomain, is_abstract: false, note: null} - {term: BusinessObject, is_abstract: false, note: "ends in Object and is heavily instantiated - refutes any suffix rule"} - {term: BusinessConcept, is_abstract: false, note: null} - {term: Metric, is_abstract: false, note: "concrete despite having KeyPerformanceIndicator beneath it"} - {term: KeyPerformanceIndicator, is_abstract: false, note: null} - {term: DataObject, is_abstract: false, note: null} - {term: DataElement, is_abstract: false, note: null} - {term: Database, is_abstract: false, note: null} - {term: Schema, is_abstract: false, note: null} - {term: Field, is_abstract: false, note: null} - {term: System, is_abstract: false, note: null} - {term: Transformation, is_abstract: false, note: null} - {term: BaseTable, is_abstract: false, note: null} - {term: ExternalTable, is_abstract: false, note: null} - {term: View, is_abstract: false, note: null} - {term: PrimaryKey, is_abstract: false, note: null} - {term: ForeignKey, is_abstract: false, note: null} - {term: DataProduct, is_abstract: false, note: null} - {term: DataContract, is_abstract: false, note: null} - {term: DataInterface, is_abstract: false, note: null} - {term: BusinessIntelligenceWorkspace, is_abstract: false, note: null} - {term: BusinessIntelligenceDataSource, is_abstract: false, note: null} - {term: BusinessIntelligenceField, is_abstract: false, note: null} - {term: BusinessIntelligenceReport, is_abstract: false, note: null} - {term: DataDomainOwner, is_abstract: false, note: null} - {term: DataSubDomainOwner, is_abstract: false, note: null} - {term: DataProductOwner, is_abstract: false, note: null} - {term: DataSteward, is_abstract: false, note: null} - {term: DataGovernanceLead, is_abstract: false, note: null} - {term: SystemType, is_abstract: false, note: "must stay concrete — it types five individuals"} # ====== ANNEX: DISPLAY (TN-011, TN-022) ====== # Authoritative and exhaustive: a class absent from this list carries no # short label and no acronym. A null short_label means any existing one is # removed, since a duplicate of the label carries no information (TN-022). display: - {iri: MetaModelObject, label: "Meta Model Object", short_label: null, acronym: null, note: null} - {iri: DefinedObject, label: "Defined Object", short_label: null, acronym: null, note: null} - {iri: CapturedObject, label: "Captured Object", short_label: null, acronym: null, note: null} - {iri: OwnershipLayerObject, label: "Ownership Layer Object", short_label: "Ownership Layer", acronym: null, note: "the categorisation role moves into rdfs:comment"} - {iri: BusinessLayerObject, label: "Business Layer Object", short_label: "Business Layer", acronym: null, note: null} - {iri: LogicalLayerObject, label: "Logical Layer Object", short_label: "Logical Layer", acronym: null, note: null} - {iri: PhysicalLayerObject, label: "Physical Layer Object", short_label: "Physical Layer", acronym: null, note: null} - {iri: DeliveryLayerObject, label: "Delivery Layer Object", short_label: "Delivery Layer", acronym: null, note: null} - {iri: ConsumptionLayerObject, label: "Consumption Layer Object", short_label: "Consumption Layer", acronym: null, note: null} - {iri: GovernanceLayerObject, label: "Governance Layer Object", short_label: "Governance Layer", acronym: null, note: null} - {iri: DataStructure, label: "Data Structure", short_label: null, acronym: null, note: "the former short label was a gloss and belongs in rdfs:comment"} - {iri: KeyConstraint, label: "Key Constraint", short_label: null, acronym: null, note: null} - {iri: Actor, label: "Actor", short_label: null, acronym: null, note: null} - {iri: DataDomain, label: "Data Domain", short_label: "Domain", acronym: "DD", note: "DD is already the short form used inside instance identifiers"} - {iri: DataSubDomain, label: "Data Sub Domain", short_label: "Sub Domain", acronym: "SD", note: "no hyphen, for consistency"} - {iri: BusinessObject, label: "Business Object", short_label: null, acronym: "BO", note: null} - {iri: BusinessConcept, label: "Business Concept", short_label: "Concept", acronym: null, note: "drives the name of usesConcept"} - {iri: Metric, label: "Metric", short_label: null, acronym: null, note: null} - {iri: KeyPerformanceIndicator, label: "Key Performance Indicator", short_label: "KPI", acronym: "KPI", note: "the acronym channel is what makes TN-002 bearable"} - {iri: DataObject, label: "Data Object", short_label: null, acronym: null, note: null} - {iri: DataElement, label: "Data Element", short_label: "Element", acronym: null, note: "drives the name of hasElement and hasGrainElement"} - {iri: Database, label: "Database", short_label: null, acronym: null, note: null} - {iri: Schema, label: "Schema", short_label: null, acronym: null, note: null} - {iri: Field, label: "Field", short_label: null, acronym: null, note: null} - {iri: System, label: "System", short_label: null, acronym: null, note: null} - {iri: SystemType, label: "System Type", short_label: null, acronym: null, note: null} - {iri: Transformation, label: "Transformation", short_label: null, acronym: null, note: null} - {iri: BaseTable, label: "Base Table", short_label: null, acronym: null, note: null} - {iri: ExternalTable, label: "External Table", short_label: null, acronym: null, note: null} - {iri: View, label: "View", short_label: null, acronym: null, note: null} - {iri: PrimaryKey, label: "Primary Key", short_label: null, acronym: null, note: null} - {iri: ForeignKey, label: "Foreign Key", short_label: null, acronym: null, note: null} - {iri: DataProduct, label: "Data Product", short_label: null, acronym: null, note: null} - {iri: DataContract, label: "Data Contract", short_label: null, acronym: null, note: null} - {iri: DataInterface, label: "Data Interface", short_label: null, acronym: null, note: null} - {iri: BusinessIntelligenceWorkspace, label: "Business Intelligence Workspace", short_label: "BI Workspace", acronym: null, note: null} - {iri: BusinessIntelligenceDataSource, label: "Business Intelligence Data Source", short_label: "BI Data Source", acronym: null, note: null} - {iri: BusinessIntelligenceField, label: "Business Intelligence Field", short_label: "BI Field", acronym: null, note: "reproduced whole in containsBIField under TN-011"} - {iri: BusinessIntelligenceReport, label: "Business Intelligence Report", short_label: "BI Report", acronym: null, note: null} - {iri: DataDomainOwner, label: "Data Domain Owner", short_label: "Domain Owner", acronym: "DDO", note: null} - {iri: DataSubDomainOwner, label: "Data Sub Domain Owner", short_label: "Sub Domain Owner", acronym: "SDO", note: null} - {iri: DataProductOwner, label: "Data Product Owner", short_label: "Product Owner", acronym: "PO", note: "not DPO - the initialism is taken by Data Protection Officer, a real GDPR role"} - {iri: DataSteward, label: "Data Steward", short_label: "Steward", acronym: null, note: null} - {iri: DataGovernanceLead, label: "Data Governance Lead", short_label: "Governance Lead", acronym: "DGL", note: null}