Compare commits
6 Commits
fbc63e55d8
...
v2.2
| Author | SHA1 | Date | |
|---|---|---|---|
| 5633845880 | |||
| 0b7d3834d2 | |||
| d90d2a316d | |||
| 908826601a | |||
| 1719ec1934 | |||
| ea21b7b300 |
@@ -1,6 +1,6 @@
|
|||||||
# Pernod Ricard Data MetaModel — T-Box Rulebook
|
# Pernod Ricard Data MetaModel — T-Box Rulebook
|
||||||
|
|
||||||
**Version 1.1** — Draft for review — generated 2026-08-03
|
**Version 1.4** — 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).
|
> Generated from `rules.yaml`. Do not edit this document: edit the source and regenerate (EV-007, EV-008).
|
||||||
|
|
||||||
@@ -188,7 +188,7 @@ The IRI is identity. It is immutable in the RDF sense — changing one is a migr
|
|||||||
|
|
||||||
## TN-009 — A relation begins with a verb
|
## 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.
|
**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. An adverb qualifying that verb is transparent and may precede it. No relation begins with a preposition or with a noun.
|
||||||
|
|
||||||
| | |
|
| | |
|
||||||
|---|---|
|
|---|---|
|
||||||
@@ -198,12 +198,12 @@ The IRI is identity. It is immutable in the RDF sense — changing one is a migr
|
|||||||
| Executor | `check_tbox_naming.py` |
|
| Executor | `check_tbox_naming.py` |
|
||||||
| Procedure | _pending_ |
|
| 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.
|
**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. Two clauses are stated rather than left implicit, because a literal reading would condemn sound relations: the participial one, which covers a whole family such as computedBy, storedIn and derivedFrom, and the adverbial one, which covers a relation whose verb is qualified rather than replaced.
|
||||||
|
|
||||||
| From | To | Note |
|
| From | To | Note |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| pr:inDatabase | pr:isInDatabase | a preposition is not a verb |
|
| pr:inDatabase | pr:isInDatabase | a preposition is not a verb |
|
||||||
| pr:primaryLocation | pr:primarilyStoredIn | a noun is not a verb |
|
| pr:primaryLocation | pr:primarilyStoredIn | a noun is not a verb; the adverb is transparent and the verb follows |
|
||||||
| pr:computedBy | pr:computedBy | participial form is conforming |
|
| pr:computedBy | pr:computedBy | participial form is conforming |
|
||||||
|
|
||||||
## TN-010 — The is prefix is either a copula or a predicate
|
## TN-010 — The is prefix is either a copula or a predicate
|
||||||
@@ -444,7 +444,7 @@ The label is derived, not authored. Anything a reader needs that the derivation
|
|||||||
|
|
||||||
## TN-022 — The short label carries the spoken form
|
## TN-022 — The short label carries the spoken form
|
||||||
|
|
||||||
**Statement.** A term carries pr:shortLabel whenever the form derived under TN-018 is not what the organisation writes or says. The short label is free: acronyms, abbreviations, hyphens, short forms.
|
**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.
|
||||||
|
|
||||||
| | |
|
| | |
|
||||||
|---|---|
|
|---|---|
|
||||||
@@ -454,12 +454,13 @@ The label is derived, not authored. Anything a reader needs that the derivation
|
|||||||
| Executor | `review checklist` |
|
| Executor | `review checklist` |
|
||||||
| Procedure | _pending_ |
|
| 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. 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.
|
**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 |
|
| From | To | Note |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| "Data Sub Domain Owner" | short label Sub Domain Owner | the word Data is dropped in the spoken form |
|
| "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 |
|
| "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
|
## TN-023 — The label is generated, never typed
|
||||||
|
|
||||||
@@ -506,7 +507,7 @@ Nothing is left to be guessed. What a term is, what it applies to, where it sits
|
|||||||
|
|
||||||
## TN-025 — Every property declares its domain and range
|
## 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.
|
**Statement.** rdfs:domain and rdfs:range are mandatory on every property, EXCEPT where the property is deliberately polymorphic. A polymorphic property declares pr:polymorphic true and has its scope declared in SHACL. Silent absence is a violation; declared absence is not.
|
||||||
|
|
||||||
| | |
|
| | |
|
||||||
|---|---|
|
|---|---|
|
||||||
@@ -516,30 +517,31 @@ Nothing is left to be guessed. What a term is, what it applies to, where it sits
|
|||||||
| Executor | `check_tbox_naming.py` |
|
| Executor | `check_tbox_naming.py` |
|
||||||
| Procedure | _pending_ |
|
| 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.
|
**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. The distinction is carried by an axiom and not by a sentence in the comment: a checker looking for a phrase in prose fails the moment someone words it differently, and what matters is declared rather than read, exactly as for abstractness.
|
||||||
|
|
||||||
| From | To | Note |
|
| From | To | Note |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| a polymorphic property with no domain and no comment | violation | silence is indistinguishable from omission |
|
| a property with no domain and no declaration | violation | silence is indistinguishable from omission |
|
||||||
| a polymorphic property with no domain | conforming | scope declared in SHACL |
|
| a property with no domain, declaring pr:polymorphic true | conforming | scope declared in SHACL |
|
||||||
|
|
||||||
## TN-026 — Every term declares how it was authored
|
## TN-026 — Every concrete class declares how its instances are produced
|
||||||
|
|
||||||
**Statement.** pr:authoringMode is mandatory on every term. pr:harvestSource is mandatory if and only if the mode is HARVESTED.
|
**Statement.** Every CONCRETE CLASS declares pr:authoringMode, stating whether its instances are ASSERTED by governance or HARVESTED from a system. pr:harvestSource is mandatory if and only if the mode is HARVESTED. The declared mode agrees with the provenance axis the class descends from: a class under CapturedObject declares HARVESTED, one under DefinedObject declares ASSERTED. Nothing else carries the property: not an abstract class, not a property, not a term of the vocabulary in its own right.
|
||||||
|
|
||||||
| | |
|
| | |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Severity | **BLOCKING** |
|
| Severity | **BLOCKING** |
|
||||||
| Applies to | class, object_property, datatype_property, annotation_property |
|
| Applies to | class |
|
||||||
| Enforced at | script |
|
| Enforced at | script |
|
||||||
| Executor | `check_tbox_naming.py` |
|
| Executor | `check_tbox_naming.py` |
|
||||||
| Procedure | _pending_ |
|
| 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.
|
**Why.** A term of the vocabulary is declared, always and by definition; recording that on every term would repeat one fact a hundred times and say nothing. It is the INSTANCES that have a provenance, and the class is where the model states which one: a governance object is asserted by a domain, a physical column is harvested from a system. The distinction matters because a class can be declared and describe captured things at once — CapturedObject itself is a declared term naming harvested instances, and conflating the two is what once put a harvesting mode on an abstraction. Nothing is inherited. A subclass declares its own mode rather than taking its parent's, so that the declaration is read where the instances are typed and not chased up a chain. The agreement clause is what keeps that free of contradiction: the axis says what kind of thing the class describes, the mode says how those things arrive, and a class under CapturedObject declaring ASSERTED is stating both at once. Declared this way, provenance also makes a harvester specifiable from the model itself rather than from a side document.
|
||||||
|
|
||||||
| From | To | Note |
|
| From | To | Note |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| harvestSource present | authoringMode HARVESTED | |
|
| authoringMode on an abstract class | nothing | an abstraction has no instances to produce |
|
||||||
|
| a class under CapturedObject declaring ASSERTED | violation | the mode contradicts the axis |
|
||||||
| authoringMode HARVESTED, harvestSource absent | harvestSource declared | |
|
| authoringMode HARVESTED, harvestSource absent | harvestSource declared | |
|
||||||
|
|
||||||
## TN-027 — A concrete class has one layer and one provenance
|
## TN-027 — A concrete class has one layer and one provenance
|
||||||
@@ -923,19 +925,47 @@ Applies TN-011 and TN-022. The short label is what screens display and what rela
|
|||||||
|
|
||||||
| IRI | rdfs:label | shortLabel | acronym | Note |
|
| IRI | rdfs:label | shortLabel | acronym | Note |
|
||||||
|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| `DataDomain` | Data Domain | Domain | DD | DD is already the short form used inside A-Box identifiers under NR-014 |
|
| `MetaModelObject` | Meta Model Object | - | - | |
|
||||||
| `DataSubDomain` | Data Sub Domain | Sub Domain | SD | no hyphen |
|
| `DefinedObject` | Defined Object | - | - | |
|
||||||
| `DataDomainOwner` | Data Domain Owner | Domain Owner | DDO | |
|
| `CapturedObject` | Captured Object | - | - | |
|
||||||
| `DataSubDomainOwner` | Data Sub Domain Owner | Sub Domain Owner | SDO | |
|
| `OwnershipLayerObject` | Ownership Layer Object | Ownership Layer | - | the categorisation role moves into rdfs:comment |
|
||||||
| `DataProductOwner` | Data Product Owner | Product Owner | PO | NOT DPO — the initialism is taken by Data Protection Officer |
|
| `BusinessLayerObject` | Business Layer Object | Business Layer | - | |
|
||||||
| `DataSteward` | Data Steward | Steward | - | |
|
| `LogicalLayerObject` | Logical Layer Object | Logical Layer | - | |
|
||||||
| `DataGovernanceLead` | Data Governance Lead | Governance Lead | DGL | |
|
| `PhysicalLayerObject` | Physical Layer Object | Physical Layer | - | |
|
||||||
| `DataElement` | Data Element | Element | - | drives the name of hasElement and hasGrainElement |
|
| `DeliveryLayerObject` | Delivery Layer Object | Delivery Layer | - | |
|
||||||
| `BusinessConcept` | Business Concept | Concept | - | drives the name of usesConcept |
|
| `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 | |
|
| `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 |
|
| `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 | - | |
|
| `BusinessIntelligenceWorkspace` | Business Intelligence Workspace | BI Workspace | - | |
|
||||||
| `BusinessIntelligenceDataSource` | Business Intelligence Data Source | BI Data Source | - | |
|
| `BusinessIntelligenceDataSource` | Business Intelligence Data Source | BI Data Source | - | |
|
||||||
| `BusinessIntelligenceField` | Business Intelligence Field | BI Field | - | |
|
| `BusinessIntelligenceField` | Business Intelligence Field | BI Field | - | reproduced whole in containsBIField under TN-011 |
|
||||||
| `BusinessIntelligenceReport` | Business Intelligence Report | BI Report | - | |
|
| `BusinessIntelligenceReport` | Business Intelligence Report | BI Report | - | |
|
||||||
| `OwnershipLayerObject` | Ownership Layer Object | - | - | the categorisation role moves into rdfs:comment |
|
| `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 | |
|
||||||
|
|||||||
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,488 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Enforce the naming and declaration rules of the T-Box rulebook.
|
||||||
|
|
||||||
|
USAGE
|
||||||
|
python3 check_tbox_naming.py [--ontology PATH] [--json PATH] [--quiet]
|
||||||
|
|
||||||
|
Named as the executor of 25 rules. EV-011 forbids declaring a rule blocking
|
||||||
|
without one, so this script is what makes those declarations true.
|
||||||
|
|
||||||
|
THREE VERDICTS
|
||||||
|
VIOLATION the rule is settled mechanically and is broken -> exit 1
|
||||||
|
REVIEW the checker cannot conclude; a person must -> exit 0
|
||||||
|
PASS conforming
|
||||||
|
|
||||||
|
The middle verdict is the point. Without it the choice would be between
|
||||||
|
blocking on false positives and staying silent on real problems. What the
|
||||||
|
checker cannot decide alone comes from naming_lexicon.yaml; a word absent from
|
||||||
|
the lexicon produces a REVIEW, never a VIOLATION.
|
||||||
|
"""
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
from rdflib import Graph, Literal, Namespace, RDF, RDFS, OWL, URIRef, XSD
|
||||||
|
|
||||||
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
RULES = os.path.join(HERE, "rules.yaml")
|
||||||
|
LEXICON = os.path.join(HERE, "naming_lexicon.yaml")
|
||||||
|
DEFAULT_ONTOLOGY = os.path.normpath(os.path.join(HERE, "..", "ontology", "pr_metamodel.ttl"))
|
||||||
|
|
||||||
|
NS = "https://ontology.pernod-ricard.com/metamodel/"
|
||||||
|
PR = Namespace(NS)
|
||||||
|
|
||||||
|
VIOLATION, REVIEW = "VIOLATION", "REVIEW"
|
||||||
|
|
||||||
|
KINDS = OrderedDict([
|
||||||
|
(OWL.Class, "class"),
|
||||||
|
(OWL.ObjectProperty, "relation"),
|
||||||
|
(OWL.DatatypeProperty, "attribute"),
|
||||||
|
(OWL.AnnotationProperty, "annotation"),
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------- findings
|
||||||
|
|
||||||
|
class Findings(object):
|
||||||
|
def __init__(self):
|
||||||
|
self.items = []
|
||||||
|
|
||||||
|
def add(self, verdict, rule, term, message):
|
||||||
|
self.items.append({"verdict": verdict, "rule": rule,
|
||||||
|
"term": term, "message": message})
|
||||||
|
|
||||||
|
def violation(self, rule, term, message):
|
||||||
|
self.add(VIOLATION, rule, term, message)
|
||||||
|
|
||||||
|
def review(self, rule, term, message):
|
||||||
|
self.add(REVIEW, rule, term, message)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def violations(self):
|
||||||
|
return [i for i in self.items if i["verdict"] == VIOLATION]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def reviews(self):
|
||||||
|
return [i for i in self.items if i["verdict"] == REVIEW]
|
||||||
|
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------- helpers
|
||||||
|
|
||||||
|
def local(uri):
|
||||||
|
s = str(uri)
|
||||||
|
return s[len(NS):] if s.startswith(NS) else None
|
||||||
|
|
||||||
|
|
||||||
|
def tokens(name):
|
||||||
|
"""Split a CamelCase local name. A run of capitals is one token: an acronym."""
|
||||||
|
return re.findall(r"[A-Z]+(?![a-z])|[A-Z][a-z0-9]*|^[a-z0-9]+|[a-z0-9]+", name)
|
||||||
|
|
||||||
|
|
||||||
|
def decamelise(name, is_class):
|
||||||
|
"""TN-018. A run of capitals is kept together: it carries an acronym."""
|
||||||
|
spaced = re.sub(r"(?<=[a-z0-9])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])", " ", name)
|
||||||
|
return spaced if is_class else spaced[0].lower() + spaced[1:]
|
||||||
|
|
||||||
|
|
||||||
|
def acronyms_in(name, lex):
|
||||||
|
"""Runs of two or more capitals, plus any known short form appearing whole.
|
||||||
|
|
||||||
|
The negative lookahead matters: without it, BIField yields BIF rather than
|
||||||
|
BI, because the run swallows the initial capital of the word that follows.
|
||||||
|
"""
|
||||||
|
found = set(re.findall(r"[A-Z]{2,}(?![a-z])", name))
|
||||||
|
for short in lex["known_acronyms"]:
|
||||||
|
if re.search(r"(?:^|(?<=[a-z]))%s(?![a-z])" % re.escape(short), name):
|
||||||
|
found.add(short)
|
||||||
|
return sorted(found)
|
||||||
|
|
||||||
|
|
||||||
|
def strip_case(text):
|
||||||
|
return re.sub(r"[^a-z0-9]", "", (text or "").lower())
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------- checks
|
||||||
|
|
||||||
|
def provenance_axis(g, term):
|
||||||
|
"""The provenance axis a class descends from, or None."""
|
||||||
|
seen, stack = set(), [term]
|
||||||
|
while stack:
|
||||||
|
node = stack.pop()
|
||||||
|
for parent in g.objects(node, RDFS.subClassOf):
|
||||||
|
if isinstance(parent, URIRef) and parent not in seen:
|
||||||
|
seen.add(parent)
|
||||||
|
stack.append(parent)
|
||||||
|
if PR.CapturedObject in seen:
|
||||||
|
return "CapturedObject"
|
||||||
|
if PR.DefinedObject in seen:
|
||||||
|
return "DefinedObject"
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def check_identifier(g, terms, lex, f):
|
||||||
|
"""TN-001 to TN-015 — the form of the IRI."""
|
||||||
|
short_labels = {t: str(v) for t in terms
|
||||||
|
for v in g.objects(t, PR.shortLabel)}
|
||||||
|
allowed_acronyms = set()
|
||||||
|
for value in short_labels.values():
|
||||||
|
allowed_acronyms |= set(re.findall(r"[A-Z]{2,}", value))
|
||||||
|
|
||||||
|
for term, kind in terms.items():
|
||||||
|
name = local(term)
|
||||||
|
if name is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# TN-001 — CamelCase, no separator, correct initial
|
||||||
|
if re.search(r"[^A-Za-z0-9]", name):
|
||||||
|
f.violation("TN-001", name, "the local name carries a separator")
|
||||||
|
expect_upper = kind in ("class", "individual")
|
||||||
|
if name and name[0].isupper() != expect_upper:
|
||||||
|
f.violation("TN-001", name,
|
||||||
|
"a %s starts with %s case" %
|
||||||
|
(kind, "upper" if expect_upper else "lower"))
|
||||||
|
|
||||||
|
# TN-004 — no digit
|
||||||
|
if any(c.isdigit() for c in name):
|
||||||
|
f.violation("TN-004", name, "a digit belongs to a value, not an identity")
|
||||||
|
|
||||||
|
# TN-002 — no acronym, except one reproduced from a target short label
|
||||||
|
for a in acronyms_in(name, lex):
|
||||||
|
if kind == "relation" and a in allowed_acronyms:
|
||||||
|
continue # TN-011 exception
|
||||||
|
expansion = lex["known_acronyms"].get(a)
|
||||||
|
f.violation("TN-002", name, "carries the acronym %s%s"
|
||||||
|
% (a, " (%s)" % expansion if expansion else ""))
|
||||||
|
|
||||||
|
if kind == "class":
|
||||||
|
check_class_name(name, lex, f)
|
||||||
|
elif kind == "relation":
|
||||||
|
check_relation_name(g, term, name, lex, f)
|
||||||
|
elif kind == "attribute":
|
||||||
|
check_attribute_name(g, term, name, lex, f)
|
||||||
|
|
||||||
|
|
||||||
|
def check_class_name(name, lex, f):
|
||||||
|
"""TN-005 — a singular common noun, no type word."""
|
||||||
|
if name in lex["type_word_exceptions"]:
|
||||||
|
return
|
||||||
|
for w in lex["type_words"]:
|
||||||
|
if re.search(r"(?:^|(?<=[a-z]))%s(?![a-z])" % w, name):
|
||||||
|
f.violation("TN-005", name,
|
||||||
|
"carries the type word %s and is not a recorded abstraction" % w)
|
||||||
|
last = tokens(name)[-1] if tokens(name) else name
|
||||||
|
if last.endswith("s") and last not in lex["not_plural"]:
|
||||||
|
f.review("TN-005", name, "%s looks plural; a class name is singular" % last)
|
||||||
|
|
||||||
|
|
||||||
|
def check_relation_name(g, term, name, lex, f):
|
||||||
|
"""TN-009, TN-010, TN-011 — verb first, is disambiguated, target named whole."""
|
||||||
|
toks = tokens(name)
|
||||||
|
head = toks[0] if toks else name
|
||||||
|
|
||||||
|
# TN-009 — an adverb is transparent, the verb follows
|
||||||
|
if head in lex["adverbs"]:
|
||||||
|
toks = toks[1:]
|
||||||
|
head = toks[0].lower() if toks else head
|
||||||
|
head = head.lower()
|
||||||
|
|
||||||
|
if head in lex["copula"]:
|
||||||
|
pass # TN-010, copula use
|
||||||
|
elif head in lex["active_verbs"] or head in lex["participles"]:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
f.review("TN-009", name,
|
||||||
|
"%s is not a verb known to the lexicon; add it there or rename" % head)
|
||||||
|
|
||||||
|
# TN-011 — if the target class is named, it is named by its short label whole
|
||||||
|
for rng in g.objects(term, RDFS.range):
|
||||||
|
target = local(rng)
|
||||||
|
if not target:
|
||||||
|
continue
|
||||||
|
short = next((str(v) for v in g.objects(rng, PR.shortLabel)), None)
|
||||||
|
reference = strip_case(short) if short else strip_case(target)
|
||||||
|
tail = strip_case("".join(toks[1:]))
|
||||||
|
if not tail or tail == reference:
|
||||||
|
continue
|
||||||
|
if reference.endswith(tail) or tail in reference:
|
||||||
|
f.violation("TN-011", name,
|
||||||
|
"names its target by a truncation; %s expects %s"
|
||||||
|
% (target, short or target))
|
||||||
|
|
||||||
|
|
||||||
|
def check_attribute_name(g, term, name, lex, f):
|
||||||
|
"""TN-012 to TN-015 — a noun, no relational suffix, booleans predicative."""
|
||||||
|
ranges = [str(r) for r in g.objects(term, RDFS.range)]
|
||||||
|
is_boolean = str(XSD.boolean) in ranges
|
||||||
|
is_date = any(r in lex["date_ranges"] for r in ranges)
|
||||||
|
toks = tokens(name)
|
||||||
|
head = (toks[0] if toks else name).lower()
|
||||||
|
|
||||||
|
if is_boolean:
|
||||||
|
if head not in lex["copula"]:
|
||||||
|
f.violation("TN-015", name, "a boolean attribute is predicative and begins with is")
|
||||||
|
return
|
||||||
|
|
||||||
|
# TN-012 — a noun, and in particular no has
|
||||||
|
if head == "has":
|
||||||
|
f.violation("TN-012", name, "an attribute is a noun; the domain already says who has it")
|
||||||
|
elif head in lex["active_verbs"] or head in lex["copula"]:
|
||||||
|
f.violation("TN-012", name, "%s is a verb; an attribute is a noun" % head)
|
||||||
|
elif head in lex["participles"]:
|
||||||
|
f.review("TN-012", name, "%s is a participle; confirm this reads as a noun" % head)
|
||||||
|
|
||||||
|
# TN-014 — relational suffixes are reserved for relations
|
||||||
|
last = toks[-1] if toks else name
|
||||||
|
if last in lex["relational_suffixes"]:
|
||||||
|
f.violation("TN-014", name,
|
||||||
|
"ends in %s, a form reserved for object properties" % last)
|
||||||
|
|
||||||
|
# TN-013 — a date attribute is nominal
|
||||||
|
if is_date and (last in lex["relational_suffixes"] or head in lex["participles"]):
|
||||||
|
f.violation("TN-013", name, "a date attribute is a noun, not a participle")
|
||||||
|
|
||||||
|
|
||||||
|
def check_declaration(g, terms, lex, f, abstractness):
|
||||||
|
"""TN-003, TN-006, TN-007, TN-024, TN-025, TN-026, TN-027, TN-028."""
|
||||||
|
layer_roots = {t for t in terms
|
||||||
|
if terms[t] == "class"
|
||||||
|
and (local(t) or "").endswith("LayerObject")}
|
||||||
|
|
||||||
|
for term, kind in terms.items():
|
||||||
|
name = local(term)
|
||||||
|
if name is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# TN-024 — every active term carries a comment
|
||||||
|
if not list(g.objects(term, RDFS.comment)):
|
||||||
|
f.violation("TN-024", name, "carries no rdfs:comment")
|
||||||
|
|
||||||
|
# TN-026 — only a concrete class declares how its instances are produced
|
||||||
|
modes = [str(m) for m in g.objects(term, PR.authoringMode)]
|
||||||
|
concrete = (kind == "class"
|
||||||
|
and Literal(False) in set(g.objects(term, PR.isAbstract)))
|
||||||
|
if modes and not concrete:
|
||||||
|
f.violation("TN-026", name,
|
||||||
|
"declares pr:authoringMode; only a concrete class does, "
|
||||||
|
"since a vocabulary term is declared by definition")
|
||||||
|
elif concrete:
|
||||||
|
if not modes:
|
||||||
|
f.violation("TN-026", name, "does not declare pr:authoringMode")
|
||||||
|
else:
|
||||||
|
if "HARVESTED" in modes and not list(g.objects(term, PR.harvestSource)):
|
||||||
|
f.violation("TN-026", name, "is HARVESTED and names no harvestSource")
|
||||||
|
axis = provenance_axis(g, term)
|
||||||
|
if axis == "CapturedObject" and "HARVESTED" not in modes:
|
||||||
|
f.violation("TN-026", name,
|
||||||
|
"descends from CapturedObject and must declare HARVESTED")
|
||||||
|
if axis == "DefinedObject" and "ASSERTED" not in modes:
|
||||||
|
f.violation("TN-026", name,
|
||||||
|
"descends from DefinedObject and must declare ASSERTED")
|
||||||
|
|
||||||
|
if kind == "class":
|
||||||
|
values = list(g.objects(term, PR.isAbstract))
|
||||||
|
# TN-007 — declared exactly once, explicitly
|
||||||
|
if len(values) != 1:
|
||||||
|
f.violation("TN-007", name,
|
||||||
|
"declares pr:isAbstract %d times; exactly one is required"
|
||||||
|
% len(values))
|
||||||
|
# TN-006 — a layer root is abstract
|
||||||
|
elif term in layer_roots and values[0].toPython() is not True:
|
||||||
|
f.violation("TN-006", name, "is a layer root and must be abstract")
|
||||||
|
# TN-027 — a concrete class has one layer and one provenance
|
||||||
|
elif values[0].toPython() is False:
|
||||||
|
check_attachment(g, term, name, layer_roots, f)
|
||||||
|
|
||||||
|
if kind in ("relation", "attribute"):
|
||||||
|
check_property_typing(g, term, name, lex, f)
|
||||||
|
|
||||||
|
# TN-003 — the display properties are annotations
|
||||||
|
for display in (PR.shortLabel, PR.acronym):
|
||||||
|
name = local(display)
|
||||||
|
types = set(g.objects(display, RDF.type))
|
||||||
|
if not types:
|
||||||
|
continue
|
||||||
|
if OWL.AnnotationProperty not in types:
|
||||||
|
f.violation("TN-003", name, "is declared %s and must be an annotation property"
|
||||||
|
% ", ".join(sorted(local(t) or str(t) for t in types)))
|
||||||
|
|
||||||
|
|
||||||
|
def check_attachment(g, term, name, layer_roots, f):
|
||||||
|
"""TN-027 — exactly one layer root and one provenance axis above."""
|
||||||
|
seen, stack = set(), [term]
|
||||||
|
while stack:
|
||||||
|
node = stack.pop()
|
||||||
|
for parent in g.objects(node, RDFS.subClassOf):
|
||||||
|
if isinstance(parent, URIRef) and parent not in seen:
|
||||||
|
seen.add(parent)
|
||||||
|
stack.append(parent)
|
||||||
|
layers = seen & layer_roots
|
||||||
|
axes = seen & {PR.DefinedObject, PR.CapturedObject}
|
||||||
|
if len(layers) != 1:
|
||||||
|
f.violation("TN-027", name, "descends from %d layer roots; exactly one is required"
|
||||||
|
% len(layers))
|
||||||
|
if len(axes) != 1:
|
||||||
|
f.violation("TN-027", name, "descends from %d provenance axes; exactly one is required"
|
||||||
|
% len(axes))
|
||||||
|
|
||||||
|
|
||||||
|
def check_property_typing(g, term, name, lex, f):
|
||||||
|
"""TN-025 and TN-028 — domain, range, and inherited domain."""
|
||||||
|
domains = list(g.objects(term, RDFS.domain))
|
||||||
|
ranges = list(g.objects(term, RDFS.range))
|
||||||
|
|
||||||
|
if not ranges and Literal(True) not in set(g.objects(term, PR.polymorphic)):
|
||||||
|
f.violation("TN-025", name, "declares no rdfs:range")
|
||||||
|
polymorphic = Literal(True) in set(g.objects(term, PR.polymorphic))
|
||||||
|
if not domains:
|
||||||
|
if polymorphic:
|
||||||
|
f.review("TN-025", name,
|
||||||
|
"declares itself polymorphic; confirm the scope is declared in SHACL")
|
||||||
|
else:
|
||||||
|
f.violation("TN-025", name,
|
||||||
|
"omits its domain in silence; declare pr:polymorphic true "
|
||||||
|
"or state the domain")
|
||||||
|
|
||||||
|
# TN-028 — a sub-property inherits the domain of its parent
|
||||||
|
for parent in g.objects(term, RDFS.subPropertyOf):
|
||||||
|
if not isinstance(parent, URIRef) or local(parent) is None:
|
||||||
|
continue
|
||||||
|
parent_domains = set(g.objects(parent, RDFS.domain))
|
||||||
|
if not parent_domains or not domains:
|
||||||
|
continue
|
||||||
|
if set(domains) & parent_domains:
|
||||||
|
continue
|
||||||
|
compatible = False
|
||||||
|
for d in domains:
|
||||||
|
ancestors, stack = set(), [d]
|
||||||
|
while stack:
|
||||||
|
node = stack.pop()
|
||||||
|
for up in g.objects(node, RDFS.subClassOf):
|
||||||
|
if isinstance(up, URIRef) and up not in ancestors:
|
||||||
|
ancestors.add(up)
|
||||||
|
stack.append(up)
|
||||||
|
if ancestors & parent_domains:
|
||||||
|
compatible = True
|
||||||
|
if not compatible:
|
||||||
|
f.review("TN-028", name,
|
||||||
|
"is a sub-property of %s, whose domain does not cover its own; "
|
||||||
|
"asserting it will retype the subject"
|
||||||
|
% local(parent))
|
||||||
|
|
||||||
|
|
||||||
|
def check_label(g, terms, f):
|
||||||
|
"""TN-018 to TN-023 — the label is derived, and carries nothing else."""
|
||||||
|
for term, kind in terms.items():
|
||||||
|
name = local(term)
|
||||||
|
if name is None:
|
||||||
|
continue
|
||||||
|
labels = list(g.objects(term, RDFS.label))
|
||||||
|
if not labels:
|
||||||
|
f.violation("TN-023", name, "carries no rdfs:label")
|
||||||
|
continue
|
||||||
|
if len(labels) > 1:
|
||||||
|
f.violation("TN-023", name, "carries %d labels; the derivation yields one"
|
||||||
|
% len(labels))
|
||||||
|
text = str(labels[0])
|
||||||
|
expected = decamelise(name, kind in ("class", "individual"))
|
||||||
|
if text != expected:
|
||||||
|
f.violation("TN-018", name, "label is %r; the derivation yields %r"
|
||||||
|
% (text, expected))
|
||||||
|
if "(" in text or ")" in text:
|
||||||
|
f.violation("TN-021", name, "the label carries a gloss; it belongs in the comment")
|
||||||
|
if re.search(r"\b(deprecated|obsolete|draft|published|v\d)\b", text, re.I):
|
||||||
|
f.violation("TN-020", name, "the label carries state; the axiom carries it")
|
||||||
|
|
||||||
|
|
||||||
|
def check_namespace(g, f):
|
||||||
|
"""EV-014 — the term namespace is never versioned."""
|
||||||
|
for prefix, uri in g.namespaces():
|
||||||
|
if str(uri).startswith("https://ontology.pernod-ricard.com/metamodel/") \
|
||||||
|
and str(uri) != NS:
|
||||||
|
f.violation("EV-014", str(uri),
|
||||||
|
"the prefix %s binds a versioned namespace; the version lives "
|
||||||
|
"in owl:versionIRI alone" % prefix)
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------ main
|
||||||
|
|
||||||
|
def collect(g):
|
||||||
|
terms = OrderedDict()
|
||||||
|
for rdf_type, kind in KINDS.items():
|
||||||
|
for term in sorted(g.subjects(RDF.type, rdf_type), key=str):
|
||||||
|
if local(term) is None:
|
||||||
|
continue
|
||||||
|
if (term, OWL.deprecated, Literal(True)) in g:
|
||||||
|
continue
|
||||||
|
terms[term] = kind
|
||||||
|
return terms
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ap = argparse.ArgumentParser()
|
||||||
|
ap.add_argument("--ontology", default=DEFAULT_ONTOLOGY)
|
||||||
|
ap.add_argument("--json", default=None, help="write the findings as JSON")
|
||||||
|
ap.add_argument("--quiet", action="store_true", help="print the summary only")
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
lex = yaml.safe_load(open(LEXICON, encoding="utf-8"))
|
||||||
|
rules = yaml.safe_load(open(RULES, encoding="utf-8"))
|
||||||
|
abstractness = {a["term"]: a["is_abstract"] for a in rules["abstractness"]}
|
||||||
|
|
||||||
|
g = Graph()
|
||||||
|
g.parse(args.ontology, format="turtle")
|
||||||
|
terms = collect(g)
|
||||||
|
|
||||||
|
f = Findings()
|
||||||
|
check_identifier(g, terms, lex, f)
|
||||||
|
check_declaration(g, terms, lex, f, abstractness)
|
||||||
|
check_label(g, terms, f)
|
||||||
|
check_namespace(g, f)
|
||||||
|
|
||||||
|
titles = {r["id"]: r["title"] for r in rules["rules"]}
|
||||||
|
print("=" * 78)
|
||||||
|
print("T-BOX NAMING CHECK")
|
||||||
|
print(" ontology : %s" % args.ontology)
|
||||||
|
print(" terms : %d active" % len(terms))
|
||||||
|
print("=" * 78)
|
||||||
|
|
||||||
|
if not args.quiet:
|
||||||
|
for verdict in (VIOLATION, REVIEW):
|
||||||
|
items = [i for i in f.items if i["verdict"] == verdict]
|
||||||
|
if not items:
|
||||||
|
continue
|
||||||
|
print("\n%s — %d" % (verdict, len(items)))
|
||||||
|
by_rule = OrderedDict()
|
||||||
|
for i in items:
|
||||||
|
by_rule.setdefault(i["rule"], []).append(i)
|
||||||
|
for rule, group in sorted(by_rule.items()):
|
||||||
|
print("\n [%s] %s (%d)" % (rule, titles.get(rule, ""), len(group)))
|
||||||
|
for i in group[:12]:
|
||||||
|
print(" %-34s %s" % (i["term"], i["message"]))
|
||||||
|
if len(group) > 12:
|
||||||
|
print(" ... and %d more" % (len(group) - 12))
|
||||||
|
|
||||||
|
print("\n" + "=" * 78)
|
||||||
|
print("RESULT : %d violation(s) | %d for review | %d term(s) checked"
|
||||||
|
% (len(f.violations), len(f.reviews), len(terms)))
|
||||||
|
print("=" * 78)
|
||||||
|
|
||||||
|
if args.json:
|
||||||
|
directory = os.path.dirname(os.path.abspath(args.json))
|
||||||
|
if directory:
|
||||||
|
os.makedirs(directory, exist_ok=True)
|
||||||
|
json.dump({"ontology": args.ontology, "terms": len(terms),
|
||||||
|
"violations": len(f.violations), "reviews": len(f.reviews),
|
||||||
|
"findings": f.items},
|
||||||
|
open(args.json, "w", encoding="utf-8"), indent=2)
|
||||||
|
print("json: %s" % args.json)
|
||||||
|
|
||||||
|
sys.exit(1 if f.violations else 0)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,416 @@
|
|||||||
|
{
|
||||||
|
"ontology": "ontology/pr_metamodel.ttl",
|
||||||
|
"terms": 130,
|
||||||
|
"violations": 49,
|
||||||
|
"reviews": 19,
|
||||||
|
"findings": [
|
||||||
|
{
|
||||||
|
"verdict": "VIOLATION",
|
||||||
|
"rule": "TN-026",
|
||||||
|
"term": "BaseTable",
|
||||||
|
"message": "is HARVESTED and names no harvestSource"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"verdict": "VIOLATION",
|
||||||
|
"rule": "TN-026",
|
||||||
|
"term": "ExternalTable",
|
||||||
|
"message": "is HARVESTED and names no harvestSource"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"verdict": "VIOLATION",
|
||||||
|
"rule": "TN-026",
|
||||||
|
"term": "ForeignKey",
|
||||||
|
"message": "is HARVESTED and names no harvestSource"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"verdict": "VIOLATION",
|
||||||
|
"rule": "TN-026",
|
||||||
|
"term": "PrimaryKey",
|
||||||
|
"message": "is HARVESTED and names no harvestSource"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"verdict": "VIOLATION",
|
||||||
|
"rule": "TN-026",
|
||||||
|
"term": "SystemType",
|
||||||
|
"message": "does not declare pr:authoringMode"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"verdict": "VIOLATION",
|
||||||
|
"rule": "TN-027",
|
||||||
|
"term": "SystemType",
|
||||||
|
"message": "descends from 0 layer roots; exactly one is required"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"verdict": "VIOLATION",
|
||||||
|
"rule": "TN-027",
|
||||||
|
"term": "SystemType",
|
||||||
|
"message": "descends from 0 provenance axes; exactly one is required"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"verdict": "VIOLATION",
|
||||||
|
"rule": "TN-026",
|
||||||
|
"term": "View",
|
||||||
|
"message": "is HARVESTED and names no harvestSource"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"verdict": "REVIEW",
|
||||||
|
"rule": "TN-025",
|
||||||
|
"term": "belongsTo",
|
||||||
|
"message": "declares itself polymorphic; confirm the scope is declared in SHACL"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"verdict": "VIOLATION",
|
||||||
|
"rule": "TN-024",
|
||||||
|
"term": "consumes",
|
||||||
|
"message": "carries no rdfs:comment"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"verdict": "VIOLATION",
|
||||||
|
"rule": "TN-024",
|
||||||
|
"term": "containsBIField",
|
||||||
|
"message": "carries no rdfs:comment"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"verdict": "VIOLATION",
|
||||||
|
"rule": "TN-024",
|
||||||
|
"term": "governedBy",
|
||||||
|
"message": "carries no rdfs:comment"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"verdict": "VIOLATION",
|
||||||
|
"rule": "TN-024",
|
||||||
|
"term": "hasDomainOwner",
|
||||||
|
"message": "carries no rdfs:comment"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"verdict": "VIOLATION",
|
||||||
|
"rule": "TN-024",
|
||||||
|
"term": "hasElement",
|
||||||
|
"message": "carries no rdfs:comment"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"verdict": "VIOLATION",
|
||||||
|
"rule": "TN-024",
|
||||||
|
"term": "hasGovernanceLead",
|
||||||
|
"message": "carries no rdfs:comment"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"verdict": "VIOLATION",
|
||||||
|
"rule": "TN-024",
|
||||||
|
"term": "hasMetric",
|
||||||
|
"message": "carries no rdfs:comment"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"verdict": "VIOLATION",
|
||||||
|
"rule": "TN-024",
|
||||||
|
"term": "hasSubDomainOwner",
|
||||||
|
"message": "carries no rdfs:comment"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"verdict": "VIOLATION",
|
||||||
|
"rule": "TN-024",
|
||||||
|
"term": "hasSystemType",
|
||||||
|
"message": "carries no rdfs:comment"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"verdict": "VIOLATION",
|
||||||
|
"rule": "TN-024",
|
||||||
|
"term": "hostedOn",
|
||||||
|
"message": "carries no rdfs:comment"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"verdict": "VIOLATION",
|
||||||
|
"rule": "TN-024",
|
||||||
|
"term": "isInBIDataSource",
|
||||||
|
"message": "carries no rdfs:comment"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"verdict": "VIOLATION",
|
||||||
|
"rule": "TN-024",
|
||||||
|
"term": "isInBIWorkspace",
|
||||||
|
"message": "carries no rdfs:comment"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"verdict": "VIOLATION",
|
||||||
|
"rule": "TN-024",
|
||||||
|
"term": "isInDatabase",
|
||||||
|
"message": "carries no rdfs:comment"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"verdict": "VIOLATION",
|
||||||
|
"rule": "TN-024",
|
||||||
|
"term": "livesIn",
|
||||||
|
"message": "carries no rdfs:comment"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"verdict": "VIOLATION",
|
||||||
|
"rule": "TN-024",
|
||||||
|
"term": "measures",
|
||||||
|
"message": "carries no rdfs:comment"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"verdict": "VIOLATION",
|
||||||
|
"rule": "TN-024",
|
||||||
|
"term": "ownedBy",
|
||||||
|
"message": "carries no rdfs:comment"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"verdict": "REVIEW",
|
||||||
|
"rule": "TN-025",
|
||||||
|
"term": "ownedBy",
|
||||||
|
"message": "declares itself polymorphic; confirm the scope is declared in SHACL"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"verdict": "REVIEW",
|
||||||
|
"rule": "TN-025",
|
||||||
|
"term": "ownedByDomain",
|
||||||
|
"message": "declares itself polymorphic; confirm the scope is declared in SHACL"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"verdict": "VIOLATION",
|
||||||
|
"rule": "TN-024",
|
||||||
|
"term": "produces",
|
||||||
|
"message": "carries no rdfs:comment"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"verdict": "VIOLATION",
|
||||||
|
"rule": "TN-024",
|
||||||
|
"term": "referencesSourceField",
|
||||||
|
"message": "carries no rdfs:comment"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"verdict": "VIOLATION",
|
||||||
|
"rule": "TN-024",
|
||||||
|
"term": "referencesTargetField",
|
||||||
|
"message": "carries no rdfs:comment"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"verdict": "REVIEW",
|
||||||
|
"rule": "TN-025",
|
||||||
|
"term": "represents",
|
||||||
|
"message": "declares itself polymorphic; confirm the scope is declared in SHACL"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"verdict": "REVIEW",
|
||||||
|
"rule": "TN-025",
|
||||||
|
"term": "sourcedFrom",
|
||||||
|
"message": "declares itself polymorphic; confirm the scope is declared in SHACL"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"verdict": "REVIEW",
|
||||||
|
"rule": "TN-025",
|
||||||
|
"term": "storedIn",
|
||||||
|
"message": "declares itself polymorphic; confirm the scope is declared in SHACL"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"verdict": "VIOLATION",
|
||||||
|
"rule": "TN-024",
|
||||||
|
"term": "usesBIDataSource",
|
||||||
|
"message": "carries no rdfs:comment"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"verdict": "VIOLATION",
|
||||||
|
"rule": "TN-024",
|
||||||
|
"term": "activationStatus",
|
||||||
|
"message": "carries no rdfs:comment"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"verdict": "REVIEW",
|
||||||
|
"rule": "TN-025",
|
||||||
|
"term": "arbitrationStatus",
|
||||||
|
"message": "declares itself polymorphic; confirm the scope is declared in SHACL"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"verdict": "VIOLATION",
|
||||||
|
"rule": "TN-024",
|
||||||
|
"term": "businessRule",
|
||||||
|
"message": "carries no rdfs:comment"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"verdict": "VIOLATION",
|
||||||
|
"rule": "TN-024",
|
||||||
|
"term": "canonicalName",
|
||||||
|
"message": "carries no rdfs:comment"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"verdict": "REVIEW",
|
||||||
|
"rule": "TN-025",
|
||||||
|
"term": "canonicalName",
|
||||||
|
"message": "declares itself polymorphic; confirm the scope is declared in SHACL"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"verdict": "VIOLATION",
|
||||||
|
"rule": "TN-024",
|
||||||
|
"term": "creationDate",
|
||||||
|
"message": "carries no rdfs:comment"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"verdict": "REVIEW",
|
||||||
|
"rule": "TN-025",
|
||||||
|
"term": "creationDate",
|
||||||
|
"message": "declares itself polymorphic; confirm the scope is declared in SHACL"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"verdict": "VIOLATION",
|
||||||
|
"rule": "TN-024",
|
||||||
|
"term": "environment",
|
||||||
|
"message": "carries no rdfs:comment"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"verdict": "VIOLATION",
|
||||||
|
"rule": "TN-024",
|
||||||
|
"term": "exampleValue",
|
||||||
|
"message": "carries no rdfs:comment"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"verdict": "REVIEW",
|
||||||
|
"rule": "TN-025",
|
||||||
|
"term": "exampleValue",
|
||||||
|
"message": "declares itself polymorphic; confirm the scope is declared in SHACL"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"verdict": "VIOLATION",
|
||||||
|
"rule": "TN-024",
|
||||||
|
"term": "formula",
|
||||||
|
"message": "carries no rdfs:comment"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"verdict": "REVIEW",
|
||||||
|
"rule": "TN-025",
|
||||||
|
"term": "fullyQualifiedName",
|
||||||
|
"message": "declares itself polymorphic; confirm the scope is declared in SHACL"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"verdict": "REVIEW",
|
||||||
|
"rule": "TN-025",
|
||||||
|
"term": "harvestDate",
|
||||||
|
"message": "declares itself polymorphic; confirm the scope is declared in SHACL"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"verdict": "VIOLATION",
|
||||||
|
"rule": "TN-024",
|
||||||
|
"term": "identifier",
|
||||||
|
"message": "carries no rdfs:comment"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"verdict": "REVIEW",
|
||||||
|
"rule": "TN-025",
|
||||||
|
"term": "identifier",
|
||||||
|
"message": "declares itself polymorphic; confirm the scope is declared in SHACL"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"verdict": "VIOLATION",
|
||||||
|
"rule": "TN-024",
|
||||||
|
"term": "isNullable",
|
||||||
|
"message": "carries no rdfs:comment"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"verdict": "VIOLATION",
|
||||||
|
"rule": "TN-024",
|
||||||
|
"term": "lastReviewDate",
|
||||||
|
"message": "carries no rdfs:comment"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"verdict": "REVIEW",
|
||||||
|
"rule": "TN-025",
|
||||||
|
"term": "lastReviewDate",
|
||||||
|
"message": "declares itself polymorphic; confirm the scope is declared in SHACL"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"verdict": "VIOLATION",
|
||||||
|
"rule": "TN-024",
|
||||||
|
"term": "logicalFormat",
|
||||||
|
"message": "carries no rdfs:comment"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"verdict": "VIOLATION",
|
||||||
|
"rule": "TN-024",
|
||||||
|
"term": "ordinalPosition",
|
||||||
|
"message": "carries no rdfs:comment"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"verdict": "VIOLATION",
|
||||||
|
"rule": "TN-024",
|
||||||
|
"term": "physicalDataType",
|
||||||
|
"message": "carries no rdfs:comment"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"verdict": "REVIEW",
|
||||||
|
"rule": "TN-025",
|
||||||
|
"term": "physicalName",
|
||||||
|
"message": "declares itself polymorphic; confirm the scope is declared in SHACL"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"verdict": "VIOLATION",
|
||||||
|
"rule": "TN-024",
|
||||||
|
"term": "queryCount",
|
||||||
|
"message": "carries no rdfs:comment"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"verdict": "REVIEW",
|
||||||
|
"rule": "TN-025",
|
||||||
|
"term": "sourceIdentifier",
|
||||||
|
"message": "declares itself polymorphic; confirm the scope is declared in SHACL"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"verdict": "VIOLATION",
|
||||||
|
"rule": "TN-024",
|
||||||
|
"term": "status",
|
||||||
|
"message": "carries no rdfs:comment"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"verdict": "REVIEW",
|
||||||
|
"rule": "TN-025",
|
||||||
|
"term": "status",
|
||||||
|
"message": "declares itself polymorphic; confirm the scope is declared in SHACL"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"verdict": "VIOLATION",
|
||||||
|
"rule": "TN-024",
|
||||||
|
"term": "synonym",
|
||||||
|
"message": "carries no rdfs:comment"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"verdict": "VIOLATION",
|
||||||
|
"rule": "TN-024",
|
||||||
|
"term": "technicalDefinition",
|
||||||
|
"message": "carries no rdfs:comment"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"verdict": "VIOLATION",
|
||||||
|
"rule": "TN-024",
|
||||||
|
"term": "timeAggregation",
|
||||||
|
"message": "carries no rdfs:comment"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"verdict": "REVIEW",
|
||||||
|
"rule": "TN-025",
|
||||||
|
"term": "unit",
|
||||||
|
"message": "declares itself polymorphic; confirm the scope is declared in SHACL"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"verdict": "VIOLATION",
|
||||||
|
"rule": "TN-024",
|
||||||
|
"term": "version",
|
||||||
|
"message": "carries no rdfs:comment"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"verdict": "REVIEW",
|
||||||
|
"rule": "TN-025",
|
||||||
|
"term": "version",
|
||||||
|
"message": "declares itself polymorphic; confirm the scope is declared in SHACL"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"verdict": "VIOLATION",
|
||||||
|
"rule": "TN-024",
|
||||||
|
"term": "viewDefinition",
|
||||||
|
"message": "carries no rdfs:comment"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"verdict": "VIOLATION",
|
||||||
|
"rule": "TN-024",
|
||||||
|
"term": "acronym",
|
||||||
|
"message": "carries no rdfs:comment"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
# T-BOX AND A-BOX MIGRATION TO v2.2 — GOVERNANCE ASSIGNMENTS
|
||||||
|
# =============================================================================
|
||||||
|
# Data consumed by migrate_tbox_v2_2.py.
|
||||||
|
#
|
||||||
|
# An actor node is an ASSIGNMENT, not a person. The post is stable and carries
|
||||||
|
# the canonical name, the identifier and the domain it answers to; the person
|
||||||
|
# holding it is a value that changes. Modelled the other way round, a change of
|
||||||
|
# incumbent would move nodes and edges rather than one literal, and the same
|
||||||
|
# person covering two domains would exist twice.
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
meta:
|
||||||
|
from_version: "2.1"
|
||||||
|
to_version: "2.2"
|
||||||
|
namespace: "https://ontology.pernod-ricard.com/metamodel/"
|
||||||
|
instance_namespace: "https://data.pernod-ricard.com/sodh/"
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# T-BOX
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
declare_properties:
|
||||||
|
- term: personName
|
||||||
|
kind: DatatypeProperty
|
||||||
|
domain: Actor
|
||||||
|
range: "http://www.w3.org/2001/XMLSchema#string"
|
||||||
|
comment: >
|
||||||
|
The name of the person currently holding the post. Declared on Actor rather
|
||||||
|
than on each role, since every role shares it and Actor is their smallest
|
||||||
|
common ancestor below the layer root. Held as a value rather than a node
|
||||||
|
because what changes is the person, not the assignment: the post keeps its
|
||||||
|
identifier, its domain and every edge that points at it, and a change of
|
||||||
|
holder rewrites one literal. Used on actors and nowhere else.
|
||||||
|
|
||||||
|
display_updates:
|
||||||
|
- {term: DataSteward, acronym: DST}
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# A-BOX — RENAME
|
||||||
|
# ST is not the short form of Data Steward; DST is, and the rulebook now
|
||||||
|
# declares it.
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
instance_renames:
|
||||||
|
- {from: ST_DD_04, to: DST_DD_04}
|
||||||
|
- {from: ST_DD_05, to: DST_DD_05}
|
||||||
|
- {from: ST_DD_06, to: DST_DD_06}
|
||||||
|
- {from: ST_DD_10, to: DST_DD_10}
|
||||||
|
- {from: ST_DD_16, to: DST_DD_16}
|
||||||
|
- {from: ST_DD_21, to: DST_DD_21}
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# A-BOX — COMPLETE THE EXISTING ASSIGNMENTS
|
||||||
|
# The canonical name becomes the name of the POST. The person moves to heldBy.
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
complete:
|
||||||
|
- {node: DDO_DD_04, identifier: "DDO-04", name: "Data Domain Owner", held_by: "David Carpio", domain: DD_04}
|
||||||
|
- {node: DDO_DD_05, identifier: "DDO-05", name: "Data Domain Owner", held_by: "David Carpio", domain: DD_05}
|
||||||
|
- {node: DDO_DD_06, identifier: "DDO-06", name: "Data Domain Owner", held_by: "Ludovic Cottier", domain: DD_06}
|
||||||
|
- {node: DDO_DD_10, identifier: "DDO-10", name: "Data Domain Owner", held_by: "Alberto Lupano", domain: DD_10}
|
||||||
|
- {node: DDO_DD_16, identifier: "DDO-16", name: "Data Domain Owner", held_by: "Julien Soisson", domain: DD_16}
|
||||||
|
- {node: DDO_DD_21, identifier: "DDO-21", name: "Data Domain Owner", held_by: "Vincent Meunier", domain: DD_21}
|
||||||
|
- {node: DGL_DD_04, identifier: "DGL-04", name: "Data Governance Lead", held_by: "Marie Carabin", domain: DD_04}
|
||||||
|
- {node: DGL_DD_05, identifier: "DGL-05", name: "Data Governance Lead", held_by: "Alberic Piot", domain: DD_05}
|
||||||
|
- {node: DGL_DD_06, identifier: "DGL-06", name: "Data Governance Lead", held_by: "Bastien Gourdon", domain: DD_06}
|
||||||
|
- {node: DGL_DD_10, identifier: "DGL-10", name: "Data Governance Lead", held_by: "Anas El Kesri", domain: DD_10}
|
||||||
|
- {node: DGL_DD_16, identifier: "DGL-16", name: "Data Governance Lead", held_by: "Gaelle Seret", domain: DD_16}
|
||||||
|
- {node: DGL_DD_21, identifier: "DGL-21", name: "Data Governance Lead", held_by: "Bastien Gourdon", domain: DD_21}
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# A-BOX — SUB-DOMAIN OWNERS, 10
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
create_sub_domain_owners:
|
||||||
|
- {node: SDO_SD_04_01, identifier: "SDO-04.01", held_by: "Etienne Coulon", sub_domain: SD_04_01, domain: DD_04}
|
||||||
|
- {node: SDO_SD_04_02, identifier: "SDO-04.02", held_by: "Etienne Coulon", sub_domain: SD_04_02, domain: DD_04}
|
||||||
|
- {node: SDO_SD_04_03, identifier: "SDO-04.03", held_by: "Etienne Coulon", sub_domain: SD_04_03, domain: DD_04}
|
||||||
|
- {node: SDO_SD_05_01, identifier: "SDO-05.01", held_by: "David Carpio", sub_domain: SD_05_01, domain: DD_05}
|
||||||
|
- {node: SDO_SD_06_01, identifier: "SDO-06.01", held_by: "Loic Berger", sub_domain: SD_06_01, domain: DD_06}
|
||||||
|
- {node: SDO_SD_10_01, identifier: "SDO-10.01", held_by: "Alberto Lupano", sub_domain: SD_10_01, domain: DD_10}
|
||||||
|
- {node: SDO_SD_16_01, identifier: "SDO-16.01", held_by: "Julien Soisson", sub_domain: SD_16_01, domain: DD_16}
|
||||||
|
- {node: SDO_SD_16_02, identifier: "SDO-16.02", held_by: "Julien Soisson", sub_domain: SD_16_02, domain: DD_16}
|
||||||
|
- {node: SDO_SD_16_03, identifier: "SDO-16.03", held_by: "Julien Soisson", sub_domain: SD_16_03, domain: DD_16}
|
||||||
|
- {node: SDO_SD_21_01, identifier: "SDO-21.01", held_by: "Vincent Meunier", sub_domain: SD_21_01, domain: DD_21}
|
||||||
|
|
||||||
|
sub_domain_owner_name: Data Sub Domain Owner
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# A-BOX — PRODUCT OWNER, 1
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
create_product_owners:
|
||||||
|
- {node: PO_SODH, identifier: "PO-06.01-001", held_by: "Guilherme Muller", product: DP_06_01_001, domain: DD_06}
|
||||||
|
|
||||||
|
product_owner_name: Data Product Owner
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# A-BOX — STEWARDS, ONE ASSIGNMENT PER BUSINESS OBJECT
|
||||||
|
# Derived from the graph rather than listed: the business objects are the source
|
||||||
|
# of truth for how many assignments exist and which domain each answers to.
|
||||||
|
# The incumbent is read from this map by domain.
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
steward_holders:
|
||||||
|
DD_04: Marc Chambouleyron
|
||||||
|
DD_05: Francois Vadrouille
|
||||||
|
DD_06: Michael Flores
|
||||||
|
DD_10: Magda Kugli
|
||||||
|
DD_16: Clementine Sauries
|
||||||
|
DD_21: Alberic Piot
|
||||||
|
|
||||||
|
steward_name: Data Steward
|
||||||
|
|
||||||
|
# The six per-domain steward nodes are replaced by one per business object.
|
||||||
|
# monitoredBy is repointed from the old node to the new one, and the old node is
|
||||||
|
# withdrawn once nothing names it (EV-003).
|
||||||
|
retire_domain_stewards: true
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# A-BOX — GOVERN A TRANSVERSE CONCEPT
|
||||||
|
# Panel Coverage is used by four business objects through usesConcept and is the
|
||||||
|
# subject of none. A concept mobilised without an object that carries it has no
|
||||||
|
# owner, no steward and no governance chain: it is used without being governed.
|
||||||
|
# Creating the missing business object closes the chain BC -> BO -> SD -> DD,
|
||||||
|
# and the steward assignment follows automatically since assignments are derived
|
||||||
|
# from the business objects present in the graph.
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
create_business_objects:
|
||||||
|
- node: BO_06_01_006
|
||||||
|
identifier: "BO-06.01-006"
|
||||||
|
name: Panel Coverage
|
||||||
|
is_about: BC_06_01_003
|
||||||
|
sub_domain: SD_06_01
|
||||||
|
domain: DD_06
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# A-BOX — ownedBy AGAINST THE OLD DOMAIN-LEVEL STEWARDS
|
||||||
|
# 53 subjects declared they belonged to the per-domain steward node. That node
|
||||||
|
# disappears, so each reference is resolved rather than dropped in silence.
|
||||||
|
#
|
||||||
|
# business object -> removed. It already carries monitoredBy towards the
|
||||||
|
# same assignment, and two properties holding one fact
|
||||||
|
# are two answers to one question waiting to diverge.
|
||||||
|
# sub-domain -> repointed to its Data Sub Domain Owner, whose function
|
||||||
|
# this is; a steward had no business holding it.
|
||||||
|
# everything else -> repointed to the steward assignment of the business
|
||||||
|
# object it belongs to, followed through the graph.
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
owned_by_resolution:
|
||||||
|
remove_on: [BusinessObject]
|
||||||
|
repoint_sub_domains: true
|
||||||
|
repoint_to_business_object_assignment: true
|
||||||
|
# Chains followed to reach the business object, in order. The first that
|
||||||
|
# resolves wins; anything unresolved is reported and left untouched.
|
||||||
|
chains:
|
||||||
|
- {via: isAbout, direction: inbound} # a BO is about this concept
|
||||||
|
- {via: represents, direction: outbound} # this object represents a BO
|
||||||
|
- {via: measures, direction: outbound} # a metric measures a concept, then isAbout
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# COMMON
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
status: PUBLISHED
|
||||||
|
version: "1.0"
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# SHAPES — NR-002 EXEMPTION
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
shape_notes:
|
||||||
|
- rule: NR-002
|
||||||
|
change: exempt descendants of Actor from the canonical-name uniqueness query
|
||||||
|
reason: >
|
||||||
|
An assignment is a post, not a catalogued object. Five steward posts in one
|
||||||
|
domain are legitimately homonymous; what tells them apart is the identifier
|
||||||
|
and the business object that names them. Applying a uniqueness rule written
|
||||||
|
for catalogued objects would force fifteen invented names for one job.
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
# T-BOX MIGRATION TO v2.1 — DECLARATIONS
|
||||||
|
# =============================================================================
|
||||||
|
# Data consumed by migrate_tbox_v2_1.py. No renaming here: v2.1 declares what
|
||||||
|
# v2.0 left silent.
|
||||||
|
#
|
||||||
|
# Three subjects, each closing a class of TN-025, TN-026 and TN-027 findings:
|
||||||
|
# 1. polymorphism made explicit rather than inferred from prose
|
||||||
|
# 2. domains stated wherever a single class of use exists
|
||||||
|
# 3. provenance moved off the terms and onto the concrete classes
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
meta:
|
||||||
|
from_version: "2.0"
|
||||||
|
to_version: "2.1"
|
||||||
|
namespace: "https://ontology.pernod-ricard.com/metamodel/"
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# THE ANNOTATION ITSELF (TN-025)
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
declare_annotations:
|
||||||
|
- term: polymorphic
|
||||||
|
label: polymorphic
|
||||||
|
comment: >
|
||||||
|
Declares that a property deliberately carries no rdfs:domain because it
|
||||||
|
serves several classes with no useful common ancestor. Its scope is
|
||||||
|
controlled class by class in SHACL instead. Without this axiom a deliberate
|
||||||
|
omission and a forgotten one look identical, and no check can tell them
|
||||||
|
apart.
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# POLYMORPHIC PROPERTIES — 18
|
||||||
|
#
|
||||||
|
# The test: a property takes as domain the smallest common ancestor of its
|
||||||
|
# classes of use, PROVIDED that ancestor sits below a layer root. A layer root,
|
||||||
|
# a provenance axis and MetaModelObject are all too high to say anything; a
|
||||||
|
# property whose smallest common ancestor is one of those is polymorphic.
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
polymorphic:
|
||||||
|
- {term: identifier, reason: "borne by every object in the graph"}
|
||||||
|
- {term: canonicalName, reason: "borne by every object in the graph"}
|
||||||
|
- {term: status, reason: "borne by every object in the graph"}
|
||||||
|
- {term: version, reason: "borne by every object in the graph"}
|
||||||
|
- {term: creationDate, reason: "borne by every object in the graph"}
|
||||||
|
- {term: lastReviewDate, reason: "borne by every object in the graph"}
|
||||||
|
- {term: arbitrationStatus, reason: "any declared object may be under arbitration"}
|
||||||
|
- {term: ownedByDomain, reason: "smallest common ancestor is a provenance axis, not a layer class"}
|
||||||
|
- {term: harvestDate, reason: "smallest common ancestor is a provenance axis, not a layer class"}
|
||||||
|
- {term: sourceIdentifier, reason: "smallest common ancestor is a provenance axis, not a layer class"}
|
||||||
|
- {term: belongsTo, reason: "sub-domain to domain and business object to sub-domain: two couples with no useful ancestor"}
|
||||||
|
- {term: storedIn, reason: "two granularities, data object to structure and data element to field"}
|
||||||
|
- {term: represents, reason: "data object to business object and data element to business concept"}
|
||||||
|
- {term: unit, reason: "data element and metric sit in different layers"}
|
||||||
|
- {term: sourcedFrom, reason: "data object and data structure sit in different layers"}
|
||||||
|
- {term: fullyQualifiedName, reason: "DATABASE.SCHEMA.RELATION[.COLUMN] names a structure and a column alike"}
|
||||||
|
- {term: ownedBy, reason: "any object may have an owner; Actor is the range, the domain is free"}
|
||||||
|
- {term: exampleValue, reason: "observed on a field, documented on a data element"}
|
||||||
|
- {term: physicalName, reason: "a data object names its table as a data element names its column; BR-013 names one carrier where there are two"}
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# DOMAINS TO DECLARE — 18
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
domains:
|
||||||
|
- {term: formula, domain: Metric, basis: "shape; forbidden on BusinessConcept, which is what distinguishes a metric"}
|
||||||
|
- {term: timeAggregation, domain: Metric, basis: "belongs to the calculation, alongside formula"}
|
||||||
|
- {term: businessDefinition, domain: BusinessConcept, basis: "shape; forbidden on BusinessObject, whose meaning lives in its concept"}
|
||||||
|
- {term: businessRule, domain: BusinessConcept, basis: "meaning belongs to the concept"}
|
||||||
|
- {term: synonym, domain: BusinessConcept, basis: "meaning belongs to the concept"}
|
||||||
|
- {term: technicalDefinition, domain: BusinessConcept, basis: "meaning belongs to the concept"}
|
||||||
|
- {term: monitoredBy, domain: BusinessObject, basis: "shape; forbidden on DataElement and DataObject, where stewardship is inherited"}
|
||||||
|
- {term: logicalFormat, domain: DataElement, basis: "a logical characteristic of the element"}
|
||||||
|
- {term: isNullable, domain: Field, basis: "a property of the column"}
|
||||||
|
- {term: ordinalPosition, domain: Field, basis: "a property of the column"}
|
||||||
|
- {term: physicalDataType, domain: Field, basis: "a property of the column"}
|
||||||
|
- {term: isInSchema, domain: DataStructure, basis: "shape"}
|
||||||
|
- {term: viewDefinition, domain: View, basis: "a table has no SQL definition"}
|
||||||
|
- {term: queryCount, domain: DataStructure, basis: "ACCESS_HISTORY traces tables, views and external tables alike"}
|
||||||
|
- {term: lastQueryDate, domain: DataStructure, basis: "ACCESS_HISTORY traces tables, views and external tables alike"}
|
||||||
|
- {term: definitionAddress, domain: Transformation, basis: "the Git URI of the code behind a transformation; a view uses viewDefinition"}
|
||||||
|
- {term: expression, domain: BusinessIntelligenceField, basis: "the calculated-field formula, compared against the metric formula to detect drift"}
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# NEW TERM (procedure C1)
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
declare_properties:
|
||||||
|
- term: serviceCommitment
|
||||||
|
kind: DatatypeProperty
|
||||||
|
domain: DataContract
|
||||||
|
range: "http://www.w3.org/2001/XMLSchema#string"
|
||||||
|
comment: >
|
||||||
|
What the contract guarantees about the product it covers: refresh schedule,
|
||||||
|
freshness, quality thresholds, support channel, notice period on a breaking
|
||||||
|
change. Held as prose for now, which no shape can verify; the structured
|
||||||
|
form, aligned on ODCS, is the next step. Recorded here because the
|
||||||
|
commitment had been written into businessRule for want of anywhere else,
|
||||||
|
where it sat next to genuine business rules and could not be told apart.
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# RANGES MISSING ON POLYMORPHIC PROPERTIES
|
||||||
|
# A polymorphic property may still have a single range: what it points AT can be
|
||||||
|
# settled even when what it starts FROM cannot.
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
ranges: []
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# PROVENANCE (TN-026)
|
||||||
|
#
|
||||||
|
# A vocabulary term is declared, always and by definition. What has a provenance
|
||||||
|
# is the INSTANCES, and the concrete class is where the model states which one.
|
||||||
|
# Nothing is inherited: each class declares its own mode, and the mode agrees
|
||||||
|
# with the provenance axis the class descends from.
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
provenance:
|
||||||
|
strip_from_non_concrete: true # abstract classes, properties, vocabulary terms
|
||||||
|
derive_from_axis: true # CapturedObject -> HARVESTED, DefinedObject -> ASSERTED
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# ATTACHMENT (TN-027)
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
attach:
|
||||||
|
- term: GovernanceLayerObject
|
||||||
|
parent: DefinedObject
|
||||||
|
basis: >
|
||||||
|
A layer root sits on a provenance axis like any other. Actors and governance
|
||||||
|
objects are declared by governance, never harvested, so the governance layer
|
||||||
|
descends from DefinedObject and its five roles inherit the axis.
|
||||||
|
|
||||||
|
# SystemType remains outside the layers: it is a controlled-vocabulary class,
|
||||||
|
# the named and closed exception of TN-027. Recorded as an open arbitration
|
||||||
|
# rather than silently resolved here.
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# INSTANCE CORRECTIONS
|
||||||
|
# Each one was surfaced by declaring a domain: an instance carrying a property
|
||||||
|
# outside its domain is retyped by RDFS inference, and the shapes of the wrong
|
||||||
|
# class then fire on it.
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
instance_fixes:
|
||||||
|
- action: remove
|
||||||
|
property: businessDefinition
|
||||||
|
subjects: [DI_06_01_001, DI_06_01_002]
|
||||||
|
reason: >
|
||||||
|
Meaning belongs to the Business Concept. An interface is a channel through
|
||||||
|
which data is delivered; it defines nothing. Same rule the shapes already
|
||||||
|
enforce on Business Object.
|
||||||
|
- action: remove
|
||||||
|
property: sourcedFrom
|
||||||
|
subjects: [DI_06_01_001, DI_06_01_002]
|
||||||
|
reason: >
|
||||||
|
An interface has no system of truth: the data it exposes is sourced by the
|
||||||
|
Data Objects behind it, and those name the system.
|
||||||
|
- action: move
|
||||||
|
property: businessRule
|
||||||
|
to: serviceCommitment
|
||||||
|
subjects: [DC_06_01_001]
|
||||||
|
reason: >
|
||||||
|
The text is a service commitment, not a business rule: refresh window,
|
||||||
|
notice period, support channel. It was written into businessRule for want
|
||||||
|
of a property to hold it.
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
{
|
||||||
|
"attempt_timestamp": "2026-08-05T09:31:13",
|
||||||
|
"mode": "dry-run",
|
||||||
|
"from_version": "1.6",
|
||||||
|
"to_version": "2.0",
|
||||||
|
"input_checksums": {
|
||||||
|
"ontology/pr_metamodel.ttl": "552bbe0e9c0883d869ac5d6de40503bb",
|
||||||
|
"instances/sodh.ttl": "9eb1dbb96e325dbd61c6e880b43eaa44",
|
||||||
|
"shapes/pr_metamodel_shapes.ttl": "f6a1b4681e5817f8852c81424312e3d9"
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"deprecated_dropped": 119,
|
||||||
|
"deprecated_kept": 2,
|
||||||
|
"rename_order_1": 244,
|
||||||
|
"rename_order_2": 256,
|
||||||
|
"rename_order_3": 14,
|
||||||
|
"merge": 5,
|
||||||
|
"reclassified": 2,
|
||||||
|
"classes_created": 1,
|
||||||
|
"reparented": 1,
|
||||||
|
"to_literal_property": 2,
|
||||||
|
"to_literal_values": 6,
|
||||||
|
"to_literal_dropped": 18,
|
||||||
|
"isAbstract_declared": 31,
|
||||||
|
"display_removed": 22,
|
||||||
|
"display_set": 25,
|
||||||
|
"labels_regenerated": 78,
|
||||||
|
"version_bumped": 1
|
||||||
|
},
|
||||||
|
"total_changes": 827,
|
||||||
|
"triples_before": {
|
||||||
|
"ontology/pr_metamodel.ttl": 853,
|
||||||
|
"instances/sodh.ttl": 2308,
|
||||||
|
"shapes/pr_metamodel_shapes.ttl": 395
|
||||||
|
},
|
||||||
|
"triples_after": {
|
||||||
|
"ontology/pr_metamodel.ttl": 734,
|
||||||
|
"instances/sodh.ttl": 2308,
|
||||||
|
"shapes/pr_metamodel_shapes.ttl": 395
|
||||||
|
},
|
||||||
|
"notes": [
|
||||||
|
"deprecated_kept: materializedAs still used 1 times",
|
||||||
|
"deprecated_kept: hasGranularity still used 1 times",
|
||||||
|
"reclassified: shortLabel",
|
||||||
|
"reclassified: acronym",
|
||||||
|
"classes_created: GovernanceLayerObject",
|
||||||
|
"reparented: Actor -> GovernanceLayerObject",
|
||||||
|
"to_literal_property: activationStatus",
|
||||||
|
"to_literal_property: environment",
|
||||||
|
"version_bumped: https://ontology.pernod-ricard.com/metamodel/2.0"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
{
|
||||||
|
"attempt_timestamp": "2026-08-05T14:11:08",
|
||||||
|
"mode": "dry-run",
|
||||||
|
"from_version": "1.6",
|
||||||
|
"to_version": "2.0",
|
||||||
|
"input_checksums": {
|
||||||
|
"ontology/pr_metamodel.ttl": "552bbe0e9c0883d869ac5d6de40503bb",
|
||||||
|
"instances/sodh.ttl": "9eb1dbb96e325dbd61c6e880b43eaa44",
|
||||||
|
"shapes/pr_metamodel_shapes.ttl": "f6a1b4681e5817f8852c81424312e3d9"
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"deprecated_dropped": 132,
|
||||||
|
"rename_order_1": 244,
|
||||||
|
"rename_order_2": 256,
|
||||||
|
"rename_order_3": 14,
|
||||||
|
"abox_aligned": 353,
|
||||||
|
"merge": 5,
|
||||||
|
"reclassified": 2,
|
||||||
|
"classes_created": 1,
|
||||||
|
"reparented": 1,
|
||||||
|
"to_literal_property": 2,
|
||||||
|
"to_literal_values": 6,
|
||||||
|
"to_literal_dropped": 18,
|
||||||
|
"isAbstract_declared": 31,
|
||||||
|
"display_removed": 22,
|
||||||
|
"display_set": 25,
|
||||||
|
"labels_regenerated": 76,
|
||||||
|
"version_bumped": 1,
|
||||||
|
"shapes_conformsTo": 1
|
||||||
|
},
|
||||||
|
"total_changes": 1190,
|
||||||
|
"triples_before": {
|
||||||
|
"ontology/pr_metamodel.ttl": 853,
|
||||||
|
"instances/sodh.ttl": 2308,
|
||||||
|
"shapes/pr_metamodel_shapes.ttl": 395
|
||||||
|
},
|
||||||
|
"triples_after": {
|
||||||
|
"ontology/pr_metamodel.ttl": 723,
|
||||||
|
"instances/sodh.ttl": 2308,
|
||||||
|
"shapes/pr_metamodel_shapes.ttl": 396
|
||||||
|
},
|
||||||
|
"notes": [
|
||||||
|
"reclassified: shortLabel",
|
||||||
|
"reclassified: acronym",
|
||||||
|
"classes_created: GovernanceLayerObject",
|
||||||
|
"reparented: Actor -> GovernanceLayerObject",
|
||||||
|
"to_literal_property: activationStatus",
|
||||||
|
"to_literal_property: environment",
|
||||||
|
"version_bumped: https://ontology.pernod-ricard.com/metamodel/2.0",
|
||||||
|
"shapes_conformsTo: https://ontology.pernod-ricard.com/metamodel/2.0"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
{
|
||||||
|
"attempt_timestamp": "2026-08-05T14:14:31",
|
||||||
|
"mode": "apply",
|
||||||
|
"from_version": "1.6",
|
||||||
|
"to_version": "2.0",
|
||||||
|
"input_checksums": {
|
||||||
|
"ontology/pr_metamodel.ttl": "552bbe0e9c0883d869ac5d6de40503bb",
|
||||||
|
"instances/sodh.ttl": "9eb1dbb96e325dbd61c6e880b43eaa44",
|
||||||
|
"shapes/pr_metamodel_shapes.ttl": "f6a1b4681e5817f8852c81424312e3d9"
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"deprecated_dropped": 132,
|
||||||
|
"rename_order_1": 244,
|
||||||
|
"rename_order_2": 256,
|
||||||
|
"rename_order_3": 14,
|
||||||
|
"abox_aligned": 353,
|
||||||
|
"merge": 5,
|
||||||
|
"reclassified": 2,
|
||||||
|
"classes_created": 1,
|
||||||
|
"reparented": 1,
|
||||||
|
"to_literal_property": 2,
|
||||||
|
"to_literal_values": 6,
|
||||||
|
"to_literal_dropped": 18,
|
||||||
|
"isAbstract_declared": 31,
|
||||||
|
"display_removed": 22,
|
||||||
|
"display_set": 25,
|
||||||
|
"labels_regenerated": 76,
|
||||||
|
"version_bumped": 1,
|
||||||
|
"shapes_conformsTo": 1
|
||||||
|
},
|
||||||
|
"total_changes": 1190,
|
||||||
|
"triples_before": {
|
||||||
|
"ontology/pr_metamodel.ttl": 853,
|
||||||
|
"instances/sodh.ttl": 2308,
|
||||||
|
"shapes/pr_metamodel_shapes.ttl": 395
|
||||||
|
},
|
||||||
|
"triples_after": {
|
||||||
|
"ontology/pr_metamodel.ttl": 723,
|
||||||
|
"instances/sodh.ttl": 2308,
|
||||||
|
"shapes/pr_metamodel_shapes.ttl": 396
|
||||||
|
},
|
||||||
|
"notes": [
|
||||||
|
"reclassified: shortLabel",
|
||||||
|
"reclassified: acronym",
|
||||||
|
"classes_created: GovernanceLayerObject",
|
||||||
|
"reparented: Actor -> GovernanceLayerObject",
|
||||||
|
"to_literal_property: activationStatus",
|
||||||
|
"to_literal_property: environment",
|
||||||
|
"version_bumped: https://ontology.pernod-ricard.com/metamodel/2.0",
|
||||||
|
"shapes_conformsTo: https://ontology.pernod-ricard.com/metamodel/2.0"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"attempt_timestamp": "2026-08-05T14:14:43",
|
||||||
|
"mode": "dry-run",
|
||||||
|
"from_version": "1.6",
|
||||||
|
"to_version": "2.0",
|
||||||
|
"input_checksums": {
|
||||||
|
"ontology/pr_metamodel.ttl": "32929c506d402738807b8b30c04fbb48",
|
||||||
|
"instances/sodh.ttl": "6bef7bad65c091738cd58cf5f0f1a395",
|
||||||
|
"shapes/pr_metamodel_shapes.ttl": "f87572aa372d4f6944c21e3cb12fb164"
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"rename_order_1": 0,
|
||||||
|
"rename_order_2": 0,
|
||||||
|
"rename_order_3": 0,
|
||||||
|
"abox_aligned": 0,
|
||||||
|
"merge": 0
|
||||||
|
},
|
||||||
|
"total_changes": 0,
|
||||||
|
"triples_before": {
|
||||||
|
"ontology/pr_metamodel.ttl": 723,
|
||||||
|
"instances/sodh.ttl": 2308,
|
||||||
|
"shapes/pr_metamodel_shapes.ttl": 396
|
||||||
|
},
|
||||||
|
"triples_after": {
|
||||||
|
"ontology/pr_metamodel.ttl": 723,
|
||||||
|
"instances/sodh.ttl": 2308,
|
||||||
|
"shapes/pr_metamodel_shapes.ttl": 396
|
||||||
|
},
|
||||||
|
"notes": []
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"attempt_timestamp": "2026-08-05T14:17:11",
|
||||||
|
"mode": "dry-run",
|
||||||
|
"from_version": "1.6",
|
||||||
|
"to_version": "2.0",
|
||||||
|
"input_checksums": {
|
||||||
|
"ontology/pr_metamodel.ttl": "32929c506d402738807b8b30c04fbb48",
|
||||||
|
"instances/sodh.ttl": "6bef7bad65c091738cd58cf5f0f1a395",
|
||||||
|
"shapes/pr_metamodel_shapes.ttl": "f6a1b4681e5817f8852c81424312e3d9"
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"rename_order_1": 9,
|
||||||
|
"rename_order_2": 2,
|
||||||
|
"rename_order_3": 0,
|
||||||
|
"abox_aligned": 0,
|
||||||
|
"merge": 0,
|
||||||
|
"shapes_conformsTo": 1
|
||||||
|
},
|
||||||
|
"total_changes": 12,
|
||||||
|
"triples_before": {
|
||||||
|
"ontology/pr_metamodel.ttl": 723,
|
||||||
|
"instances/sodh.ttl": 2308,
|
||||||
|
"shapes/pr_metamodel_shapes.ttl": 395
|
||||||
|
},
|
||||||
|
"triples_after": {
|
||||||
|
"ontology/pr_metamodel.ttl": 723,
|
||||||
|
"instances/sodh.ttl": 2308,
|
||||||
|
"shapes/pr_metamodel_shapes.ttl": 398
|
||||||
|
},
|
||||||
|
"notes": [
|
||||||
|
"shapes_conformsTo: https://ontology.pernod-ricard.com/metamodel/2.0"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"attempt_timestamp": "2026-08-05T14:18:45",
|
||||||
|
"mode": "apply",
|
||||||
|
"from_version": "1.6",
|
||||||
|
"to_version": "2.0",
|
||||||
|
"input_checksums": {
|
||||||
|
"ontology/pr_metamodel.ttl": "32929c506d402738807b8b30c04fbb48",
|
||||||
|
"instances/sodh.ttl": "6bef7bad65c091738cd58cf5f0f1a395",
|
||||||
|
"shapes/pr_metamodel_shapes.ttl": "f6a1b4681e5817f8852c81424312e3d9"
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"rename_order_1": 9,
|
||||||
|
"rename_order_2": 2,
|
||||||
|
"rename_order_3": 0,
|
||||||
|
"abox_aligned": 0,
|
||||||
|
"merge": 0,
|
||||||
|
"shapes_conformsTo": 1
|
||||||
|
},
|
||||||
|
"total_changes": 12,
|
||||||
|
"triples_before": {
|
||||||
|
"ontology/pr_metamodel.ttl": 723,
|
||||||
|
"instances/sodh.ttl": 2308,
|
||||||
|
"shapes/pr_metamodel_shapes.ttl": 395
|
||||||
|
},
|
||||||
|
"triples_after": {
|
||||||
|
"ontology/pr_metamodel.ttl": 723,
|
||||||
|
"instances/sodh.ttl": 2308,
|
||||||
|
"shapes/pr_metamodel_shapes.ttl": 398
|
||||||
|
},
|
||||||
|
"notes": [
|
||||||
|
"shapes_conformsTo: https://ontology.pernod-ricard.com/metamodel/2.0"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"attempt_timestamp": "2026-08-05T14:18:59",
|
||||||
|
"mode": "dry-run",
|
||||||
|
"from_version": "1.6",
|
||||||
|
"to_version": "2.0",
|
||||||
|
"input_checksums": {
|
||||||
|
"ontology/pr_metamodel.ttl": "ab4100d1eb8f0e16231339f675f80797",
|
||||||
|
"instances/sodh.ttl": "6bef7bad65c091738cd58cf5f0f1a395",
|
||||||
|
"shapes/pr_metamodel_shapes.ttl": "baa6fdf5edae6d8e5b1772f5ee467df9"
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"rename_order_1": 0,
|
||||||
|
"rename_order_2": 0,
|
||||||
|
"rename_order_3": 0,
|
||||||
|
"abox_aligned": 0,
|
||||||
|
"merge": 0
|
||||||
|
},
|
||||||
|
"total_changes": 0,
|
||||||
|
"triples_before": {
|
||||||
|
"ontology/pr_metamodel.ttl": 723,
|
||||||
|
"instances/sodh.ttl": 2308,
|
||||||
|
"shapes/pr_metamodel_shapes.ttl": 398
|
||||||
|
},
|
||||||
|
"triples_after": {
|
||||||
|
"ontology/pr_metamodel.ttl": 723,
|
||||||
|
"instances/sodh.ttl": 2308,
|
||||||
|
"shapes/pr_metamodel_shapes.ttl": 398
|
||||||
|
},
|
||||||
|
"notes": []
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
{
|
||||||
|
"attempt_timestamp": "2026-08-05T14:25:50",
|
||||||
|
"mode": "dry-run",
|
||||||
|
"from_version": "1.6",
|
||||||
|
"to_version": "2.0",
|
||||||
|
"input_checksums": {
|
||||||
|
"ontology/pr_metamodel.ttl": "552bbe0e9c0883d869ac5d6de40503bb",
|
||||||
|
"instances/sodh.ttl": "9eb1dbb96e325dbd61c6e880b43eaa44",
|
||||||
|
"shapes/pr_metamodel_shapes.ttl": "f6a1b4681e5817f8852c81424312e3d9"
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"deprecated_dropped": 130,
|
||||||
|
"rename_order_1": 244,
|
||||||
|
"rename_order_2": 256,
|
||||||
|
"rename_order_3": 14,
|
||||||
|
"abox_aligned": 356,
|
||||||
|
"queries_rewritten": 1,
|
||||||
|
"merge": 5,
|
||||||
|
"reclassified": 2,
|
||||||
|
"classes_created": 1,
|
||||||
|
"reparented": 1,
|
||||||
|
"to_literal_property": 2,
|
||||||
|
"to_literal_values": 6,
|
||||||
|
"to_literal_dropped": 18,
|
||||||
|
"isAbstract_declared": 31,
|
||||||
|
"display_removed": 22,
|
||||||
|
"display_set": 25,
|
||||||
|
"labels_regenerated": 76,
|
||||||
|
"version_bumped": 1,
|
||||||
|
"shapes_conformsTo": 1
|
||||||
|
},
|
||||||
|
"total_changes": 1192,
|
||||||
|
"triples_before": {
|
||||||
|
"ontology/pr_metamodel.ttl": 853,
|
||||||
|
"instances/sodh.ttl": 2308,
|
||||||
|
"shapes/pr_metamodel_shapes.ttl": 395
|
||||||
|
},
|
||||||
|
"triples_after": {
|
||||||
|
"ontology/pr_metamodel.ttl": 723,
|
||||||
|
"instances/sodh.ttl": 2308,
|
||||||
|
"shapes/pr_metamodel_shapes.ttl": 398
|
||||||
|
},
|
||||||
|
"notes": [
|
||||||
|
"reclassified: shortLabel",
|
||||||
|
"reclassified: acronym",
|
||||||
|
"classes_created: GovernanceLayerObject",
|
||||||
|
"reparented: Actor -> GovernanceLayerObject",
|
||||||
|
"to_literal_property: activationStatus",
|
||||||
|
"to_literal_property: environment",
|
||||||
|
"version_bumped: https://ontology.pernod-ricard.com/metamodel/2.0",
|
||||||
|
"shapes_conformsTo: https://ontology.pernod-ricard.com/metamodel/2.0"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
{
|
||||||
|
"attempt_timestamp": "2026-08-05T14:27:09",
|
||||||
|
"mode": "apply",
|
||||||
|
"from_version": "1.6",
|
||||||
|
"to_version": "2.0",
|
||||||
|
"input_checksums": {
|
||||||
|
"ontology/pr_metamodel.ttl": "552bbe0e9c0883d869ac5d6de40503bb",
|
||||||
|
"instances/sodh.ttl": "9eb1dbb96e325dbd61c6e880b43eaa44",
|
||||||
|
"shapes/pr_metamodel_shapes.ttl": "f6a1b4681e5817f8852c81424312e3d9"
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"deprecated_dropped": 130,
|
||||||
|
"rename_order_1": 244,
|
||||||
|
"rename_order_2": 256,
|
||||||
|
"rename_order_3": 14,
|
||||||
|
"abox_aligned": 356,
|
||||||
|
"queries_rewritten": 1,
|
||||||
|
"merge": 5,
|
||||||
|
"reclassified": 2,
|
||||||
|
"classes_created": 1,
|
||||||
|
"reparented": 1,
|
||||||
|
"to_literal_property": 2,
|
||||||
|
"to_literal_values": 6,
|
||||||
|
"to_literal_dropped": 18,
|
||||||
|
"isAbstract_declared": 31,
|
||||||
|
"display_removed": 22,
|
||||||
|
"display_set": 25,
|
||||||
|
"labels_regenerated": 76,
|
||||||
|
"version_bumped": 1,
|
||||||
|
"shapes_conformsTo": 1
|
||||||
|
},
|
||||||
|
"total_changes": 1192,
|
||||||
|
"triples_before": {
|
||||||
|
"ontology/pr_metamodel.ttl": 853,
|
||||||
|
"instances/sodh.ttl": 2308,
|
||||||
|
"shapes/pr_metamodel_shapes.ttl": 395
|
||||||
|
},
|
||||||
|
"triples_after": {
|
||||||
|
"ontology/pr_metamodel.ttl": 723,
|
||||||
|
"instances/sodh.ttl": 2308,
|
||||||
|
"shapes/pr_metamodel_shapes.ttl": 398
|
||||||
|
},
|
||||||
|
"notes": [
|
||||||
|
"reclassified: shortLabel",
|
||||||
|
"reclassified: acronym",
|
||||||
|
"classes_created: GovernanceLayerObject",
|
||||||
|
"reparented: Actor -> GovernanceLayerObject",
|
||||||
|
"to_literal_property: activationStatus",
|
||||||
|
"to_literal_property: environment",
|
||||||
|
"version_bumped: https://ontology.pernod-ricard.com/metamodel/2.0",
|
||||||
|
"shapes_conformsTo: https://ontology.pernod-ricard.com/metamodel/2.0"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"attempt_timestamp": "2026-08-05T14:27:20",
|
||||||
|
"mode": "dry-run",
|
||||||
|
"from_version": "1.6",
|
||||||
|
"to_version": "2.0",
|
||||||
|
"input_checksums": {
|
||||||
|
"ontology/pr_metamodel.ttl": "32929c506d402738807b8b30c04fbb48",
|
||||||
|
"instances/sodh.ttl": "6bef7bad65c091738cd58cf5f0f1a395",
|
||||||
|
"shapes/pr_metamodel_shapes.ttl": "d67b69756389a9864e306241d53114f1"
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"rename_order_1": 0,
|
||||||
|
"rename_order_2": 0,
|
||||||
|
"rename_order_3": 0,
|
||||||
|
"abox_aligned": 0,
|
||||||
|
"queries_rewritten": 0,
|
||||||
|
"merge": 0
|
||||||
|
},
|
||||||
|
"total_changes": 0,
|
||||||
|
"triples_before": {
|
||||||
|
"ontology/pr_metamodel.ttl": 723,
|
||||||
|
"instances/sodh.ttl": 2308,
|
||||||
|
"shapes/pr_metamodel_shapes.ttl": 398
|
||||||
|
},
|
||||||
|
"triples_after": {
|
||||||
|
"ontology/pr_metamodel.ttl": 723,
|
||||||
|
"instances/sodh.ttl": 2308,
|
||||||
|
"shapes/pr_metamodel_shapes.ttl": 398
|
||||||
|
},
|
||||||
|
"notes": []
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"attempt_timestamp": "2026-08-06T12:21:45",
|
||||||
|
"mode": "dry-run",
|
||||||
|
"from_version": "2.0",
|
||||||
|
"to_version": "2.1",
|
||||||
|
"input_checksums": {
|
||||||
|
"ontology/pr_metamodel.ttl": "32929c506d402738807b8b30c04fbb48"
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"annotations_declared": 1,
|
||||||
|
"polymorphic_declared": 18,
|
||||||
|
"domains_declared": 18,
|
||||||
|
"attached": 1,
|
||||||
|
"provenance_stripped": 5,
|
||||||
|
"provenance_declared": 29,
|
||||||
|
"provenance_no_axis": 1,
|
||||||
|
"version_bumped": 1
|
||||||
|
},
|
||||||
|
"total_changes": 74,
|
||||||
|
"triples_before": 723,
|
||||||
|
"triples_after": 786,
|
||||||
|
"notes": [
|
||||||
|
"annotations_declared: polymorphic",
|
||||||
|
"attached: GovernanceLayerObject -> DefinedObject",
|
||||||
|
"provenance_stripped: storedIn",
|
||||||
|
"provenance_stripped: primarilyStoredIn",
|
||||||
|
"provenance_stripped: DefinedObject",
|
||||||
|
"provenance_stripped: exposesMetric",
|
||||||
|
"provenance_stripped: CapturedObject",
|
||||||
|
"provenance_no_axis: SystemType",
|
||||||
|
"version_bumped: https://ontology.pernod-ricard.com/metamodel/2.1"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"attempt_timestamp": "2026-08-06T12:22:48",
|
||||||
|
"mode": "apply",
|
||||||
|
"from_version": "2.0",
|
||||||
|
"to_version": "2.1",
|
||||||
|
"input_checksums": {
|
||||||
|
"ontology/pr_metamodel.ttl": "32929c506d402738807b8b30c04fbb48"
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"annotations_declared": 1,
|
||||||
|
"polymorphic_declared": 18,
|
||||||
|
"domains_declared": 18,
|
||||||
|
"attached": 1,
|
||||||
|
"provenance_stripped": 5,
|
||||||
|
"provenance_declared": 29,
|
||||||
|
"provenance_no_axis": 1,
|
||||||
|
"version_bumped": 1
|
||||||
|
},
|
||||||
|
"total_changes": 74,
|
||||||
|
"triples_before": 723,
|
||||||
|
"triples_after": 786,
|
||||||
|
"notes": [
|
||||||
|
"annotations_declared: polymorphic",
|
||||||
|
"attached: GovernanceLayerObject -> DefinedObject",
|
||||||
|
"provenance_stripped: DefinedObject",
|
||||||
|
"provenance_stripped: CapturedObject",
|
||||||
|
"provenance_stripped: storedIn",
|
||||||
|
"provenance_stripped: exposesMetric",
|
||||||
|
"provenance_stripped: primarilyStoredIn",
|
||||||
|
"provenance_no_axis: SystemType",
|
||||||
|
"version_bumped: https://ontology.pernod-ricard.com/metamodel/2.1"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"attempt_timestamp": "2026-08-06T12:22:50",
|
||||||
|
"mode": "dry-run",
|
||||||
|
"from_version": "2.0",
|
||||||
|
"to_version": "2.1",
|
||||||
|
"input_checksums": {
|
||||||
|
"ontology/pr_metamodel.ttl": "04273379898d04b9952de68004de5768"
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"provenance_no_axis": 1
|
||||||
|
},
|
||||||
|
"total_changes": 1,
|
||||||
|
"triples_before": 786,
|
||||||
|
"triples_after": 786,
|
||||||
|
"notes": [
|
||||||
|
"provenance_no_axis: SystemType"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
{
|
||||||
|
"attempt_timestamp": "2026-08-06T14:34:17",
|
||||||
|
"mode": "dry-run",
|
||||||
|
"from_version": "2.0",
|
||||||
|
"to_version": "2.1",
|
||||||
|
"input_checksums": {
|
||||||
|
"ontology/pr_metamodel.ttl": "32929c506d402738807b8b30c04fbb48",
|
||||||
|
"instances/sodh.ttl": "6bef7bad65c091738cd58cf5f0f1a395"
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"annotations_declared": 1,
|
||||||
|
"properties_declared": 1,
|
||||||
|
"polymorphic_declared": 19,
|
||||||
|
"domains_declared": 17,
|
||||||
|
"attached": 1,
|
||||||
|
"provenance_stripped": 5,
|
||||||
|
"provenance_declared": 29,
|
||||||
|
"provenance_no_axis": 1,
|
||||||
|
"instances_removed": 4,
|
||||||
|
"instances_moved": 1,
|
||||||
|
"version_bumped": 1
|
||||||
|
},
|
||||||
|
"total_changes": 80,
|
||||||
|
"triples_before": {
|
||||||
|
"ontology/pr_metamodel.ttl": 723,
|
||||||
|
"instances/sodh.ttl": 2308
|
||||||
|
},
|
||||||
|
"triples_after": {
|
||||||
|
"ontology/pr_metamodel.ttl": 791,
|
||||||
|
"instances/sodh.ttl": 2304
|
||||||
|
},
|
||||||
|
"notes": [
|
||||||
|
"annotations_declared: polymorphic",
|
||||||
|
"properties_declared: serviceCommitment",
|
||||||
|
"attached: GovernanceLayerObject -> DefinedObject",
|
||||||
|
"provenance_stripped: CapturedObject",
|
||||||
|
"provenance_stripped: storedIn",
|
||||||
|
"provenance_stripped: primarilyStoredIn",
|
||||||
|
"provenance_stripped: DefinedObject",
|
||||||
|
"provenance_stripped: exposesMetric",
|
||||||
|
"provenance_no_axis: SystemType",
|
||||||
|
"instances_removed: DI_06_01_001 businessDefinition",
|
||||||
|
"instances_removed: DI_06_01_002 businessDefinition",
|
||||||
|
"instances_removed: DI_06_01_001 sourcedFrom",
|
||||||
|
"instances_removed: DI_06_01_002 sourcedFrom",
|
||||||
|
"instances_moved: DC_06_01_001 businessRule",
|
||||||
|
"version_bumped: https://ontology.pernod-ricard.com/metamodel/2.1"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
{
|
||||||
|
"attempt_timestamp": "2026-08-06T14:35:03",
|
||||||
|
"mode": "apply",
|
||||||
|
"from_version": "2.0",
|
||||||
|
"to_version": "2.1",
|
||||||
|
"input_checksums": {
|
||||||
|
"ontology/pr_metamodel.ttl": "32929c506d402738807b8b30c04fbb48",
|
||||||
|
"instances/sodh.ttl": "6bef7bad65c091738cd58cf5f0f1a395"
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"annotations_declared": 1,
|
||||||
|
"properties_declared": 1,
|
||||||
|
"polymorphic_declared": 19,
|
||||||
|
"domains_declared": 17,
|
||||||
|
"attached": 1,
|
||||||
|
"provenance_stripped": 5,
|
||||||
|
"provenance_declared": 29,
|
||||||
|
"provenance_no_axis": 1,
|
||||||
|
"instances_removed": 4,
|
||||||
|
"instances_moved": 1,
|
||||||
|
"version_bumped": 1
|
||||||
|
},
|
||||||
|
"total_changes": 80,
|
||||||
|
"triples_before": {
|
||||||
|
"ontology/pr_metamodel.ttl": 723,
|
||||||
|
"instances/sodh.ttl": 2308
|
||||||
|
},
|
||||||
|
"triples_after": {
|
||||||
|
"ontology/pr_metamodel.ttl": 791,
|
||||||
|
"instances/sodh.ttl": 2304
|
||||||
|
},
|
||||||
|
"notes": [
|
||||||
|
"annotations_declared: polymorphic",
|
||||||
|
"properties_declared: serviceCommitment",
|
||||||
|
"attached: GovernanceLayerObject -> DefinedObject",
|
||||||
|
"provenance_stripped: storedIn",
|
||||||
|
"provenance_stripped: primarilyStoredIn",
|
||||||
|
"provenance_stripped: DefinedObject",
|
||||||
|
"provenance_stripped: CapturedObject",
|
||||||
|
"provenance_stripped: exposesMetric",
|
||||||
|
"provenance_no_axis: SystemType",
|
||||||
|
"instances_removed: DI_06_01_001 businessDefinition",
|
||||||
|
"instances_removed: DI_06_01_002 businessDefinition",
|
||||||
|
"instances_removed: DI_06_01_001 sourcedFrom",
|
||||||
|
"instances_removed: DI_06_01_002 sourcedFrom",
|
||||||
|
"instances_moved: DC_06_01_001 businessRule",
|
||||||
|
"version_bumped: https://ontology.pernod-ricard.com/metamodel/2.1"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"attempt_timestamp": "2026-08-06T14:35:07",
|
||||||
|
"mode": "dry-run",
|
||||||
|
"from_version": "2.0",
|
||||||
|
"to_version": "2.1",
|
||||||
|
"input_checksums": {
|
||||||
|
"ontology/pr_metamodel.ttl": "d264f924c61faa2fe8caf7be8ec7490c",
|
||||||
|
"instances/sodh.ttl": "135278dd413486cb1273fe193a5aa590"
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"provenance_no_axis": 1
|
||||||
|
},
|
||||||
|
"total_changes": 1,
|
||||||
|
"triples_before": {
|
||||||
|
"ontology/pr_metamodel.ttl": 791,
|
||||||
|
"instances/sodh.ttl": 2304
|
||||||
|
},
|
||||||
|
"triples_after": {
|
||||||
|
"ontology/pr_metamodel.ttl": 791,
|
||||||
|
"instances/sodh.ttl": 2304
|
||||||
|
},
|
||||||
|
"notes": [
|
||||||
|
"provenance_no_axis: SystemType"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
{
|
||||||
|
"attempt_timestamp": "2026-08-07T10:27:29",
|
||||||
|
"mode": "apply",
|
||||||
|
"from_version": "2.0",
|
||||||
|
"to_version": "2.1",
|
||||||
|
"input_checksums": {
|
||||||
|
"ontology/pr_metamodel.ttl": "32929c506d402738807b8b30c04fbb48",
|
||||||
|
"instances/sodh.ttl": "6bef7bad65c091738cd58cf5f0f1a395"
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"annotations_declared": 1,
|
||||||
|
"properties_declared": 1,
|
||||||
|
"polymorphic_declared": 19,
|
||||||
|
"domains_declared": 17,
|
||||||
|
"attached": 1,
|
||||||
|
"provenance_stripped": 5,
|
||||||
|
"provenance_declared": 29,
|
||||||
|
"provenance_no_axis": 1,
|
||||||
|
"instances_removed": 4,
|
||||||
|
"instances_moved": 1,
|
||||||
|
"version_bumped": 1
|
||||||
|
},
|
||||||
|
"total_changes": 80,
|
||||||
|
"triples_before": {
|
||||||
|
"ontology/pr_metamodel.ttl": 723,
|
||||||
|
"instances/sodh.ttl": 2308
|
||||||
|
},
|
||||||
|
"triples_after": {
|
||||||
|
"ontology/pr_metamodel.ttl": 791,
|
||||||
|
"instances/sodh.ttl": 2304
|
||||||
|
},
|
||||||
|
"notes": [
|
||||||
|
"annotations_declared: polymorphic",
|
||||||
|
"properties_declared: serviceCommitment",
|
||||||
|
"attached: GovernanceLayerObject -> DefinedObject",
|
||||||
|
"provenance_stripped: storedIn",
|
||||||
|
"provenance_stripped: exposesMetric",
|
||||||
|
"provenance_stripped: DefinedObject",
|
||||||
|
"provenance_stripped: CapturedObject",
|
||||||
|
"provenance_stripped: primarilyStoredIn",
|
||||||
|
"provenance_no_axis: SystemType",
|
||||||
|
"instances_removed: DI_06_01_001 businessDefinition",
|
||||||
|
"instances_removed: DI_06_01_002 businessDefinition",
|
||||||
|
"instances_removed: DI_06_01_001 sourcedFrom",
|
||||||
|
"instances_removed: DI_06_01_002 sourcedFrom",
|
||||||
|
"instances_moved: DC_06_01_001 businessRule",
|
||||||
|
"version_bumped: https://ontology.pernod-ricard.com/metamodel/2.1"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"attempt_timestamp": "2026-08-06T15:17:04",
|
||||||
|
"mode": "dry-run",
|
||||||
|
"from_version": "2.1",
|
||||||
|
"to_version": "2.2",
|
||||||
|
"input_checksums": {
|
||||||
|
"ontology/pr_metamodel.ttl": "d264f924c61faa2fe8caf7be8ec7490c",
|
||||||
|
"instances/sodh.ttl": "135278dd413486cb1273fe193a5aa590"
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"properties_declared": 1,
|
||||||
|
"display_updated": 1,
|
||||||
|
"instances_renamed": 80,
|
||||||
|
"assignments_completed": 12,
|
||||||
|
"sub_domain_owners_created": 10,
|
||||||
|
"product_owners_created": 1,
|
||||||
|
"stewards_created": 15,
|
||||||
|
"steward_still_referenced": 6,
|
||||||
|
"version_bumped": 1
|
||||||
|
},
|
||||||
|
"total_changes": 127,
|
||||||
|
"triples_before": {
|
||||||
|
"ontology/pr_metamodel.ttl": 791,
|
||||||
|
"instances/sodh.ttl": 2304
|
||||||
|
},
|
||||||
|
"triples_after": {
|
||||||
|
"ontology/pr_metamodel.ttl": 796,
|
||||||
|
"instances/sodh.ttl": 2557
|
||||||
|
},
|
||||||
|
"notes": [
|
||||||
|
"properties_declared: heldBy",
|
||||||
|
"display_updated: DataSteward -> DST",
|
||||||
|
"steward_still_referenced: DST_DD_04 cited by 8 subject(s)",
|
||||||
|
"steward_still_referenced: DST_DD_05 cited by 3 subject(s)",
|
||||||
|
"steward_still_referenced: DST_DD_06 cited by 23 subject(s)",
|
||||||
|
"steward_still_referenced: DST_DD_10 cited by 4 subject(s)",
|
||||||
|
"steward_still_referenced: DST_DD_16 cited by 11 subject(s)",
|
||||||
|
"steward_still_referenced: DST_DD_21 cited by 4 subject(s)",
|
||||||
|
"version_bumped: https://ontology.pernod-ricard.com/metamodel/2.2"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"attempt_timestamp": "2026-08-06T15:23:27",
|
||||||
|
"mode": "dry-run",
|
||||||
|
"from_version": "2.1",
|
||||||
|
"to_version": "2.2",
|
||||||
|
"input_checksums": {
|
||||||
|
"ontology/pr_metamodel.ttl": "d264f924c61faa2fe8caf7be8ec7490c",
|
||||||
|
"instances/sodh.ttl": "135278dd413486cb1273fe193a5aa590"
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"properties_declared": 1,
|
||||||
|
"display_updated": 1,
|
||||||
|
"instances_renamed": 80,
|
||||||
|
"assignments_completed": 12,
|
||||||
|
"sub_domain_owners_created": 10,
|
||||||
|
"product_owners_created": 1,
|
||||||
|
"stewards_created": 15,
|
||||||
|
"ownedby_removed": 15,
|
||||||
|
"ownedby_repointed_to_sdo": 7,
|
||||||
|
"ownedby_repointed_to_steward": 30,
|
||||||
|
"ownedby_unresolved": 1,
|
||||||
|
"domain_stewards_retired": 5,
|
||||||
|
"steward_still_referenced": 1,
|
||||||
|
"version_bumped": 1
|
||||||
|
},
|
||||||
|
"total_changes": 180,
|
||||||
|
"triples_before": {
|
||||||
|
"ontology/pr_metamodel.ttl": 791,
|
||||||
|
"instances/sodh.ttl": 2304
|
||||||
|
},
|
||||||
|
"triples_after": {
|
||||||
|
"ontology/pr_metamodel.ttl": 796,
|
||||||
|
"instances/sodh.ttl": 2532
|
||||||
|
},
|
||||||
|
"notes": [
|
||||||
|
"properties_declared: heldBy",
|
||||||
|
"display_updated: DataSteward -> DST",
|
||||||
|
"ownedby_unresolved: BC_06_01_003 reaches no business object",
|
||||||
|
"domain_stewards_retired: DST_DD_04",
|
||||||
|
"domain_stewards_retired: DST_DD_05",
|
||||||
|
"steward_still_referenced: DST_DD_06 cited by 1 subject(s)",
|
||||||
|
"domain_stewards_retired: DST_DD_10",
|
||||||
|
"domain_stewards_retired: DST_DD_16",
|
||||||
|
"domain_stewards_retired: DST_DD_21",
|
||||||
|
"version_bumped: https://ontology.pernod-ricard.com/metamodel/2.2"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
{
|
||||||
|
"attempt_timestamp": "2026-08-06T19:49:37",
|
||||||
|
"mode": "dry-run",
|
||||||
|
"from_version": "2.1",
|
||||||
|
"to_version": "2.2",
|
||||||
|
"input_checksums": {
|
||||||
|
"ontology/pr_metamodel.ttl": "d264f924c61faa2fe8caf7be8ec7490c",
|
||||||
|
"instances/sodh.ttl": "135278dd413486cb1273fe193a5aa590"
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"properties_declared": 1,
|
||||||
|
"display_updated": 1,
|
||||||
|
"instances_renamed": 80,
|
||||||
|
"assignments_completed": 12,
|
||||||
|
"sub_domain_owners_created": 10,
|
||||||
|
"product_owners_created": 1,
|
||||||
|
"business_objects_created": 1,
|
||||||
|
"stewards_created": 16,
|
||||||
|
"ownedby_repointed_to_steward": 31,
|
||||||
|
"ownedby_removed": 15,
|
||||||
|
"ownedby_repointed_to_sdo": 7,
|
||||||
|
"domain_stewards_retired": 6,
|
||||||
|
"version_bumped": 1
|
||||||
|
},
|
||||||
|
"total_changes": 182,
|
||||||
|
"triples_before": {
|
||||||
|
"ontology/pr_metamodel.ttl": 791,
|
||||||
|
"instances/sodh.ttl": 2304
|
||||||
|
},
|
||||||
|
"triples_after": {
|
||||||
|
"ontology/pr_metamodel.ttl": 796,
|
||||||
|
"instances/sodh.ttl": 2546
|
||||||
|
},
|
||||||
|
"notes": [
|
||||||
|
"properties_declared: heldBy",
|
||||||
|
"display_updated: DataSteward -> DST",
|
||||||
|
"business_objects_created: BO_06_01_006",
|
||||||
|
"domain_stewards_retired: DST_DD_04",
|
||||||
|
"domain_stewards_retired: DST_DD_05",
|
||||||
|
"domain_stewards_retired: DST_DD_06",
|
||||||
|
"domain_stewards_retired: DST_DD_10",
|
||||||
|
"domain_stewards_retired: DST_DD_16",
|
||||||
|
"domain_stewards_retired: DST_DD_21",
|
||||||
|
"version_bumped: https://ontology.pernod-ricard.com/metamodel/2.2"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
{
|
||||||
|
"attempt_timestamp": "2026-08-06T19:51:46",
|
||||||
|
"mode": "apply",
|
||||||
|
"from_version": "2.1",
|
||||||
|
"to_version": "2.2",
|
||||||
|
"input_checksums": {
|
||||||
|
"ontology/pr_metamodel.ttl": "d264f924c61faa2fe8caf7be8ec7490c",
|
||||||
|
"instances/sodh.ttl": "135278dd413486cb1273fe193a5aa590"
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"properties_declared": 1,
|
||||||
|
"display_updated": 1,
|
||||||
|
"instances_renamed": 80,
|
||||||
|
"assignments_completed": 12,
|
||||||
|
"sub_domain_owners_created": 10,
|
||||||
|
"product_owners_created": 1,
|
||||||
|
"business_objects_created": 1,
|
||||||
|
"stewards_created": 16,
|
||||||
|
"ownedby_repointed_to_steward": 31,
|
||||||
|
"ownedby_removed": 15,
|
||||||
|
"ownedby_repointed_to_sdo": 7,
|
||||||
|
"domain_stewards_retired": 6,
|
||||||
|
"version_bumped": 1
|
||||||
|
},
|
||||||
|
"total_changes": 182,
|
||||||
|
"triples_before": {
|
||||||
|
"ontology/pr_metamodel.ttl": 791,
|
||||||
|
"instances/sodh.ttl": 2304
|
||||||
|
},
|
||||||
|
"triples_after": {
|
||||||
|
"ontology/pr_metamodel.ttl": 796,
|
||||||
|
"instances/sodh.ttl": 2546
|
||||||
|
},
|
||||||
|
"notes": [
|
||||||
|
"properties_declared: heldBy",
|
||||||
|
"display_updated: DataSteward -> DST",
|
||||||
|
"business_objects_created: BO_06_01_006",
|
||||||
|
"domain_stewards_retired: DST_DD_04",
|
||||||
|
"domain_stewards_retired: DST_DD_05",
|
||||||
|
"domain_stewards_retired: DST_DD_06",
|
||||||
|
"domain_stewards_retired: DST_DD_10",
|
||||||
|
"domain_stewards_retired: DST_DD_16",
|
||||||
|
"domain_stewards_retired: DST_DD_21",
|
||||||
|
"version_bumped: https://ontology.pernod-ricard.com/metamodel/2.2"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"attempt_timestamp": "2026-08-06T19:51:51",
|
||||||
|
"mode": "dry-run",
|
||||||
|
"from_version": "2.1",
|
||||||
|
"to_version": "2.2",
|
||||||
|
"input_checksums": {
|
||||||
|
"ontology/pr_metamodel.ttl": "e705cde06956d746631e412719b6cc6d",
|
||||||
|
"instances/sodh.ttl": "82ced2220c1e0525c1dcd5028afbc546"
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"instances_renamed": 0
|
||||||
|
},
|
||||||
|
"total_changes": 0,
|
||||||
|
"triples_before": {
|
||||||
|
"ontology/pr_metamodel.ttl": 796,
|
||||||
|
"instances/sodh.ttl": 2546
|
||||||
|
},
|
||||||
|
"triples_after": {
|
||||||
|
"ontology/pr_metamodel.ttl": 796,
|
||||||
|
"instances/sodh.ttl": 2546
|
||||||
|
},
|
||||||
|
"notes": []
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
{
|
||||||
|
"attempt_timestamp": "2026-08-07T10:21:51",
|
||||||
|
"mode": "apply",
|
||||||
|
"from_version": "2.1",
|
||||||
|
"to_version": "2.2",
|
||||||
|
"input_checksums": {
|
||||||
|
"ontology/pr_metamodel.ttl": "32929c506d402738807b8b30c04fbb48",
|
||||||
|
"instances/sodh.ttl": "6bef7bad65c091738cd58cf5f0f1a395"
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"properties_declared": 1,
|
||||||
|
"display_updated": 1,
|
||||||
|
"instances_renamed": 80,
|
||||||
|
"assignments_completed": 12,
|
||||||
|
"sub_domain_owners_created": 10,
|
||||||
|
"product_owners_created": 1,
|
||||||
|
"business_objects_created": 1,
|
||||||
|
"stewards_created": 16,
|
||||||
|
"ownedby_repointed_to_steward": 31,
|
||||||
|
"ownedby_removed": 15,
|
||||||
|
"ownedby_repointed_to_sdo": 7,
|
||||||
|
"domain_stewards_retired": 6,
|
||||||
|
"version_bumped": 1
|
||||||
|
},
|
||||||
|
"total_changes": 182,
|
||||||
|
"triples_before": {
|
||||||
|
"ontology/pr_metamodel.ttl": 723,
|
||||||
|
"instances/sodh.ttl": 2308
|
||||||
|
},
|
||||||
|
"triples_after": {
|
||||||
|
"ontology/pr_metamodel.ttl": 729,
|
||||||
|
"instances/sodh.ttl": 2550
|
||||||
|
},
|
||||||
|
"notes": [
|
||||||
|
"properties_declared: personName",
|
||||||
|
"display_updated: DataSteward -> DST",
|
||||||
|
"business_objects_created: BO_06_01_006",
|
||||||
|
"domain_stewards_retired: DST_DD_04",
|
||||||
|
"domain_stewards_retired: DST_DD_05",
|
||||||
|
"domain_stewards_retired: DST_DD_06",
|
||||||
|
"domain_stewards_retired: DST_DD_10",
|
||||||
|
"domain_stewards_retired: DST_DD_16",
|
||||||
|
"domain_stewards_retired: DST_DD_21",
|
||||||
|
"version_bumped: https://ontology.pernod-ricard.com/metamodel/2.2"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"attempt_timestamp": "2026-08-07T10:21:59",
|
||||||
|
"mode": "dry-run",
|
||||||
|
"from_version": "2.1",
|
||||||
|
"to_version": "2.2",
|
||||||
|
"input_checksums": {
|
||||||
|
"ontology/pr_metamodel.ttl": "6f2deb192272d0bceecac31fcb525ad1",
|
||||||
|
"instances/sodh.ttl": "7c0d42f33590d2208b0af7b2068f559a"
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"instances_renamed": 0
|
||||||
|
},
|
||||||
|
"total_changes": 0,
|
||||||
|
"triples_before": {
|
||||||
|
"ontology/pr_metamodel.ttl": 729,
|
||||||
|
"instances/sodh.ttl": 2550
|
||||||
|
},
|
||||||
|
"triples_after": {
|
||||||
|
"ontology/pr_metamodel.ttl": 729,
|
||||||
|
"instances/sodh.ttl": 2550
|
||||||
|
},
|
||||||
|
"notes": []
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
{
|
||||||
|
"attempt_timestamp": "2026-08-07T10:27:33",
|
||||||
|
"mode": "apply",
|
||||||
|
"from_version": "2.1",
|
||||||
|
"to_version": "2.2",
|
||||||
|
"input_checksums": {
|
||||||
|
"ontology/pr_metamodel.ttl": "96687771a06f85759e7b894f9d248083",
|
||||||
|
"instances/sodh.ttl": "135278dd413486cb1273fe193a5aa590"
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"properties_declared": 1,
|
||||||
|
"display_updated": 1,
|
||||||
|
"instances_renamed": 80,
|
||||||
|
"assignments_completed": 12,
|
||||||
|
"sub_domain_owners_created": 10,
|
||||||
|
"product_owners_created": 1,
|
||||||
|
"business_objects_created": 1,
|
||||||
|
"stewards_created": 16,
|
||||||
|
"ownedby_repointed_to_steward": 31,
|
||||||
|
"ownedby_removed": 15,
|
||||||
|
"ownedby_repointed_to_sdo": 7,
|
||||||
|
"domain_stewards_retired": 6,
|
||||||
|
"version_bumped": 1
|
||||||
|
},
|
||||||
|
"total_changes": 182,
|
||||||
|
"triples_before": {
|
||||||
|
"ontology/pr_metamodel.ttl": 791,
|
||||||
|
"instances/sodh.ttl": 2304
|
||||||
|
},
|
||||||
|
"triples_after": {
|
||||||
|
"ontology/pr_metamodel.ttl": 797,
|
||||||
|
"instances/sodh.ttl": 2546
|
||||||
|
},
|
||||||
|
"notes": [
|
||||||
|
"properties_declared: personName",
|
||||||
|
"display_updated: DataSteward -> DST",
|
||||||
|
"business_objects_created: BO_06_01_006",
|
||||||
|
"domain_stewards_retired: DST_DD_04",
|
||||||
|
"domain_stewards_retired: DST_DD_05",
|
||||||
|
"domain_stewards_retired: DST_DD_06",
|
||||||
|
"domain_stewards_retired: DST_DD_10",
|
||||||
|
"domain_stewards_retired: DST_DD_16",
|
||||||
|
"domain_stewards_retired: DST_DD_21",
|
||||||
|
"version_bumped: https://ontology.pernod-ricard.com/metamodel/2.2"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -31,6 +31,7 @@ from collections import OrderedDict
|
|||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
from rdflib import Graph, Literal, Namespace, RDF, RDFS, OWL, URIRef, XSD
|
from rdflib import Graph, Literal, Namespace, RDF, RDFS, OWL, URIRef, XSD
|
||||||
|
from rdflib.namespace import SH
|
||||||
|
|
||||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||||
SPEC = os.path.join(HERE, "renames.yaml")
|
SPEC = os.path.join(HERE, "renames.yaml")
|
||||||
@@ -133,7 +134,13 @@ def drop_subject(graph, subject, report, step):
|
|||||||
|
|
||||||
|
|
||||||
def count_instances(graphs, term):
|
def count_instances(graphs, term):
|
||||||
"""EV-002. The proof required before any permanent withdrawal."""
|
"""EV-002. The proof required before any permanent withdrawal.
|
||||||
|
|
||||||
|
Counted on INSTANCE graphs only. A mention inside a shapes graph is not an
|
||||||
|
instantiation: a shape carrying sh:maxCount 0 on a term is a guard forbidding
|
||||||
|
its reappearance, which is the opposite of a use, and it keeps working after
|
||||||
|
the term is gone since it checks an identifier rather than a declaration.
|
||||||
|
"""
|
||||||
n = 0
|
n = 0
|
||||||
for g in graphs:
|
for g in graphs:
|
||||||
n += len(list(g.triples((None, RDF.type, term))))
|
n += len(list(g.triples((None, RDF.type, term))))
|
||||||
@@ -142,34 +149,84 @@ def count_instances(graphs, term):
|
|||||||
return n
|
return n
|
||||||
|
|
||||||
|
|
||||||
def migrate(onto, others, spec, rules, report):
|
def rewrite_queries(graph, renames, report):
|
||||||
|
"""Rename inside embedded SPARQL queries.
|
||||||
|
|
||||||
|
A sh:select or sh:ask body is a literal, so no triple-level rename reaches
|
||||||
|
it: the query keeps naming a term that no longer exists and the constraint
|
||||||
|
silently stops matching. This is not the line-by-line editing EV-005
|
||||||
|
forbids: the graph is parsed, the literal is retrieved as a value, and what
|
||||||
|
is rewritten is a structured query language where a prefixed name has clear
|
||||||
|
token boundaries.
|
||||||
|
"""
|
||||||
|
n = 0
|
||||||
|
for prop in (SH.select, SH.ask, SH.construct):
|
||||||
|
for s, p, o in list(graph.triples((None, prop, None))):
|
||||||
|
text = str(o)
|
||||||
|
new = text
|
||||||
|
for old, target in renames:
|
||||||
|
new = re.sub(r"\bpr:%s\b" % re.escape(old), "pr:%s" % target, new)
|
||||||
|
if new != text:
|
||||||
|
graph.remove((s, p, o))
|
||||||
|
graph.add((s, p, Literal(new)))
|
||||||
|
n += 1
|
||||||
|
report.add("queries_rewritten", n)
|
||||||
|
return n
|
||||||
|
|
||||||
|
|
||||||
|
def migrate(onto, instances, shapes, spec, rules, report):
|
||||||
ns = spec["meta"]["namespace"]
|
ns = spec["meta"]["namespace"]
|
||||||
pr = Namespace(ns)
|
pr = Namespace(ns)
|
||||||
|
others = instances + shapes
|
||||||
all_graphs = [onto] + others
|
all_graphs = [onto] + others
|
||||||
|
|
||||||
# 1 — deprecated terms are withdrawn outright (phase clause), proof first
|
# 1 — deprecated terms are withdrawn outright (phase clause), proof first.
|
||||||
|
# The proof is taken on instance graphs only: a mention in a shapes graph
|
||||||
|
# is a guard against reappearance, not a use.
|
||||||
|
# Reference cleaning likewise stops at the ontology and the instances.
|
||||||
|
# EV-003 guards against phantom nodes rebuilt by inference in the model
|
||||||
|
# graph; a mention inside a shape is a constraint on an identifier, in a
|
||||||
|
# separate graph, taking part in no inference over the ontology. Removing
|
||||||
|
# it would strip the sh:path from a guard and leave a property shape
|
||||||
|
# carrying a maxCount and no path — an invalid shape, and the loss of the
|
||||||
|
# very rule that forbids the term from coming back.
|
||||||
if spec["structural"].get("drop_deprecated"):
|
if spec["structural"].get("drop_deprecated"):
|
||||||
for subj in list(onto.subjects(OWL.deprecated, Literal(True))):
|
for subj in list(onto.subjects(OWL.deprecated, Literal(True))):
|
||||||
name = local(subj, ns) or str(subj)
|
name = local(subj, ns) or str(subj)
|
||||||
used = sum(count_instances([g], subj) for g in others)
|
used = count_instances(instances, subj)
|
||||||
if used:
|
if used:
|
||||||
report.add("deprecated_kept", 1, "%s still used %d times" % (name, used))
|
report.add("deprecated_kept", 1, "%s instantiated %d times" % (name, used))
|
||||||
continue
|
continue
|
||||||
for g in all_graphs:
|
for g in [onto] + instances:
|
||||||
drop_subject(g, subj, report, "deprecated_dropped")
|
drop_subject(g, subj, report, "deprecated_dropped")
|
||||||
|
|
||||||
# 2 — renames, in dependency order
|
# 2 — vocabulary renames, in dependency order
|
||||||
for order in (1, 2, 3):
|
for order in (1, 2, 3):
|
||||||
for r in [x for x in spec["renames"] if x["order"] == order]:
|
for r in [x for x in spec["renames"] if x["order"] == order]:
|
||||||
for g in all_graphs:
|
for g in all_graphs:
|
||||||
rename(g, pr, r["from"], r["to"], report, "rename_order_%d" % order)
|
rename(g, pr, r["from"], r["to"], report, "rename_order_%d" % order)
|
||||||
|
|
||||||
# 3 — merges
|
# 3 — instance-side alignment: forms the instances use that the vocabulary
|
||||||
|
# never declared and that the TN-012 renames do not converge on.
|
||||||
|
# Applied to the shapes too: they target these forms in sh:path.
|
||||||
|
for r in spec.get("abox_renames") or []:
|
||||||
|
for g in all_graphs:
|
||||||
|
rename(g, pr, r["from"], r["to"], report, "abox_aligned")
|
||||||
|
|
||||||
|
# 4 — embedded SPARQL queries carry term names as text, invisible to a
|
||||||
|
# triple-level rename. Rewritten with the same map, after it.
|
||||||
|
pairs = [(r["from"], r["to"]) for r in spec["renames"]]
|
||||||
|
pairs += [(r["from"], r["to"]) for r in (spec.get("abox_renames") or [])]
|
||||||
|
pairs += [(m["from"], m["into"]) for m in (spec.get("merges") or [])]
|
||||||
|
for g in all_graphs:
|
||||||
|
rewrite_queries(g, pairs, report)
|
||||||
|
|
||||||
|
# 5 — merges
|
||||||
for m in spec.get("merges") or []:
|
for m in spec.get("merges") or []:
|
||||||
for g in all_graphs:
|
for g in all_graphs:
|
||||||
merge(g, pr, m["from"], m["into"], report)
|
merge(g, pr, m["from"], m["into"], report)
|
||||||
|
|
||||||
# 4 — reclassify display properties (TN-003)
|
# 6 — reclassify display properties (TN-003)
|
||||||
for rc in spec["structural"]["reclassify"]:
|
for rc in spec["structural"]["reclassify"]:
|
||||||
term = pr[rc["term"]]
|
term = pr[rc["term"]]
|
||||||
if (term, RDF.type, OWL.AnnotationProperty) not in onto:
|
if (term, RDF.type, OWL.AnnotationProperty) not in onto:
|
||||||
@@ -177,7 +234,7 @@ def migrate(onto, others, spec, rules, report):
|
|||||||
onto.add((term, RDF.type, OWL.AnnotationProperty))
|
onto.add((term, RDF.type, OWL.AnnotationProperty))
|
||||||
report.add("reclassified", 1, rc["term"])
|
report.add("reclassified", 1, rc["term"])
|
||||||
|
|
||||||
# 5 — create the governance layer root (TN-027)
|
# 7 — create the governance layer root (TN-027)
|
||||||
for c in spec["structural"].get("create_classes") or []:
|
for c in spec["structural"].get("create_classes") or []:
|
||||||
term = pr[c["term"]]
|
term = pr[c["term"]]
|
||||||
if (term, RDF.type, OWL.Class) not in onto:
|
if (term, RDF.type, OWL.Class) not in onto:
|
||||||
@@ -188,21 +245,21 @@ def migrate(onto, others, spec, rules, report):
|
|||||||
onto.add((term, pr.isAbstract, Literal(True)))
|
onto.add((term, pr.isAbstract, Literal(True)))
|
||||||
report.add("classes_created", 1, c["term"])
|
report.add("classes_created", 1, c["term"])
|
||||||
|
|
||||||
# 6 — reparent (TN-027)
|
# 8 — reparent (TN-027)
|
||||||
for rp in spec["structural"].get("reparent") or []:
|
for rp in spec["structural"].get("reparent") or []:
|
||||||
term, parent = pr[rp["term"]], pr[rp["parent"]]
|
term, parent = pr[rp["term"]], pr[rp["parent"]]
|
||||||
if (term, RDFS.subClassOf, parent) not in onto:
|
if (term, RDFS.subClassOf, parent) not in onto:
|
||||||
onto.add((term, RDFS.subClassOf, parent))
|
onto.add((term, RDFS.subClassOf, parent))
|
||||||
report.add("reparented", 1, "%s -> %s" % (rp["term"], rp["parent"]))
|
report.add("reparented", 1, "%s -> %s" % (rp["term"], rp["parent"]))
|
||||||
|
|
||||||
# 7 — sub-properties (TN-028)
|
# 9 — sub-properties (TN-028)
|
||||||
for sp in spec["structural"].get("subproperties") or []:
|
for sp in spec["structural"].get("subproperties") or []:
|
||||||
term, parent = pr[sp["term"]], pr[sp["parent"]]
|
term, parent = pr[sp["term"]], pr[sp["parent"]]
|
||||||
if (term, RDFS.subPropertyOf, parent) not in onto:
|
if (term, RDFS.subPropertyOf, parent) not in onto:
|
||||||
onto.add((term, RDFS.subPropertyOf, parent))
|
onto.add((term, RDFS.subPropertyOf, parent))
|
||||||
report.add("subproperties", 1, "%s -> %s" % (sp["term"], sp["parent"]))
|
report.add("subproperties", 1, "%s -> %s" % (sp["term"], sp["parent"]))
|
||||||
|
|
||||||
# 8 — controlled values become literals (TN-016, TN-017)
|
# 10 — controlled values become literals (TN-016, TN-017)
|
||||||
for conv in spec["structural"].get("to_literal") or []:
|
for conv in spec["structural"].get("to_literal") or []:
|
||||||
prop = pr[conv["property"]]
|
prop = pr[conv["property"]]
|
||||||
if (prop, RDF.type, OWL.DatatypeProperty) not in onto:
|
if (prop, RDF.type, OWL.DatatypeProperty) not in onto:
|
||||||
@@ -222,10 +279,10 @@ def migrate(onto, others, spec, rules, report):
|
|||||||
for ind in conv["drop_individuals"] + [conv["drop_class"]]:
|
for ind in conv["drop_individuals"] + [conv["drop_class"]]:
|
||||||
subj = pr[ind]
|
subj = pr[ind]
|
||||||
if (subj, None, None) in onto:
|
if (subj, None, None) in onto:
|
||||||
for g in all_graphs:
|
for g in [onto] + instances: # shapes keep their guards
|
||||||
drop_subject(g, subj, report, "to_literal_dropped")
|
drop_subject(g, subj, report, "to_literal_dropped")
|
||||||
|
|
||||||
# 9 — abstractness, from the rulebook annex (TN-006, TN-007)
|
# 11 — abstractness, from the rulebook annex (TN-006, TN-007)
|
||||||
for a in rules["abstractness"]:
|
for a in rules["abstractness"]:
|
||||||
term = pr[a["term"]]
|
term = pr[a["term"]]
|
||||||
if (term, RDF.type, OWL.Class) not in onto:
|
if (term, RDF.type, OWL.Class) not in onto:
|
||||||
@@ -237,19 +294,25 @@ def migrate(onto, others, spec, rules, report):
|
|||||||
onto.add((term, pr.isAbstract, want))
|
onto.add((term, pr.isAbstract, want))
|
||||||
report.add("isAbstract_declared", 1)
|
report.add("isAbstract_declared", 1)
|
||||||
|
|
||||||
# 10 — display annotations, from the rulebook annex (TN-022)
|
# 12 — display annotations, from the rulebook annex (TN-022)
|
||||||
|
# The annex is authoritative AND exhaustive: a null value means any existing
|
||||||
|
# annotation is removed. A short label identical to the label carries no
|
||||||
|
# information and is one more thing to keep in step.
|
||||||
for d in rules["display"]:
|
for d in rules["display"]:
|
||||||
term = pr[d["iri"]]
|
term = pr[d["iri"]]
|
||||||
for prop, value in ((pr.shortLabel, d.get("short_label")),
|
for prop, value in ((pr.shortLabel, d.get("short_label")),
|
||||||
(pr.acronym, d.get("acronym"))):
|
(pr.acronym, d.get("acronym"))):
|
||||||
if not value:
|
current = list(onto.objects(term, prop))
|
||||||
continue
|
if value:
|
||||||
if (term, prop, Literal(value)) not in onto:
|
if (term, prop, Literal(value)) not in onto:
|
||||||
onto.remove((term, prop, None))
|
onto.remove((term, prop, None))
|
||||||
onto.add((term, prop, Literal(value)))
|
onto.add((term, prop, Literal(value)))
|
||||||
report.add("display_set", 1)
|
report.add("display_set", 1)
|
||||||
|
elif current:
|
||||||
|
onto.remove((term, prop, None))
|
||||||
|
report.add("display_removed", len(current))
|
||||||
|
|
||||||
# 11 — regenerate every label by derivation (TN-018, TN-023)
|
# 13 — regenerate every label by derivation (TN-018, TN-023)
|
||||||
kinds = (OWL.Class, OWL.ObjectProperty, OWL.DatatypeProperty, OWL.AnnotationProperty)
|
kinds = (OWL.Class, OWL.ObjectProperty, OWL.DatatypeProperty, OWL.AnnotationProperty)
|
||||||
for kind in kinds:
|
for kind in kinds:
|
||||||
for term in set(onto.subjects(RDF.type, kind)):
|
for term in set(onto.subjects(RDF.type, kind)):
|
||||||
@@ -262,7 +325,7 @@ def migrate(onto, others, spec, rules, report):
|
|||||||
onto.add((term, RDFS.label, want))
|
onto.add((term, RDFS.label, want))
|
||||||
report.add("labels_regenerated", 1)
|
report.add("labels_regenerated", 1)
|
||||||
|
|
||||||
# 12 — bump the ontology version (EV-014: the namespace itself never moves)
|
# 14 — bump the ontology version (EV-014: the namespace itself never moves)
|
||||||
target = URIRef(ns.rstrip("/") + "/" + spec["meta"]["to_version"])
|
target = URIRef(ns.rstrip("/") + "/" + spec["meta"]["to_version"])
|
||||||
for onto_iri in set(onto.subjects(RDF.type, OWL.Ontology)):
|
for onto_iri in set(onto.subjects(RDF.type, OWL.Ontology)):
|
||||||
if (onto_iri, OWL.versionIRI, target) not in onto:
|
if (onto_iri, OWL.versionIRI, target) not in onto:
|
||||||
@@ -270,6 +333,19 @@ def migrate(onto, others, spec, rules, report):
|
|||||||
onto.add((onto_iri, OWL.versionIRI, target))
|
onto.add((onto_iri, OWL.versionIRI, target))
|
||||||
report.add("version_bumped", 1, str(target))
|
report.add("version_bumped", 1, str(target))
|
||||||
|
|
||||||
|
# 15 — the shapes declare the ontology version they target (EV-010).
|
||||||
|
# The shapes graph carries no ontology node, so it is created here.
|
||||||
|
so = spec["structural"].get("shapes_ontology")
|
||||||
|
if so:
|
||||||
|
node = URIRef(so["iri"])
|
||||||
|
for g in shapes:
|
||||||
|
if (node, DCTERMS.conformsTo, target) not in g:
|
||||||
|
g.remove((node, DCTERMS.conformsTo, None))
|
||||||
|
g.add((node, RDF.type, OWL.Ontology))
|
||||||
|
g.add((node, RDFS.label, Literal(so["label"])))
|
||||||
|
g.add((node, DCTERMS.conformsTo, target))
|
||||||
|
report.add("shapes_conformsTo", 1, str(target))
|
||||||
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------- main
|
# ----------------------------------------------------------------------- main
|
||||||
|
|
||||||
@@ -297,15 +373,22 @@ def main():
|
|||||||
|
|
||||||
onto = Graph()
|
onto = Graph()
|
||||||
onto.parse(args.ontology, format="turtle")
|
onto.parse(args.ontology, format="turtle")
|
||||||
others, other_paths = [], args.instances + args.shapes
|
instances, shapes = [], []
|
||||||
for p in other_paths:
|
for p in args.instances:
|
||||||
g = Graph()
|
g = Graph()
|
||||||
g.parse(p, format="turtle")
|
g.parse(p, format="turtle")
|
||||||
others.append(g)
|
instances.append(g)
|
||||||
|
for p in args.shapes:
|
||||||
|
g = Graph()
|
||||||
|
g.parse(p, format="turtle")
|
||||||
|
g.bind("dcterms", DCTERMS)
|
||||||
|
shapes.append(g)
|
||||||
|
others = instances + shapes
|
||||||
|
other_paths = args.instances + args.shapes
|
||||||
|
|
||||||
before = [len(onto)] + [len(g) for g in others]
|
before = [len(onto)] + [len(g) for g in others]
|
||||||
report = Report()
|
report = Report()
|
||||||
migrate(onto, others, spec, rules, report)
|
migrate(onto, instances, shapes, spec, rules, report)
|
||||||
after = [len(onto)] + [len(g) for g in others]
|
after = [len(onto)] + [len(g) for g in others]
|
||||||
|
|
||||||
print("MIGRATION %s -> %s %s"
|
print("MIGRATION %s -> %s %s"
|
||||||
|
|||||||
@@ -0,0 +1,296 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Migrate the Pernod Ricard Data MetaModel from v2.0 to v2.1.
|
||||||
|
|
||||||
|
USAGE
|
||||||
|
python3 migrate_tbox_v2_1.py # dry run, writes nothing
|
||||||
|
python3 migrate_tbox_v2_1.py --apply
|
||||||
|
|
||||||
|
v2.1 renames nothing. It declares what v2.0 left silent: which properties are
|
||||||
|
polymorphic on purpose, what domain the others take, and how the instances of
|
||||||
|
each concrete class are produced.
|
||||||
|
|
||||||
|
DESIGN
|
||||||
|
EV-004 dry run is the default
|
||||||
|
EV-005 every edit goes through rdflib
|
||||||
|
EV-006 guards test the target state, so a replay reports zero change
|
||||||
|
EV-015 an execution log is written for every attempt
|
||||||
|
"""
|
||||||
|
import argparse
|
||||||
|
import datetime
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
from rdflib import Graph, Literal, Namespace, RDF, RDFS, OWL, URIRef
|
||||||
|
|
||||||
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
SPEC = os.path.join(HERE, "declarations_v2_1.yaml")
|
||||||
|
|
||||||
|
|
||||||
|
class Report(object):
|
||||||
|
def __init__(self):
|
||||||
|
self.steps = OrderedDict()
|
||||||
|
self.notes = []
|
||||||
|
|
||||||
|
def add(self, step, n=1, detail=None):
|
||||||
|
self.steps[step] = self.steps.get(step, 0) + n
|
||||||
|
if detail:
|
||||||
|
self.notes.append("%s: %s" % (step, detail))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def total(self):
|
||||||
|
return sum(self.steps.values())
|
||||||
|
|
||||||
|
|
||||||
|
def md5(path):
|
||||||
|
h = hashlib.md5()
|
||||||
|
with open(path, "rb") as fh:
|
||||||
|
for chunk in iter(lambda: fh.read(65536), b""):
|
||||||
|
h.update(chunk)
|
||||||
|
return h.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def derive_label(name):
|
||||||
|
"""TN-018 and TN-023: a label is derived from the local name, never typed."""
|
||||||
|
spaced = re.sub(r"(?<=[a-z0-9])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])", " ", name)
|
||||||
|
return spaced[0].lower() + spaced[1:]
|
||||||
|
|
||||||
|
|
||||||
|
def local(uri, ns):
|
||||||
|
s = str(uri)
|
||||||
|
return s[len(ns):] if s.startswith(ns) else None
|
||||||
|
|
||||||
|
|
||||||
|
def ancestors(g, term):
|
||||||
|
seen, stack = set(), [term]
|
||||||
|
while stack:
|
||||||
|
node = stack.pop()
|
||||||
|
for parent in g.objects(node, RDFS.subClassOf):
|
||||||
|
if isinstance(parent, URIRef) and parent not in seen:
|
||||||
|
seen.add(parent)
|
||||||
|
stack.append(parent)
|
||||||
|
return seen
|
||||||
|
|
||||||
|
|
||||||
|
def check_version(g, spec, path):
|
||||||
|
"""EV-010 applied to a migration script: refuse to run against the wrong input.
|
||||||
|
|
||||||
|
A script announcing 2.1 -> 2.2 will happily work on a file still in 2.0 and
|
||||||
|
report success, leaving a state that is neither version. The same silent
|
||||||
|
failure the shapes are guarded against, one layer down.
|
||||||
|
"""
|
||||||
|
ns = spec["meta"]["namespace"]
|
||||||
|
expected = URIRef(ns.rstrip("/") + "/" + spec["meta"]["from_version"])
|
||||||
|
target = URIRef(ns.rstrip("/") + "/" + spec["meta"]["to_version"])
|
||||||
|
found = set(g.objects(None, OWL.versionIRI))
|
||||||
|
if target in found:
|
||||||
|
print("ALREADY AT %s — nothing to do." % spec["meta"]["to_version"])
|
||||||
|
sys.exit(0)
|
||||||
|
if expected not in found:
|
||||||
|
print("ABORT — %s declares %s; this script migrates from %s."
|
||||||
|
% (os.path.basename(path),
|
||||||
|
", ".join(sorted(str(f) for f in found)) or "no version",
|
||||||
|
expected))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def migrate(g, instances, spec, report):
|
||||||
|
ns = spec["meta"]["namespace"]
|
||||||
|
pr = Namespace(ns)
|
||||||
|
|
||||||
|
# 1 — declare the annotation property itself
|
||||||
|
for a in spec.get("declare_annotations") or []:
|
||||||
|
term = pr[a["term"]]
|
||||||
|
if (term, RDF.type, OWL.AnnotationProperty) not in g:
|
||||||
|
g.add((term, RDF.type, OWL.AnnotationProperty))
|
||||||
|
g.add((term, RDFS.label, Literal(a["label"])))
|
||||||
|
g.add((term, RDFS.comment, Literal(" ".join(a["comment"].split()))))
|
||||||
|
report.add("annotations_declared", 1, a["term"])
|
||||||
|
|
||||||
|
# 2 — declare the new properties (procedure C1)
|
||||||
|
for d in spec.get("declare_properties") or []:
|
||||||
|
term = pr[d["term"]]
|
||||||
|
kind = getattr(OWL, d["kind"])
|
||||||
|
if (term, RDF.type, kind) not in g:
|
||||||
|
g.add((term, RDF.type, kind))
|
||||||
|
g.add((term, RDFS.label, Literal(derive_label(d["term"]))))
|
||||||
|
g.add((term, RDFS.domain, pr[d["domain"]]))
|
||||||
|
g.add((term, RDFS.range, URIRef(d["range"])))
|
||||||
|
g.add((term, RDFS.comment, Literal(" ".join(d["comment"].split()))))
|
||||||
|
report.add("properties_declared", 1, d["term"])
|
||||||
|
|
||||||
|
# 3 — mark the deliberately polymorphic properties (TN-025)
|
||||||
|
for p in spec.get("polymorphic") or []:
|
||||||
|
term = pr[p["term"]]
|
||||||
|
if (term, None, None) not in g:
|
||||||
|
report.add("polymorphic_missing", 1, p["term"])
|
||||||
|
continue
|
||||||
|
if (term, pr.polymorphic, Literal(True)) not in g:
|
||||||
|
g.add((term, pr.polymorphic, Literal(True)))
|
||||||
|
report.add("polymorphic_declared", 1)
|
||||||
|
if list(g.objects(term, RDFS.domain)):
|
||||||
|
report.add("polymorphic_has_domain", 1,
|
||||||
|
"%s is marked polymorphic and still declares a domain" % p["term"])
|
||||||
|
|
||||||
|
# 4 — declare the domains that were merely missing (TN-025)
|
||||||
|
for d in spec.get("domains") or []:
|
||||||
|
term, target = pr[d["term"]], pr[d["domain"]]
|
||||||
|
if (term, None, None) not in g:
|
||||||
|
report.add("domain_missing_term", 1, d["term"])
|
||||||
|
continue
|
||||||
|
if (target, RDF.type, OWL.Class) not in g:
|
||||||
|
report.add("domain_missing_class", 1, "%s -> %s" % (d["term"], d["domain"]))
|
||||||
|
continue
|
||||||
|
if (term, RDFS.domain, target) not in g:
|
||||||
|
g.remove((term, RDFS.domain, None))
|
||||||
|
g.add((term, RDFS.domain, target))
|
||||||
|
report.add("domains_declared", 1)
|
||||||
|
|
||||||
|
# 5 — ranges, where a polymorphic property still points at one class
|
||||||
|
for r in spec.get("ranges") or []:
|
||||||
|
term, target = pr[r["term"]], pr[r["range"]]
|
||||||
|
if (term, RDFS.range, target) not in g:
|
||||||
|
g.remove((term, RDFS.range, None))
|
||||||
|
g.add((term, RDFS.range, target))
|
||||||
|
report.add("ranges_declared", 1)
|
||||||
|
|
||||||
|
# 6 — attach a layer root to its provenance axis (TN-027)
|
||||||
|
for a in spec.get("attach") or []:
|
||||||
|
term, parent = pr[a["term"]], pr[a["parent"]]
|
||||||
|
if (term, RDFS.subClassOf, parent) not in g:
|
||||||
|
g.add((term, RDFS.subClassOf, parent))
|
||||||
|
report.add("attached", 1, "%s -> %s" % (a["term"], a["parent"]))
|
||||||
|
|
||||||
|
# 7 — provenance belongs to the concrete classes, and to nothing else (TN-026)
|
||||||
|
prov = spec.get("provenance") or {}
|
||||||
|
concrete = {c for c in g.subjects(RDF.type, OWL.Class)
|
||||||
|
if (c, pr.isAbstract, Literal(False)) in g}
|
||||||
|
|
||||||
|
if prov.get("strip_from_non_concrete"):
|
||||||
|
for s in set(g.subjects(pr.authoringMode, None)):
|
||||||
|
if s in concrete:
|
||||||
|
continue
|
||||||
|
g.remove((s, pr.authoringMode, None))
|
||||||
|
g.remove((s, pr.harvestSource, None))
|
||||||
|
report.add("provenance_stripped", 1, local(s, ns))
|
||||||
|
|
||||||
|
if prov.get("derive_from_axis"):
|
||||||
|
for cls in sorted(concrete, key=str):
|
||||||
|
up = ancestors(g, cls)
|
||||||
|
if pr.CapturedObject in up:
|
||||||
|
want = "HARVESTED"
|
||||||
|
elif pr.DefinedObject in up:
|
||||||
|
want = "ASSERTED"
|
||||||
|
else:
|
||||||
|
report.add("provenance_no_axis", 1, local(cls, ns))
|
||||||
|
continue
|
||||||
|
if (cls, pr.authoringMode, Literal(want)) in g:
|
||||||
|
continue
|
||||||
|
g.remove((cls, pr.authoringMode, None))
|
||||||
|
g.add((cls, pr.authoringMode, Literal(want)))
|
||||||
|
report.add("provenance_declared", 1)
|
||||||
|
|
||||||
|
# 8 — instance corrections, each one surfaced by declaring a domain
|
||||||
|
ex = Namespace("https://data.pernod-ricard.com/sodh/")
|
||||||
|
for fix in spec.get("instance_fixes") or []:
|
||||||
|
prop = pr[fix["property"]]
|
||||||
|
dest = pr[fix["to"]] if fix.get("to") else None
|
||||||
|
for subject in fix["subjects"]:
|
||||||
|
node = ex[subject]
|
||||||
|
for gi in instances:
|
||||||
|
for _, _, value in list(gi.triples((node, prop, None))):
|
||||||
|
gi.remove((node, prop, value))
|
||||||
|
if dest is not None:
|
||||||
|
gi.add((node, dest, value))
|
||||||
|
report.add("instances_%sd" % fix["action"], 1,
|
||||||
|
"%s %s" % (subject, fix["property"]))
|
||||||
|
|
||||||
|
# 9 — bump the version (EV-014: the namespace itself never moves)
|
||||||
|
target = URIRef(ns.rstrip("/") + "/" + spec["meta"]["to_version"])
|
||||||
|
for onto in set(g.subjects(RDF.type, OWL.Ontology)):
|
||||||
|
if (onto, OWL.versionIRI, target) not in g:
|
||||||
|
g.remove((onto, OWL.versionIRI, None))
|
||||||
|
g.add((onto, OWL.versionIRI, target))
|
||||||
|
report.add("version_bumped", 1, str(target))
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ap = argparse.ArgumentParser()
|
||||||
|
ap.add_argument("--apply", action="store_true")
|
||||||
|
ap.add_argument("--ontology", default=os.path.join(HERE, "..", "..", "ontology",
|
||||||
|
"pr_metamodel.ttl"))
|
||||||
|
ap.add_argument("--instances", action="append", default=[])
|
||||||
|
ap.add_argument("--log-dir", default=os.path.join(HERE, "logs"))
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
if not os.path.exists(args.ontology):
|
||||||
|
print("MISSING INPUT: %s" % args.ontology)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
spec = yaml.safe_load(open(SPEC, encoding="utf-8"))
|
||||||
|
checksums = OrderedDict((p, md5(p)) for p in [args.ontology] + args.instances)
|
||||||
|
|
||||||
|
g = Graph()
|
||||||
|
g.parse(args.ontology, format="turtle")
|
||||||
|
instances = []
|
||||||
|
for path in args.instances:
|
||||||
|
gi = Graph()
|
||||||
|
gi.parse(path, format="turtle")
|
||||||
|
instances.append(gi)
|
||||||
|
check_version(g, spec, args.ontology)
|
||||||
|
|
||||||
|
before = [len(g)] + [len(x) for x in instances]
|
||||||
|
|
||||||
|
report = Report()
|
||||||
|
migrate(g, instances, spec, report)
|
||||||
|
after = [len(g)] + [len(x) for x in instances]
|
||||||
|
|
||||||
|
print("MIGRATION %s -> %s %s"
|
||||||
|
% (spec["meta"]["from_version"], spec["meta"]["to_version"],
|
||||||
|
"APPLY" if args.apply else "DRY RUN"))
|
||||||
|
print()
|
||||||
|
for step, n in report.steps.items():
|
||||||
|
print(" %-28s %6d" % (step, n))
|
||||||
|
print(" %-28s %6d" % ("total changes", report.total))
|
||||||
|
print()
|
||||||
|
for path, b, a in zip([args.ontology] + args.instances, before, after):
|
||||||
|
print(" %-46s %6d -> %6d triples" % (os.path.basename(path), b, a))
|
||||||
|
if report.notes:
|
||||||
|
print()
|
||||||
|
for n in report.notes:
|
||||||
|
print(" note: " + n)
|
||||||
|
|
||||||
|
os.makedirs(args.log_dir, exist_ok=True)
|
||||||
|
stamp = datetime.datetime.now().strftime("%Y%m%dT%H%M%S")
|
||||||
|
log_path = os.path.join(args.log_dir, "migration_v2_1_%s.json" % stamp)
|
||||||
|
json.dump({"attempt_timestamp": datetime.datetime.now().isoformat(timespec="seconds"),
|
||||||
|
"mode": "apply" if args.apply else "dry-run",
|
||||||
|
"from_version": spec["meta"]["from_version"],
|
||||||
|
"to_version": spec["meta"]["to_version"],
|
||||||
|
"input_checksums": checksums,
|
||||||
|
"steps": report.steps, "total_changes": report.total,
|
||||||
|
"triples_before": dict(zip([args.ontology] + args.instances, before)),
|
||||||
|
"triples_after": dict(zip([args.ontology] + args.instances, after)),
|
||||||
|
"notes": report.notes},
|
||||||
|
open(log_path, "w", encoding="utf-8"), indent=2)
|
||||||
|
print("\n log: %s" % log_path)
|
||||||
|
|
||||||
|
if not args.apply:
|
||||||
|
print("\n DRY RUN — nothing written. Re-run with --apply when the counts "
|
||||||
|
"above are what you expect.")
|
||||||
|
return
|
||||||
|
|
||||||
|
g.serialize(destination=args.ontology, format="turtle")
|
||||||
|
for path, gi in zip(args.instances, instances):
|
||||||
|
gi.serialize(destination=path, format="turtle")
|
||||||
|
print("\n written. Replay this script now: a second run must report zero "
|
||||||
|
"changes (EV-006).")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,367 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Migrate the Pernod Ricard Data MetaModel and the SODH instances to v2.2.
|
||||||
|
|
||||||
|
USAGE
|
||||||
|
python3 migrate_tbox_v2_2.py --ontology ... --instances ...
|
||||||
|
python3 migrate_tbox_v2_2.py --ontology ... --instances ... --apply
|
||||||
|
|
||||||
|
v2.2 turns the actor nodes into governance ASSIGNMENTS: a stable post carrying
|
||||||
|
an identifier, a domain and a name, plus the person holding it as a value. It
|
||||||
|
completes the twelve that existed, creates the ten sub-domain owners, the
|
||||||
|
product owner, and one steward assignment per business object.
|
||||||
|
|
||||||
|
DESIGN
|
||||||
|
EV-004 dry run is the default
|
||||||
|
EV-005 every edit goes through rdflib
|
||||||
|
EV-006 guards test the target state, so a replay reports zero change
|
||||||
|
EV-015 an execution log is written for every attempt
|
||||||
|
"""
|
||||||
|
import argparse
|
||||||
|
import datetime
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
from rdflib import Graph, Literal, Namespace, RDF, RDFS, OWL, URIRef
|
||||||
|
|
||||||
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
SPEC = os.path.join(HERE, "assignments_v2_2.yaml")
|
||||||
|
|
||||||
|
|
||||||
|
class Report(object):
|
||||||
|
def __init__(self):
|
||||||
|
self.steps = OrderedDict()
|
||||||
|
self.notes = []
|
||||||
|
|
||||||
|
def add(self, step, n=1, detail=None):
|
||||||
|
self.steps[step] = self.steps.get(step, 0) + n
|
||||||
|
if detail:
|
||||||
|
self.notes.append("%s: %s" % (step, detail))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def total(self):
|
||||||
|
return sum(self.steps.values())
|
||||||
|
|
||||||
|
|
||||||
|
def derive_label(name):
|
||||||
|
"""TN-018 and TN-023: a label is derived from the local name, never typed."""
|
||||||
|
spaced = re.sub(r"(?<=[a-z0-9])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])", " ", name)
|
||||||
|
return spaced[0].lower() + spaced[1:]
|
||||||
|
|
||||||
|
|
||||||
|
def md5(path):
|
||||||
|
h = hashlib.md5()
|
||||||
|
with open(path, "rb") as fh:
|
||||||
|
for chunk in iter(lambda: fh.read(65536), b""):
|
||||||
|
h.update(chunk)
|
||||||
|
return h.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def rename_node(g, old, new, report, step):
|
||||||
|
"""EV-001 applied to an instance: rewrite every triple naming the node."""
|
||||||
|
if (new, None, None) in g and (old, None, None) not in g:
|
||||||
|
return 0
|
||||||
|
n = 0
|
||||||
|
for s, p, o in list(g):
|
||||||
|
ns, no = (new if s == old else s), (new if o == old else o)
|
||||||
|
if (ns, no) != (s, o):
|
||||||
|
g.remove((s, p, o))
|
||||||
|
g.add((ns, p, no))
|
||||||
|
n += 1
|
||||||
|
report.add(step, n)
|
||||||
|
return n
|
||||||
|
|
||||||
|
|
||||||
|
def assign(g, pr, node, role, identifier, name, held_by, domain, spec, report, step):
|
||||||
|
"""Write one assignment. The guard tests the identifier, so a replay is a no-op."""
|
||||||
|
if (node, pr.identifier, Literal(identifier)) in g:
|
||||||
|
return False
|
||||||
|
g.add((node, RDF.type, role))
|
||||||
|
g.remove((node, pr.identifier, None))
|
||||||
|
g.add((node, pr.identifier, Literal(identifier)))
|
||||||
|
g.remove((node, pr.canonicalName, None))
|
||||||
|
g.add((node, pr.canonicalName, Literal(name)))
|
||||||
|
g.remove((node, pr.personName, None))
|
||||||
|
if held_by:
|
||||||
|
g.add((node, pr.personName, Literal(held_by)))
|
||||||
|
g.remove((node, pr.status, None))
|
||||||
|
g.add((node, pr.status, Literal(spec["status"])))
|
||||||
|
g.remove((node, pr.version, None))
|
||||||
|
g.add((node, pr.version, Literal(spec["version"])))
|
||||||
|
if domain is not None:
|
||||||
|
g.remove((node, pr.ownedByDomain, None))
|
||||||
|
g.add((node, pr.ownedByDomain, domain))
|
||||||
|
report.add(step, 1)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def check_version(g, spec, path):
|
||||||
|
"""EV-010 applied to a migration script: refuse to run against the wrong input.
|
||||||
|
|
||||||
|
A script announcing 2.1 -> 2.2 will happily work on a file still in 2.0 and
|
||||||
|
report success, leaving a state that is neither version. The same silent
|
||||||
|
failure the shapes are guarded against, one layer down.
|
||||||
|
"""
|
||||||
|
ns = spec["meta"]["namespace"]
|
||||||
|
expected = URIRef(ns.rstrip("/") + "/" + spec["meta"]["from_version"])
|
||||||
|
target = URIRef(ns.rstrip("/") + "/" + spec["meta"]["to_version"])
|
||||||
|
found = set(g.objects(None, OWL.versionIRI))
|
||||||
|
if target in found:
|
||||||
|
print("ALREADY AT %s — nothing to do." % spec["meta"]["to_version"])
|
||||||
|
sys.exit(0)
|
||||||
|
if expected not in found:
|
||||||
|
print("ABORT — %s declares %s; this script migrates from %s."
|
||||||
|
% (os.path.basename(path),
|
||||||
|
", ".join(sorted(str(f) for f in found)) or "no version",
|
||||||
|
expected))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def migrate(onto, data, spec, report):
|
||||||
|
ns, ins = spec["meta"]["namespace"], spec["meta"]["instance_namespace"]
|
||||||
|
pr, ex = Namespace(ns), Namespace(ins)
|
||||||
|
|
||||||
|
# 1 — declare heldBy on Actor (procedure C1)
|
||||||
|
for d in spec.get("declare_properties") or []:
|
||||||
|
term = pr[d["term"]]
|
||||||
|
if (term, RDF.type, getattr(OWL, d["kind"])) not in onto:
|
||||||
|
onto.add((term, RDF.type, getattr(OWL, d["kind"])))
|
||||||
|
onto.add((term, RDFS.label, Literal(derive_label(d["term"]))))
|
||||||
|
onto.add((term, RDFS.domain, pr[d["domain"]]))
|
||||||
|
onto.add((term, RDFS.range, URIRef(d["range"])))
|
||||||
|
onto.add((term, RDFS.comment, Literal(" ".join(d["comment"].split()))))
|
||||||
|
report.add("properties_declared", 1, d["term"])
|
||||||
|
|
||||||
|
# 2 — the short form of Data Steward is DST, and the rulebook now says so
|
||||||
|
for u in spec.get("display_updates") or []:
|
||||||
|
term = pr[u["term"]]
|
||||||
|
if (term, pr.acronym, Literal(u["acronym"])) not in onto:
|
||||||
|
onto.remove((term, pr.acronym, None))
|
||||||
|
onto.add((term, pr.acronym, Literal(u["acronym"])))
|
||||||
|
report.add("display_updated", 1, "%s -> %s" % (u["term"], u["acronym"]))
|
||||||
|
|
||||||
|
# 3 — rename the instance nodes that carried the wrong short form
|
||||||
|
for r in spec.get("instance_renames") or []:
|
||||||
|
rename_node(data, ex[r["from"]], ex[r["to"]], report, "instances_renamed")
|
||||||
|
|
||||||
|
# 4 — complete the assignments that already existed
|
||||||
|
for c in spec.get("complete") or []:
|
||||||
|
node = ex[c["node"]]
|
||||||
|
if (node, None, None) not in data:
|
||||||
|
report.add("complete_missing", 1, c["node"])
|
||||||
|
continue
|
||||||
|
role = next(iter(data.objects(node, RDF.type)), None)
|
||||||
|
assign(data, pr, node, role, c["identifier"], c["name"],
|
||||||
|
c["held_by"], ex[c["domain"]], spec, report, "assignments_completed")
|
||||||
|
|
||||||
|
# 5 — sub-domain owners
|
||||||
|
sdo_of = {}
|
||||||
|
for s in spec.get("create_sub_domain_owners") or []:
|
||||||
|
node = ex[s["node"]]
|
||||||
|
sdo_of[ex[s["sub_domain"]]] = node
|
||||||
|
if assign(data, pr, node, pr.DataSubDomainOwner, s["identifier"],
|
||||||
|
spec["sub_domain_owner_name"], s["held_by"], ex[s["domain"]],
|
||||||
|
spec, report, "sub_domain_owners_created"):
|
||||||
|
data.add((ex[s["sub_domain"]], pr.hasSubDomainOwner, node))
|
||||||
|
|
||||||
|
# 6 — product owners
|
||||||
|
for p in spec.get("create_product_owners") or []:
|
||||||
|
node = ex[p["node"]]
|
||||||
|
product = ex[p["product"]]
|
||||||
|
if (product, None, None) not in data:
|
||||||
|
report.add("product_missing", 1, p["product"])
|
||||||
|
continue
|
||||||
|
if assign(data, pr, node, pr.DataProductOwner, p["identifier"],
|
||||||
|
spec["product_owner_name"], p["held_by"], ex[p["domain"]],
|
||||||
|
spec, report, "product_owners_created"):
|
||||||
|
data.add((product, pr.hasProductOwner, node))
|
||||||
|
|
||||||
|
# 7 — create the business objects that close a governance chain
|
||||||
|
for b in spec.get("create_business_objects") or []:
|
||||||
|
node = ex[b["node"]]
|
||||||
|
if (node, pr.identifier, Literal(b["identifier"])) in data:
|
||||||
|
continue
|
||||||
|
data.add((node, RDF.type, pr.BusinessObject))
|
||||||
|
data.add((node, pr.identifier, Literal(b["identifier"])))
|
||||||
|
data.add((node, pr.canonicalName, Literal(b["name"])))
|
||||||
|
data.add((node, pr.isAbout, ex[b["is_about"]]))
|
||||||
|
data.add((node, pr.belongsTo, ex[b["sub_domain"]]))
|
||||||
|
data.add((node, pr.ownedByDomain, ex[b["domain"]]))
|
||||||
|
data.add((node, pr.status, Literal(spec["status"])))
|
||||||
|
data.add((node, pr.version, Literal(spec["version"])))
|
||||||
|
report.add("business_objects_created", 1, b["node"])
|
||||||
|
|
||||||
|
# 8 — one steward assignment per business object, derived from the graph
|
||||||
|
holders = spec.get("steward_holders") or {}
|
||||||
|
steward_of = {}
|
||||||
|
for bo in sorted(data.subjects(RDF.type, pr.BusinessObject), key=str):
|
||||||
|
ident = next((str(v) for v in data.objects(bo, pr.identifier)), None)
|
||||||
|
if not ident:
|
||||||
|
report.add("steward_no_identifier", 1, str(bo).replace(ins, ""))
|
||||||
|
continue
|
||||||
|
suffix = ident.split("-", 1)[1] # 06.01-001
|
||||||
|
domain_key = "DD_" + suffix.split(".", 1)[0] # DD_06
|
||||||
|
holder = holders.get(domain_key)
|
||||||
|
if holder is None:
|
||||||
|
report.add("steward_no_holder", 1, domain_key)
|
||||||
|
continue
|
||||||
|
node = ex["DST_" + suffix.replace(".", "_").replace("-", "_")]
|
||||||
|
steward_of[bo] = node
|
||||||
|
if assign(data, pr, node, pr.DataSteward, "DST-" + suffix,
|
||||||
|
spec["steward_name"], holder, ex[domain_key],
|
||||||
|
spec, report, "stewards_created"):
|
||||||
|
for _, _, old in list(data.triples((bo, pr.monitoredBy, None))):
|
||||||
|
data.remove((bo, pr.monitoredBy, old))
|
||||||
|
data.add((bo, pr.monitoredBy, node))
|
||||||
|
|
||||||
|
# 9 — resolve every ownedBy pointing at a per-domain steward node
|
||||||
|
res = spec.get("owned_by_resolution") or {}
|
||||||
|
old_stewards = {ex[r["from"]] for r in (spec.get("instance_renames") or [])} | \
|
||||||
|
{ex[r["to"]] for r in (spec.get("instance_renames") or [])}
|
||||||
|
|
||||||
|
def business_object_of(subject):
|
||||||
|
"""Follow the declared chains until a business object is reached."""
|
||||||
|
for chain in res.get("chains") or []:
|
||||||
|
prop = pr[chain["via"]]
|
||||||
|
if chain["direction"] == "inbound":
|
||||||
|
for candidate in data.subjects(prop, subject):
|
||||||
|
if (candidate, RDF.type, pr.BusinessObject) in data:
|
||||||
|
return candidate
|
||||||
|
else:
|
||||||
|
for target in data.objects(subject, prop):
|
||||||
|
if (target, RDF.type, pr.BusinessObject) in data:
|
||||||
|
return target
|
||||||
|
for candidate in data.subjects(pr.isAbout, target):
|
||||||
|
if (candidate, RDF.type, pr.BusinessObject) in data:
|
||||||
|
return candidate
|
||||||
|
return None
|
||||||
|
|
||||||
|
remove_on = {pr[c] for c in (res.get("remove_on") or [])}
|
||||||
|
for subject, _, target in list(data.triples((None, pr.ownedBy, None))):
|
||||||
|
if target not in old_stewards:
|
||||||
|
continue
|
||||||
|
types = set(data.objects(subject, RDF.type))
|
||||||
|
if types & remove_on:
|
||||||
|
data.remove((subject, pr.ownedBy, target))
|
||||||
|
report.add("ownedby_removed", 1)
|
||||||
|
continue
|
||||||
|
if pr.DataSubDomain in types and res.get("repoint_sub_domains"):
|
||||||
|
owner = sdo_of.get(subject)
|
||||||
|
if owner is None:
|
||||||
|
report.add("ownedby_unresolved", 1,
|
||||||
|
"%s has no sub-domain owner" % str(subject).replace(ins, ""))
|
||||||
|
continue
|
||||||
|
data.remove((subject, pr.ownedBy, target))
|
||||||
|
data.add((subject, pr.ownedBy, owner))
|
||||||
|
report.add("ownedby_repointed_to_sdo", 1)
|
||||||
|
continue
|
||||||
|
bo = business_object_of(subject)
|
||||||
|
owner = steward_of.get(bo) if bo is not None else None
|
||||||
|
if owner is None:
|
||||||
|
report.add("ownedby_unresolved", 1,
|
||||||
|
"%s reaches no business object" % str(subject).replace(ins, ""))
|
||||||
|
continue
|
||||||
|
data.remove((subject, pr.ownedBy, target))
|
||||||
|
data.add((subject, pr.ownedBy, owner))
|
||||||
|
report.add("ownedby_repointed_to_steward", 1)
|
||||||
|
|
||||||
|
# 10 — withdraw the per-domain steward nodes, once nothing names them (EV-003)
|
||||||
|
if spec.get("retire_domain_stewards"):
|
||||||
|
for r in spec.get("instance_renames") or []:
|
||||||
|
node = ex[r["to"]]
|
||||||
|
referrers = {s for s in data.subjects(None, node)}
|
||||||
|
if referrers:
|
||||||
|
report.add("steward_still_referenced", 1,
|
||||||
|
"%s cited by %d subject(s)" % (r["to"], len(referrers)))
|
||||||
|
continue
|
||||||
|
n = 0
|
||||||
|
for t in list(data.triples((node, None, None))):
|
||||||
|
data.remove(t)
|
||||||
|
n += 1
|
||||||
|
if n:
|
||||||
|
report.add("domain_stewards_retired", 1, r["to"])
|
||||||
|
|
||||||
|
# 11 — bump the version
|
||||||
|
target = URIRef(ns.rstrip("/") + "/" + spec["meta"]["to_version"])
|
||||||
|
for o in set(onto.subjects(RDF.type, OWL.Ontology)):
|
||||||
|
if (o, OWL.versionIRI, target) not in onto:
|
||||||
|
onto.remove((o, OWL.versionIRI, None))
|
||||||
|
onto.add((o, OWL.versionIRI, target))
|
||||||
|
report.add("version_bumped", 1, str(target))
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ap = argparse.ArgumentParser()
|
||||||
|
ap.add_argument("--apply", action="store_true")
|
||||||
|
ap.add_argument("--ontology", required=True)
|
||||||
|
ap.add_argument("--instances", required=True)
|
||||||
|
ap.add_argument("--log-dir", default=os.path.join(HERE, "logs"))
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
for path in (args.ontology, args.instances):
|
||||||
|
if not os.path.exists(path):
|
||||||
|
print("MISSING INPUT: %s" % path)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
spec = yaml.safe_load(open(SPEC, encoding="utf-8"))
|
||||||
|
checksums = OrderedDict((p, md5(p)) for p in (args.ontology, args.instances))
|
||||||
|
|
||||||
|
onto, data = Graph(), Graph()
|
||||||
|
onto.parse(args.ontology, format="turtle")
|
||||||
|
data.parse(args.instances, format="turtle")
|
||||||
|
check_version(onto, spec, args.ontology)
|
||||||
|
|
||||||
|
before = (len(onto), len(data))
|
||||||
|
|
||||||
|
report = Report()
|
||||||
|
migrate(onto, data, spec, report)
|
||||||
|
after = (len(onto), len(data))
|
||||||
|
|
||||||
|
print("MIGRATION %s -> %s %s"
|
||||||
|
% (spec["meta"]["from_version"], spec["meta"]["to_version"],
|
||||||
|
"APPLY" if args.apply else "DRY RUN"))
|
||||||
|
print()
|
||||||
|
for step, n in report.steps.items():
|
||||||
|
print(" %-28s %6d" % (step, n))
|
||||||
|
print(" %-28s %6d" % ("total changes", report.total))
|
||||||
|
print()
|
||||||
|
for path, b, a in zip((args.ontology, args.instances), before, after):
|
||||||
|
print(" %-46s %6d -> %6d triples" % (os.path.basename(path), b, a))
|
||||||
|
if report.notes:
|
||||||
|
print()
|
||||||
|
for n in report.notes:
|
||||||
|
print(" note: " + n)
|
||||||
|
|
||||||
|
os.makedirs(args.log_dir, exist_ok=True)
|
||||||
|
stamp = datetime.datetime.now().strftime("%Y%m%dT%H%M%S")
|
||||||
|
log_path = os.path.join(args.log_dir, "migration_v2_2_%s.json" % stamp)
|
||||||
|
json.dump({"attempt_timestamp": datetime.datetime.now().isoformat(timespec="seconds"),
|
||||||
|
"mode": "apply" if args.apply else "dry-run",
|
||||||
|
"from_version": spec["meta"]["from_version"],
|
||||||
|
"to_version": spec["meta"]["to_version"],
|
||||||
|
"input_checksums": checksums,
|
||||||
|
"steps": report.steps, "total_changes": report.total,
|
||||||
|
"triples_before": dict(zip((args.ontology, args.instances), before)),
|
||||||
|
"triples_after": dict(zip((args.ontology, args.instances), after)),
|
||||||
|
"notes": report.notes},
|
||||||
|
open(log_path, "w", encoding="utf-8"), indent=2)
|
||||||
|
print("\n log: %s" % log_path)
|
||||||
|
|
||||||
|
if not args.apply:
|
||||||
|
print("\n DRY RUN — nothing written. Re-run with --apply when the counts "
|
||||||
|
"above are what you expect.")
|
||||||
|
return
|
||||||
|
|
||||||
|
onto.serialize(destination=args.ontology, format="turtle")
|
||||||
|
data.serialize(destination=args.instances, format="turtle")
|
||||||
|
print("\n written. Replay this script now: a second run must report zero "
|
||||||
|
"changes (EV-006).")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -98,6 +98,19 @@ merges:
|
|||||||
controlled per class by SHACL. Ranges are compatible, so no RDFS retyping
|
controlled per class by SHACL. Ranges are compatible, so no RDFS retyping
|
||||||
is introduced (TN-028).
|
is introduced (TN-028).
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# A-BOX ALIGNMENT
|
||||||
|
# The instance graph was written against nominal forms that the vocabulary never
|
||||||
|
# declared: pr:identifier, pr:version, pr:status, pr:businessDefinition, pr:unit,
|
||||||
|
# pr:formula, pr:shortLabel, pr:physicalName. The TN-012 renames reconcile the
|
||||||
|
# two sides, since the vocabulary moves to the forms the instances already use.
|
||||||
|
# Two of them do not converge on their own and are renamed on the instance side.
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
abox_renames:
|
||||||
|
- {from: name, to: canonicalName, reason: "the vocabulary target is canonicalName; name alone is too generic to stand as an identity"}
|
||||||
|
- {from: format, to: logicalFormat, reason: "the vocabulary target is logicalFormat; format alone does not say which of the two"}
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# STRUCTURAL OPERATIONS
|
# STRUCTURAL OPERATIONS
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -152,3 +165,11 @@ structural:
|
|||||||
# deprecated terms are removed outright rather than kept as stubs. EV-002
|
# deprecated terms are removed outright rather than kept as stubs. EV-002
|
||||||
# still applies — the count must be zero.
|
# still applies — the count must be zero.
|
||||||
drop_deprecated: true
|
drop_deprecated: true
|
||||||
|
|
||||||
|
# EV-010: the shapes graph carries no ontology node at all, so the declaration
|
||||||
|
# has to be created rather than aligned. Without it, an instance graph can be
|
||||||
|
# validated against shapes written for another version and the run reports
|
||||||
|
# violations that read exactly like a regression of the model.
|
||||||
|
shapes_ontology:
|
||||||
|
iri: "https://ontology.pernod-ricard.com/shapes"
|
||||||
|
label: PR Data MetaModel SHACL Shapes
|
||||||
|
|||||||
@@ -0,0 +1,152 @@
|
|||||||
|
# NAMING LEXICON
|
||||||
|
# =============================================================================
|
||||||
|
# The word knowledge the naming checks need and cannot derive. No script can
|
||||||
|
# decide on its own that "qualified" is a participle and "ordinal" an adjective.
|
||||||
|
#
|
||||||
|
# Anything absent from this file is not a violation: it is reported as REVIEW.
|
||||||
|
# A checker that guessed would either block on false positives or, worse, stay
|
||||||
|
# silent on real ones. Adding a word here is a deliberate act, reviewed like any
|
||||||
|
# other change to the vocabulary.
|
||||||
|
#
|
||||||
|
# Consumed by check_tbox_naming.py.
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# VERBS — TN-009. A relation begins with one of these.
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
active_verbs:
|
||||||
|
- belongs
|
||||||
|
- computes
|
||||||
|
- consumes
|
||||||
|
- constrains
|
||||||
|
- contains
|
||||||
|
- depends
|
||||||
|
- exposes
|
||||||
|
- has
|
||||||
|
- lives
|
||||||
|
- measures
|
||||||
|
- packages
|
||||||
|
- produces
|
||||||
|
- references
|
||||||
|
- represents
|
||||||
|
- serves
|
||||||
|
- sources
|
||||||
|
- stores
|
||||||
|
- uses
|
||||||
|
|
||||||
|
# A past participle is a verb in first position (TN-009). Listed separately
|
||||||
|
# because the two forms read differently on an edge: the active says what the
|
||||||
|
# subject does, the participial what was done to it.
|
||||||
|
participles:
|
||||||
|
- computed
|
||||||
|
- derived
|
||||||
|
- governed
|
||||||
|
- hosted
|
||||||
|
- monitored
|
||||||
|
- operated
|
||||||
|
- owned
|
||||||
|
- packaged
|
||||||
|
- qualified
|
||||||
|
- served
|
||||||
|
- sourced
|
||||||
|
- stored
|
||||||
|
|
||||||
|
# The copula, admitted by TN-010 on object properties only.
|
||||||
|
copula:
|
||||||
|
- is
|
||||||
|
|
||||||
|
# TN-009: an adverb qualifying the verb is transparent. The checker skips it and
|
||||||
|
# tests the token that follows.
|
||||||
|
adverbs:
|
||||||
|
- primarily
|
||||||
|
- fully
|
||||||
|
- partially
|
||||||
|
- directly
|
||||||
|
- initially
|
||||||
|
- currently
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# ACRONYMS — TN-002 and TN-019.
|
||||||
|
# Forbidden in an IRI, with the single exception of TN-011: a relation
|
||||||
|
# reproducing the short label of the class it targets carries it as it stands.
|
||||||
|
# Listed so that the checker names what it found rather than reporting an
|
||||||
|
# anonymous run of capitals.
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
known_acronyms:
|
||||||
|
BI: Business Intelligence
|
||||||
|
KPI: Key Performance Indicator
|
||||||
|
DD: Data Domain
|
||||||
|
SD: Data Sub Domain
|
||||||
|
BO: Business Object
|
||||||
|
DDO: Data Domain Owner
|
||||||
|
SDO: Data Sub Domain Owner
|
||||||
|
PO: Data Product Owner
|
||||||
|
DGL: Data Governance Lead
|
||||||
|
URI: Uniform Resource Identifier
|
||||||
|
URL: Uniform Resource Locator
|
||||||
|
API: Application Programming Interface
|
||||||
|
SQL: Structured Query Language
|
||||||
|
ETL: Extract Transform Load
|
||||||
|
UAT: User Acceptance Testing
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# TYPE WORDS — TN-005. A class name carries none of these unless the word names
|
||||||
|
# a genuine abstraction of the model, which the exceptions below record.
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
type_words:
|
||||||
|
- Entity
|
||||||
|
- Item
|
||||||
|
- Element
|
||||||
|
- Thing
|
||||||
|
- Record
|
||||||
|
|
||||||
|
type_word_exceptions:
|
||||||
|
- MetaModelObject
|
||||||
|
- DefinedObject
|
||||||
|
- CapturedObject
|
||||||
|
- OwnershipLayerObject
|
||||||
|
- BusinessLayerObject
|
||||||
|
- LogicalLayerObject
|
||||||
|
- PhysicalLayerObject
|
||||||
|
- DeliveryLayerObject
|
||||||
|
- ConsumptionLayerObject
|
||||||
|
- GovernanceLayerObject
|
||||||
|
- BusinessObject
|
||||||
|
- DataElement
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# PLURALS — TN-005. A class name is singular. English plural detection is
|
||||||
|
# unreliable, so the checker reports a suspected plural as REVIEW and this list
|
||||||
|
# silences the words that only look plural.
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
not_plural:
|
||||||
|
- Status
|
||||||
|
- Address
|
||||||
|
- Business
|
||||||
|
- Analysis
|
||||||
|
- Schema
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# RELATIONAL SUFFIXES — TN-014. Reserved for object properties. An attribute
|
||||||
|
# ending in one of these reads as an edge and will be mistaken for one.
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
relational_suffixes:
|
||||||
|
- By
|
||||||
|
- In
|
||||||
|
- From
|
||||||
|
- On
|
||||||
|
- To
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# DATE RANGES — TN-013. A property of one of these ranges is a date attribute
|
||||||
|
# and takes the nominal form.
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
date_ranges:
|
||||||
|
- "http://www.w3.org/2001/XMLSchema#date"
|
||||||
|
- "http://www.w3.org/2001/XMLSchema#dateTime"
|
||||||
+103
-41
@@ -17,7 +17,7 @@
|
|||||||
|
|
||||||
meta:
|
meta:
|
||||||
title: Pernod Ricard Data MetaModel — T-Box Rulebook
|
title: Pernod Ricard Data MetaModel — T-Box Rulebook
|
||||||
version: "1.1"
|
version: "1.4"
|
||||||
status: Draft for review
|
status: Draft for review
|
||||||
date: "2026-08-03"
|
date: "2026-08-03"
|
||||||
scope: >
|
scope: >
|
||||||
@@ -249,8 +249,9 @@ rules:
|
|||||||
statement: >
|
statement: >
|
||||||
The local name of an object property begins with a verb, in lower case. Both
|
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
|
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
|
participle is a verb in first position. An adverb qualifying that verb is
|
||||||
preposition or with a noun.
|
transparent and may precede it. No relation begins with a preposition or
|
||||||
|
with a noun.
|
||||||
scope: [object_property]
|
scope: [object_property]
|
||||||
severity: BLOCKING
|
severity: BLOCKING
|
||||||
control: {tier: script, executor: check_tbox_naming.py, procedure: null}
|
control: {tier: script, executor: check_tbox_naming.py, procedure: null}
|
||||||
@@ -258,12 +259,14 @@ rules:
|
|||||||
rationale: >
|
rationale: >
|
||||||
A relation reads as a verb and an attribute reads as a noun. The distinction
|
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
|
is what lets a reader tell an edge from a field without opening the
|
||||||
declaration. The explicit clause on participial forms is required: read
|
declaration. Two clauses are stated rather than left implicit, because a
|
||||||
literally, a verb-first rule would condemn a whole family of sound relations
|
literal reading would condemn sound relations: the participial one, which
|
||||||
such as computedBy, storedIn and derivedFrom.
|
covers a whole family such as computedBy, storedIn and derivedFrom, and the
|
||||||
|
adverbial one, which covers a relation whose verb is qualified rather than
|
||||||
|
replaced.
|
||||||
examples:
|
examples:
|
||||||
- {from: "pr:inDatabase", to: "pr:isInDatabase", note: a preposition is not a verb}
|
- {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:primaryLocation", to: "pr:primarilyStoredIn", note: "a noun is not a verb; the adverb is transparent and the verb follows"}
|
||||||
- {from: "pr:computedBy", to: "pr:computedBy", note: participial form is conforming}
|
- {from: "pr:computedBy", to: "pr:computedBy", note: participial form is conforming}
|
||||||
|
|
||||||
- id: TN-010
|
- id: TN-010
|
||||||
@@ -520,9 +523,12 @@ rules:
|
|||||||
category: Label
|
category: Label
|
||||||
title: The short label carries the spoken form
|
title: The short label carries the spoken form
|
||||||
statement: >
|
statement: >
|
||||||
A term carries pr:shortLabel whenever the form derived under TN-018 is not
|
A term carries pr:shortLabel ONLY where the form derived under TN-018 is not
|
||||||
what the organisation writes or says. The short label is free: acronyms,
|
what the organisation writes or says. Where the two coincide, no short label
|
||||||
abbreviations, hyphens, short forms.
|
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]
|
scope: [class, individual]
|
||||||
severity: MAJOR
|
severity: MAJOR
|
||||||
control: {tier: human, executor: review checklist, procedure: null}
|
control: {tier: human, executor: review checklist, procedure: null}
|
||||||
@@ -531,12 +537,16 @@ rules:
|
|||||||
The short label is what makes the derivation rule bearable. Without it, a
|
The short label is what makes the derivation rule bearable. Without it, a
|
||||||
mechanical label impoverishes every screen; with it, display becomes
|
mechanical label impoverishes every screen; with it, display becomes
|
||||||
explicit and centrally governed instead of being improvised by each
|
explicit and centrally governed instead of being improvised by each
|
||||||
consumer. It is MAJOR rather than BLOCKING because judging whether a derived
|
consumer. Restricting it to the cases where it actually differs is what
|
||||||
form matches what people actually say is human work, and EV-011 forbids
|
keeps it meaningful: a short label present on every term degenerates into a
|
||||||
declaring a rule blocking with no executor.
|
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:
|
examples:
|
||||||
- {from: "\"Data Sub Domain Owner\"", to: "short label Sub Domain Owner", note: the word Data is dropped in the spoken form}
|
- {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: "\"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
|
- id: TN-023
|
||||||
category: Label
|
category: Label
|
||||||
@@ -589,9 +599,9 @@ rules:
|
|||||||
title: Every property declares its domain and range
|
title: Every property declares its domain and range
|
||||||
statement: >
|
statement: >
|
||||||
rdfs:domain and rdfs:range are mandatory on every property, EXCEPT where the
|
rdfs:domain and rdfs:range are mandatory on every property, EXCEPT where the
|
||||||
property is deliberately polymorphic. A polymorphic property states so in
|
property is deliberately polymorphic. A polymorphic property declares
|
||||||
its comment and has its scope declared in SHACL. Silent absence is a
|
pr:polymorphic true and has its scope declared in SHACL. Silent absence is a
|
||||||
violation; documented absence is not.
|
violation; declared absence is not.
|
||||||
scope: [object_property, datatype_property]
|
scope: [object_property, datatype_property]
|
||||||
severity: BLOCKING
|
severity: BLOCKING
|
||||||
control: {tier: [script, human], executor: check_tbox_naming.py, procedure: null}
|
control: {tier: [script, human], executor: check_tbox_naming.py, procedure: null}
|
||||||
@@ -601,30 +611,51 @@ rules:
|
|||||||
properties are polymorphic by design, their scope controlled class by class
|
properties are polymorphic by design, their scope controlled class by class
|
||||||
in SHACL rather than by twin properties; giving those an rdfs:domain would
|
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
|
trigger the RDFS retyping described in TN-028. The rule therefore separates
|
||||||
the two cases rather than demanding a domain everywhere.
|
the two cases rather than demanding a domain everywhere. The distinction is
|
||||||
|
carried by an axiom and not by a sentence in the comment: a checker looking
|
||||||
|
for a phrase in prose fails the moment someone words it differently, and
|
||||||
|
what matters is declared rather than read, exactly as for abstractness.
|
||||||
examples:
|
examples:
|
||||||
- {from: a polymorphic property with no domain and no comment, to: violation, note: silence is indistinguishable from omission}
|
- {from: a property with no domain and no declaration, to: violation, note: silence is indistinguishable from omission}
|
||||||
- {from: a polymorphic property with no domain, documented, to: conforming, note: scope declared in SHACL}
|
- {from: "a property with no domain, declaring pr:polymorphic true", to: conforming, note: scope declared in SHACL}
|
||||||
|
|
||||||
- id: TN-026
|
- id: TN-026
|
||||||
category: Declaration
|
category: Declaration
|
||||||
title: Every term declares how it was authored
|
title: Every concrete class declares how its instances are produced
|
||||||
statement: >
|
statement: >
|
||||||
pr:authoringMode is mandatory on every term. pr:harvestSource is mandatory
|
Every CONCRETE CLASS declares pr:authoringMode, stating whether its
|
||||||
if and only if the mode is HARVESTED.
|
instances are ASSERTED by governance or HARVESTED from a system.
|
||||||
scope: [class, object_property, datatype_property, annotation_property]
|
pr:harvestSource is mandatory if and only if the mode is HARVESTED. The
|
||||||
|
declared mode agrees with the provenance axis the class descends from: a
|
||||||
|
class under CapturedObject declares HARVESTED, one under DefinedObject
|
||||||
|
declares ASSERTED. Nothing else carries the property: not an abstract class,
|
||||||
|
not a property, not a term of the vocabulary in its own right.
|
||||||
|
scope: [class]
|
||||||
severity: BLOCKING
|
severity: BLOCKING
|
||||||
control: {tier: script, executor: check_tbox_naming.py, procedure: null}
|
control: {tier: script, executor: check_tbox_naming.py, procedure: null}
|
||||||
filiation: null
|
filiation: null
|
||||||
rationale: >
|
rationale: >
|
||||||
Provenance decides who may edit a term and what a divergence means. A term
|
A term of the vocabulary is declared, always and by definition; recording
|
||||||
stating where its data comes from without stating that it is harvested, or
|
that on every term would repeat one fact a hundred times and say nothing. It
|
||||||
declaring itself harvested without naming a source, is half-declared in a
|
is the INSTANCES that have a provenance, and the class is where the model
|
||||||
way no control can catch. Declared symmetrically, provenance also makes a
|
states which one: a governance object is asserted by a domain, a physical
|
||||||
harvester specifiable from the model itself rather than from a side
|
column is harvested from a system. The distinction matters because a class
|
||||||
document.
|
can be declared and describe captured things at once — CapturedObject itself
|
||||||
|
is a declared term naming harvested instances, and conflating the two is
|
||||||
|
what once put a harvesting mode on an abstraction.
|
||||||
|
|
||||||
|
Nothing is inherited. A subclass declares its own mode rather than taking
|
||||||
|
its parent's, so that the declaration is read where the instances are typed
|
||||||
|
and not chased up a chain. The agreement clause is what keeps that free of
|
||||||
|
contradiction: the axis says what kind of thing the class describes, the
|
||||||
|
mode says how those things arrive, and a class under CapturedObject
|
||||||
|
declaring ASSERTED is stating both at once.
|
||||||
|
|
||||||
|
Declared this way, provenance also makes a harvester specifiable from the
|
||||||
|
model itself rather than from a side document.
|
||||||
examples:
|
examples:
|
||||||
- {from: harvestSource present, authoringMode absent, to: "authoringMode HARVESTED", note: null}
|
- {from: "authoringMode on an abstract class", to: nothing, note: an abstraction has no instances to produce}
|
||||||
|
- {from: "a class under CapturedObject declaring ASSERTED", to: violation, note: the mode contradicts the axis}
|
||||||
- {from: "authoringMode HARVESTED, harvestSource absent", to: harvestSource declared, note: null}
|
- {from: "authoringMode HARVESTED, harvestSource absent", to: harvestSource declared, note: null}
|
||||||
|
|
||||||
- id: TN-027
|
- id: TN-027
|
||||||
@@ -1026,20 +1057,51 @@ abstractness:
|
|||||||
- {term: SystemType, is_abstract: false, note: "must stay concrete — it types five individuals"}
|
- {term: SystemType, is_abstract: false, note: "must stay concrete — it types five individuals"}
|
||||||
|
|
||||||
# ====== ANNEX: DISPLAY (TN-011, TN-022) ======
|
# ====== 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:
|
display:
|
||||||
- {iri: DataDomain, label: "Data Domain", short_label: "Domain", acronym: "DD", note: "DD is already the short form used inside A-Box identifiers under NR-014"}
|
- {iri: MetaModelObject, label: "Meta Model Object", short_label: null, acronym: null, note: null}
|
||||||
- {iri: DataSubDomain, label: "Data Sub Domain", short_label: "Sub Domain", acronym: "SD", note: "no hyphen"}
|
- {iri: DefinedObject, label: "Defined Object", short_label: null, acronym: null, note: null}
|
||||||
- {iri: DataDomainOwner, label: "Data Domain Owner", short_label: "Domain Owner", acronym: "DDO", note: null}
|
- {iri: CapturedObject, label: "Captured Object", short_label: null, acronym: null, note: null}
|
||||||
- {iri: DataSubDomainOwner, label: "Data Sub Domain Owner", short_label: "Sub Domain Owner", acronym: "SDO", note: null}
|
- {iri: OwnershipLayerObject, label: "Ownership Layer Object", short_label: "Ownership Layer", acronym: null, note: "the categorisation role moves into rdfs:comment"}
|
||||||
- {iri: DataProductOwner, label: "Data Product Owner", short_label: "Product Owner", acronym: "PO", note: "NOT DPO — the initialism is taken by Data Protection Officer"}
|
- {iri: BusinessLayerObject, label: "Business Layer Object", short_label: "Business Layer", acronym: null, note: null}
|
||||||
- {iri: DataSteward, label: "Data Steward", short_label: "Steward", acronym: null, note: null}
|
- {iri: LogicalLayerObject, label: "Logical Layer Object", short_label: "Logical Layer", acronym: null, note: null}
|
||||||
- {iri: DataGovernanceLead, label: "Data Governance Lead", short_label: "Governance Lead", acronym: "DGL", note: null}
|
- {iri: PhysicalLayerObject, label: "Physical Layer Object", short_label: "Physical Layer", acronym: null, note: null}
|
||||||
- {iri: DataElement, label: "Data Element", short_label: "Element", acronym: null, note: "drives the name of hasElement and hasGrainElement"}
|
- {iri: DeliveryLayerObject, label: "Delivery Layer Object", short_label: "Delivery Layer", acronym: null, note: null}
|
||||||
- {iri: BusinessConcept, label: "Business Concept", short_label: "Concept", acronym: null, note: "drives the name of usesConcept"}
|
- {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: 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: 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: 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: 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: 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: BusinessIntelligenceReport, label: "Business Intelligence Report", short_label: "BI Report", acronym: null, note: null}
|
||||||
- {iri: OwnershipLayerObject, label: "Ownership Layer Object", short_label: null, acronym: null, note: "the categorisation role moves into rdfs:comment"}
|
- {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}
|
||||||
|
|||||||
+2768
-2111
File diff suppressed because it is too large
Load Diff
+882
-1032
File diff suppressed because it is too large
Load Diff
@@ -1,519 +0,0 @@
|
|||||||
@prefix sh: <http://www.w3.org/ns/shacl#> .
|
|
||||||
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
|
|
||||||
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
|
|
||||||
@prefix dcterms: <http://purl.org/dc/terms/> .
|
|
||||||
@prefix pr: <https://ontology.pernod-ricard.com/metamodel/> .
|
|
||||||
@prefix prs: <https://ontology.pernod-ricard.com/metamodel/shapes/> .
|
|
||||||
|
|
||||||
#################################################################
|
|
||||||
# PR DATA META MODEL - SHACL SHAPES v0.2
|
|
||||||
# Aligned with T-Box v1.1 and Rulebook v0.3.x
|
|
||||||
#
|
|
||||||
# WHAT CHANGED vs v0.1.1
|
|
||||||
# ----------------------
|
|
||||||
# 1. SPLIT BY PROVENANCE. v0.1.1 targeted pr:MetaModelObject and demanded an
|
|
||||||
# identifier, a name, an owner, an owning domain, a status and a version on
|
|
||||||
# everything. Applied to the captured half of the graph that would fail on
|
|
||||||
# every one of ~50 000 Snowflake columns. Defined and Captured objects now
|
|
||||||
# have separate shapes with separate expectations.
|
|
||||||
#
|
|
||||||
# 2. SCOPE FOR THE UNIFIED RELATIONS. belongsTo and represents deliberately
|
|
||||||
# carry no rdfs:domain/range in the T-Box, because each serves two classes
|
|
||||||
# with different targets. The scope lives here instead, per class.
|
|
||||||
#
|
|
||||||
# 3. MEANING IS AN XOR, NOT A CHECKLIST. A Data Element reaches business
|
|
||||||
# meaning by exactly one route. sh:xone rejects both zero routes and two.
|
|
||||||
#
|
|
||||||
# 4. NO COVERAGE RULES. Nothing here requires a Business Concept to be
|
|
||||||
# realized, or a Business Object to hold N concepts. That was BR-004, and
|
|
||||||
# encoding a coverage expectation as a blocking constraint does not produce
|
|
||||||
# coverage -- it produces padding. Coverage is a dashboard metric.
|
|
||||||
#
|
|
||||||
# 5. PORTABLE REGEX. No lookbehind: sh:pattern is normatively XPath 2.0
|
|
||||||
# regex, which does not support it. Written as sh:not [ sh:pattern ... ].
|
|
||||||
#
|
|
||||||
# SEVERITY MAPPING
|
|
||||||
# sh:Violation -> BLOCKING, cannot reach PUBLISHED, fails CI (DQ-011)
|
|
||||||
# sh:Warning -> MAJOR, must be reviewed, does not block
|
|
||||||
# sh:Info -> GUIDELINE, informational
|
|
||||||
#################################################################
|
|
||||||
|
|
||||||
<https://ontology.pernod-ricard.com/metamodel/shapes/>
|
|
||||||
dcterms:title "PR Data Meta Model - SHACL Shapes" ;
|
|
||||||
dcterms:modified "2026-07-30"^^xsd:date ;
|
|
||||||
rdfs:comment "Executable form of the rulebook. Validated by scripts/run_shacl_validation.py." .
|
|
||||||
|
|
||||||
|
|
||||||
#################################################################
|
|
||||||
# IDENTITY AND LIFECYCLE -- defined objects only
|
|
||||||
#################################################################
|
|
||||||
|
|
||||||
prs:DefinedObjectShape a sh:NodeShape ;
|
|
||||||
sh:targetClass pr:DefinedObject ;
|
|
||||||
rdfs:comment "Applies to everything governance writes. Captured objects are deliberately exempt: see CapturedObjectShape." ;
|
|
||||||
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:identifier ; sh:datatype xsd:string ;
|
|
||||||
sh:minCount 1 ; sh:maxCount 1 ;
|
|
||||||
sh:severity sh:Violation ;
|
|
||||||
sh:message "DQ-001 -- every defined object carries exactly one identifier. NOTE: uniqueness across the graph is NOT tested here; SHACL checks cardinality only. The reconciliation job tests uniqueness." ] ;
|
|
||||||
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:name ; sh:datatype xsd:string ;
|
|
||||||
sh:minCount 1 ; sh:maxCount 1 ; sh:minLength 3 ;
|
|
||||||
sh:severity sh:Violation ;
|
|
||||||
sh:message "Every defined object carries exactly one canonical name." ] ;
|
|
||||||
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:status ; sh:minCount 1 ;
|
|
||||||
sh:in ( "DRAFT" "UNDER_REVIEW" "PUBLISHED" "DEPRECATED" "RETIRED" ) ;
|
|
||||||
sh:severity sh:Violation ;
|
|
||||||
sh:message "LC-001 -- status must be one of DRAFT, UNDER_REVIEW, PUBLISHED, DEPRECATED, RETIRED." ] ;
|
|
||||||
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:owningDomain ; sh:maxCount 1 ;
|
|
||||||
sh:severity sh:Violation ;
|
|
||||||
sh:message "OW-001 -- at most one owning Data Domain. On a Data Element this may be re-declared to differ from the parent chain: that marks a borrowed element." ] ;
|
|
||||||
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:version ; sh:maxCount 1 ;
|
|
||||||
sh:severity sh:Warning ;
|
|
||||||
sh:message "A published object should carry a version." ] .
|
|
||||||
|
|
||||||
|
|
||||||
prs:PublishedShape a sh:NodeShape ;
|
|
||||||
sh:targetClass pr:DefinedObject ;
|
|
||||||
rdfs:comment "Extra demands that only bite once an object claims to be PUBLISHED. Keeping these off the base shape is what lets a domain be documented progressively." ;
|
|
||||||
|
|
||||||
sh:sparql [
|
|
||||||
sh:severity sh:Violation ;
|
|
||||||
sh:message "OW-007 -- an object flagged TO_ARBITRATE cannot be PUBLISHED. Resolve at the Cross-Domain Council first." ;
|
|
||||||
sh:select """
|
|
||||||
PREFIX pr: <https://ontology.pernod-ricard.com/metamodel/>
|
|
||||||
SELECT $this WHERE {
|
|
||||||
$this pr:status "PUBLISHED" ;
|
|
||||||
pr:arbitrationStatus "TO_ARBITRATE" .
|
|
||||||
}""" ] ;
|
|
||||||
|
|
||||||
sh:sparql [
|
|
||||||
sh:severity sh:Violation ;
|
|
||||||
sh:message "OW-003 -- a PUBLISHED object must have an owning Data Domain." ;
|
|
||||||
sh:select """
|
|
||||||
PREFIX pr: <https://ontology.pernod-ricard.com/metamodel/>
|
|
||||||
SELECT $this WHERE {
|
|
||||||
$this pr:status "PUBLISHED" .
|
|
||||||
FILTER NOT EXISTS { $this pr:owningDomain ?d }
|
|
||||||
}""" ] .
|
|
||||||
|
|
||||||
|
|
||||||
#################################################################
|
|
||||||
# OBSERVED OBJECTS -- captured, never hand-edited
|
|
||||||
#################################################################
|
|
||||||
|
|
||||||
prs:CapturedObjectShape a sh:NodeShape ;
|
|
||||||
sh:targetClass pr:CapturedObject ;
|
|
||||||
rdfs:comment "No identifier, no name, no owner, no status, no version. A harvested column is attributed through the Data Element mapped to it, never asserted directly. What it must carry is enough to be re-found on the next run." ;
|
|
||||||
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:sourceIdentifier ; sh:datatype xsd:string ;
|
|
||||||
sh:maxCount 1 ;
|
|
||||||
sh:severity sh:Warning ;
|
|
||||||
sh:message "A native immutable source id makes reconciliation idempotent and survives renames, which names do not." ] ;
|
|
||||||
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:harvestedOn ; sh:datatype xsd:dateTime ;
|
|
||||||
sh:minCount 1 ; sh:maxCount 1 ;
|
|
||||||
sh:severity sh:Violation ;
|
|
||||||
sh:message "Every captured object records when it was harvested. Without it there is no way to tell a current fact from a stale one." ] ;
|
|
||||||
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:owningDomain ; sh:maxCount 0 ;
|
|
||||||
sh:severity sh:Warning ;
|
|
||||||
sh:message "Captured objects are not attributed directly -- ownership flows from the Data Element mapped to them." ] .
|
|
||||||
|
|
||||||
|
|
||||||
# inSchema is deliberately NOT required anywhere. Neo4j has no schema and
|
|
||||||
# BigQuery calls the level a dataset; a minCount 1 here would break on the
|
|
||||||
# first harvest outside Snowflake.
|
|
||||||
prs:DataStructureShape a sh:NodeShape ;
|
|
||||||
sh:targetClass pr:DataStructure ;
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:inSchema ; sh:maxCount 1 ;
|
|
||||||
sh:severity sh:Info ;
|
|
||||||
sh:message "Optional on purpose: not every engine has a schema level." ] ;
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:sourcedFrom ; sh:maxCount 1 ;
|
|
||||||
sh:severity sh:Info ] .
|
|
||||||
|
|
||||||
|
|
||||||
prs:FieldShape a sh:NodeShape ;
|
|
||||||
sh:targetClass pr:Field ;
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:fullyQualifiedName ; sh:datatype xsd:string ;
|
|
||||||
sh:minCount 1 ; sh:maxCount 1 ;
|
|
||||||
sh:pattern "^[A-Za-z0-9_$]+\\\\.[A-Za-z0-9_$]+\\\\.[A-Za-z0-9_$]+\\\\.[A-Za-z0-9_$]+$" ;
|
|
||||||
sh:severity sh:Violation ;
|
|
||||||
sh:message "A Field carries DATABASE.SCHEMA.RELATION.COLUMN. A bare column name is ambiguous as soon as it exists in two tables, so the FQN is the real join key against INFORMATION_SCHEMA." ] ;
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:livesIn ; sh:minCount 1 ; sh:maxCount 1 ;
|
|
||||||
sh:severity sh:Violation ;
|
|
||||||
sh:message "Every Field lives in exactly one Physical Relation." ] .
|
|
||||||
|
|
||||||
|
|
||||||
#################################################################
|
|
||||||
# CATEGORIZATION TREE -- scope of the unified belongsTo
|
|
||||||
#################################################################
|
|
||||||
|
|
||||||
prs:SubDomainShape a sh:NodeShape ;
|
|
||||||
sh:targetClass pr:SubDomain ;
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:belongsTo ; sh:class pr:DataDomain ;
|
|
||||||
sh:minCount 1 ; sh:maxCount 1 ;
|
|
||||||
sh:severity sh:Violation ;
|
|
||||||
sh:message "A Sub-Domain belongs to exactly one Data Domain." ] ;
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:hasSubDomainOwner ; sh:maxCount 1 ;
|
|
||||||
sh:severity sh:Warning ;
|
|
||||||
sh:message "A Sub-Domain should name its owner." ] .
|
|
||||||
|
|
||||||
|
|
||||||
prs:DataDomainShape a sh:NodeShape ;
|
|
||||||
sh:targetClass pr:DataDomain ;
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:hasDomainOwner ; sh:maxCount 1 ;
|
|
||||||
sh:severity sh:Warning ;
|
|
||||||
sh:message "A Data Domain should name its owner." ] ;
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:hasDGL ; sh:maxCount 1 ;
|
|
||||||
sh:severity sh:Warning ;
|
|
||||||
sh:message "A Data Domain should name its Data Governance Lead." ] ;
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:hasActivationStatus ; sh:maxCount 1 ;
|
|
||||||
sh:severity sh:Info ] .
|
|
||||||
|
|
||||||
|
|
||||||
#################################################################
|
|
||||||
# BUSINESS OBJECT -- subject, perimeter, stewardship
|
|
||||||
#################################################################
|
|
||||||
|
|
||||||
prs:BusinessObjectShape a sh:NodeShape ;
|
|
||||||
sh:targetClass pr:BusinessObject ;
|
|
||||||
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:belongsTo ; sh:class pr:SubDomain ;
|
|
||||||
sh:minCount 1 ; sh:maxCount 1 ;
|
|
||||||
sh:severity sh:Violation ;
|
|
||||||
sh:message "A Business Object belongs to exactly one Sub-Domain." ] ;
|
|
||||||
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:aboutConcept ; sh:class pr:BusinessConcept ;
|
|
||||||
sh:minCount 1 ; sh:maxCount 1 ;
|
|
||||||
sh:severity sh:Violation ;
|
|
||||||
sh:message "BR-004 -- a Business Object is about exactly one Business Concept. Replaces the old 3..10 minimum, which was satisfied in v0.6 by attaching unrelated concepts. If the subject cannot be named, the object is not scoped." ] ;
|
|
||||||
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:usesConcept ; sh:class pr:BusinessConcept ;
|
|
||||||
sh:severity sh:Info ;
|
|
||||||
sh:message "Other notions needed to read this object. Zero is a legitimate answer." ] ;
|
|
||||||
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:monitoredBy ; sh:class pr:DataSteward ;
|
|
||||||
sh:minCount 1 ;
|
|
||||||
sh:severity sh:Warning ;
|
|
||||||
sh:message "The Business Object is where a steward is declared -- Data Objects and Data Elements inherit from here." ] ;
|
|
||||||
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:businessDefinition ; sh:maxCount 0 ;
|
|
||||||
sh:severity sh:Violation ;
|
|
||||||
sh:message "A Business Object carries no definition of its own: meaning belongs to the Business Concept named by aboutConcept. Two definitions for one notion are two definitions free to diverge. MIGRATION: move the text to that Concept." ] ;
|
|
||||||
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:sourcedFrom ; sh:maxCount 0 ;
|
|
||||||
sh:severity sh:Violation ;
|
|
||||||
sh:message "A Business Object has no system of truth. It is a governance grouping, not data: its data lives in the Data Objects that represent it, and those name the system. In v0.6 this property held a grab-bag on the 15 objects -- physical locations, migration notes, arbitration remarks -- all under one name." ] .
|
|
||||||
|
|
||||||
|
|
||||||
#################################################################
|
|
||||||
# BUSINESS CONCEPT AND METRIC
|
|
||||||
#################################################################
|
|
||||||
|
|
||||||
prs:BusinessConceptShape a sh:NodeShape ;
|
|
||||||
sh:targetClass pr:BusinessConcept ;
|
|
||||||
rdfs:comment "Note what is NOT here: no requirement that a Concept be realized by any Data Element or measured by any Metric. The business layer is allowed to run ahead of the data. Coverage is a dashboard metric, not a rule." ;
|
|
||||||
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:businessDefinition ; sh:datatype xsd:string ;
|
|
||||||
sh:minCount 1 ; sh:maxCount 1 ; sh:minLength 20 ;
|
|
||||||
sh:severity sh:Violation ;
|
|
||||||
sh:message "A Business Concept exists to carry meaning: it must hold exactly one business definition of at least 20 characters." ] ;
|
|
||||||
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:formula ; sh:maxCount 0 ;
|
|
||||||
sh:severity sh:Violation ;
|
|
||||||
sh:message "BR-003 -- the presence of a formula is what makes something a Metric. A Concept with a formula is a misclassified Metric." ] ;
|
|
||||||
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:sourcedFrom ; sh:maxCount 0 ;
|
|
||||||
sh:severity sh:Violation ;
|
|
||||||
sh:message "A Business Concept has no system of truth. Its authority is its owning domain and its definition belongs in the glossary. Naming a system here would ask which of the systems that USE the concept defines it -- a question with no answer." ] .
|
|
||||||
|
|
||||||
|
|
||||||
prs:MetricShape a sh:NodeShape ;
|
|
||||||
sh:targetClass pr:Metric ;
|
|
||||||
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:formula ; sh:datatype xsd:string ;
|
|
||||||
sh:minCount 1 ; sh:maxCount 1 ;
|
|
||||||
sh:severity sh:Violation ;
|
|
||||||
sh:message "BR-003 -- a Metric is a measurement with a calculation rule. No formula, no Metric." ] ;
|
|
||||||
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:unit ; sh:minCount 1 ;
|
|
||||||
sh:severity sh:Violation ;
|
|
||||||
sh:message "A Metric states every unit its variants are expressed in. NOT capped at one: Sell Out Volume is computed in 9L and in L, Sell Out Value in EUR, USD and local currency. The single-unit rule belongs on the Data Element, where it holds -- see DataElementShape." ] ;
|
|
||||||
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:measures ; sh:class pr:BusinessConcept ;
|
|
||||||
sh:minCount 1 ;
|
|
||||||
sh:severity sh:Violation ;
|
|
||||||
sh:message "A Metric measures at least one Business Concept -- this is how a measure element reaches business meaning." ] ;
|
|
||||||
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:hasGranularity ; sh:maxCount 0 ;
|
|
||||||
sh:severity sh:Warning ;
|
|
||||||
sh:message "Grain moved off the Metric in v1.1. A Metric is a calculation rule and has no rows; grain belongs to the Data Object, stated with hasGrainElement and checkable against the real primary key." ] ;
|
|
||||||
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:physicalName ; sh:maxCount 0 ;
|
|
||||||
sh:severity sh:Violation ;
|
|
||||||
sh:message "A Metric must not carry a physical name -- that is a layer leak. Under BR-013 the physical name belongs on the Data Element." ] .
|
|
||||||
|
|
||||||
|
|
||||||
# The source of truth of a Metric is asserted by exception, then checked.
|
|
||||||
# "Where can I get this metric" is a first-order question and the derivation
|
|
||||||
# chain will have gaps before harvesting, so the assertion stands on its own --
|
|
||||||
# but it must agree with the elements that implement the metric, or the two
|
|
||||||
# will drift the day an element moves.
|
|
||||||
prs:MetricSourceShape a sh:NodeShape ;
|
|
||||||
sh:targetClass pr:Metric ;
|
|
||||||
rdfs:comment "Rewritten in v1.4 to compare System references rather than strings. A string comparison would have called 'SODH Gold' and 'Snowflake (SODH Gold)' two different systems." ;
|
|
||||||
sh:sparql [
|
|
||||||
sh:severity sh:Violation ;
|
|
||||||
sh:message "A Metric's system of truth must match that of at least one element computing it. If they disagree, either the metric points at the wrong system or an element has moved." ;
|
|
||||||
sh:select """
|
|
||||||
PREFIX pr: <https://ontology.pernod-ricard.com/metamodel/>
|
|
||||||
SELECT $this WHERE {
|
|
||||||
$this pr:sourcedFrom ?sys ; pr:computedBy ?de .
|
|
||||||
FILTER NOT EXISTS {
|
|
||||||
$this pr:computedBy ?any .
|
|
||||||
?any pr:sourcedFrom ?sys .
|
|
||||||
}
|
|
||||||
}""" ] ;
|
|
||||||
sh:sparql [
|
|
||||||
sh:severity sh:Warning ;
|
|
||||||
sh:message "Elements computing one Metric come from several systems. Legitimate for a metric blending sources, worth a look otherwise." ;
|
|
||||||
sh:select """
|
|
||||||
PREFIX pr: <https://ontology.pernod-ricard.com/metamodel/>
|
|
||||||
SELECT $this WHERE {
|
|
||||||
$this pr:computedBy ?a , ?b .
|
|
||||||
?a pr:sourcedFrom ?sa .
|
|
||||||
?b pr:sourcedFrom ?sb .
|
|
||||||
FILTER(?sa != ?sb)
|
|
||||||
}""" ] .
|
|
||||||
|
|
||||||
|
|
||||||
# BR-013, finally executable. v0.1.1 declared it BLOCKING with no shape.
|
|
||||||
prs:MotherMetricShape a sh:NodeShape ;
|
|
||||||
sh:targetClass pr:Metric ;
|
|
||||||
sh:sparql [
|
|
||||||
sh:severity sh:Warning ;
|
|
||||||
sh:message "BR-013 -- a PUBLISHED Metric should be implemented by at least one Data Element through computedBy. A metric no element computes is a definition with nothing behind it." ;
|
|
||||||
sh:select """
|
|
||||||
PREFIX pr: <https://ontology.pernod-ricard.com/metamodel/>
|
|
||||||
SELECT $this WHERE {
|
|
||||||
$this pr:status "PUBLISHED" .
|
|
||||||
FILTER NOT EXISTS { $this pr:computedBy ?de }
|
|
||||||
}""" ] .
|
|
||||||
|
|
||||||
|
|
||||||
#################################################################
|
|
||||||
# DATA OBJECT -- the pivot, and grain
|
|
||||||
#################################################################
|
|
||||||
|
|
||||||
prs:DataObjectShape a sh:NodeShape ;
|
|
||||||
sh:targetClass pr:DataObject ;
|
|
||||||
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:represents ; sh:class pr:BusinessObject ;
|
|
||||||
sh:minCount 1 ; sh:maxCount 1 ;
|
|
||||||
sh:severity sh:Violation ;
|
|
||||||
sh:message "BR-006 -- a Data Object represents exactly one Business Object. This is the pivot: the single junction between governance and technology." ] ;
|
|
||||||
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:hasElement ; sh:class pr:DataElement ;
|
|
||||||
sh:minCount 1 ;
|
|
||||||
sh:severity sh:Violation ;
|
|
||||||
sh:message "A Data Object holds at least one Data Element." ] ;
|
|
||||||
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:hasGrainElement ; sh:class pr:DataElement ;
|
|
||||||
sh:severity sh:Warning ;
|
|
||||||
sh:message "State the grain: the elements whose combination makes a row unique. Checkable against the harvested primary key." ] ;
|
|
||||||
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:monitoredBy ; sh:maxCount 0 ;
|
|
||||||
sh:severity sh:Warning ;
|
|
||||||
sh:message "Stewardship is declared on the Business Object and inherited through represents. Declaring it here allows two answers to the same question." ] ;
|
|
||||||
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:sourcedFrom ; sh:class pr:System ; sh:maxCount 1 ;
|
|
||||||
sh:severity sh:Warning ;
|
|
||||||
sh:message "Name the System holding the authoritative data, as a reference rather than a string. A string could not answer 'what depends on MDM Product'." ] ;
|
|
||||||
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:storedIn ; sh:class pr:DataStructure ;
|
|
||||||
sh:severity sh:Warning ;
|
|
||||||
sh:message "Where the object actually sits: a table, a view, later a node label. The seam between what governance defines and what a harvester captures -- proposed by the reconciliation job, confirmed by a steward." ] ;
|
|
||||||
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:materializedAs ; sh:maxCount 0 ;
|
|
||||||
sh:severity sh:Warning ;
|
|
||||||
sh:message "Deprecated in v1.4: storedIn now serves both granularities." ] .
|
|
||||||
|
|
||||||
|
|
||||||
# A declared grain element must actually belong to the object it grains.
|
|
||||||
prs:GrainConsistencyShape a sh:NodeShape ;
|
|
||||||
sh:targetClass pr:DataObject ;
|
|
||||||
sh:sparql [
|
|
||||||
sh:severity sh:Violation ;
|
|
||||||
sh:message "A grain element must be one of the object's own Data Elements." ;
|
|
||||||
sh:select """
|
|
||||||
PREFIX pr: <https://ontology.pernod-ricard.com/metamodel/>
|
|
||||||
SELECT $this ?grain WHERE {
|
|
||||||
$this pr:hasGrainElement ?grain .
|
|
||||||
FILTER NOT EXISTS { $this pr:hasElement ?grain }
|
|
||||||
}""" ] .
|
|
||||||
|
|
||||||
|
|
||||||
#################################################################
|
|
||||||
# DATA ELEMENT -- the XOR of meaning
|
|
||||||
#################################################################
|
|
||||||
|
|
||||||
prs:DataElementShape a sh:NodeShape ;
|
|
||||||
sh:targetClass pr:DataElement ;
|
|
||||||
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:physicalName ; sh:datatype xsd:string ;
|
|
||||||
sh:maxCount 1 ;
|
|
||||||
sh:severity sh:Warning ;
|
|
||||||
sh:message "NR-009 -- the physical name is recorded verbatim and never altered." ] ;
|
|
||||||
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:fullyQualifiedName ; sh:maxCount 1 ;
|
|
||||||
sh:severity sh:Info ] ;
|
|
||||||
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:unit ; sh:maxCount 1 ;
|
|
||||||
sh:severity sh:Violation ;
|
|
||||||
sh:message "A Data Element carries exactly one unit. This is the counterpart of the Metric rule: the mother metric lists the units its variants use, each variant states the single unit it is actually expressed in. An element with two units is two elements." ] ;
|
|
||||||
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:represents ; sh:class pr:BusinessConcept ;
|
|
||||||
sh:maxCount 1 ;
|
|
||||||
sh:severity sh:Violation ;
|
|
||||||
sh:message "A Data Element represents at most one Business Concept. If a column carries two business notions, it should almost certainly be split -- so the case where a steward cannot choose is exactly the one worth looking at." ] ;
|
|
||||||
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:monitoredBy ; sh:maxCount 0 ;
|
|
||||||
sh:severity sh:Warning ;
|
|
||||||
sh:message "Stewardship is inherited from the Business Object, not declared here." ] .
|
|
||||||
|
|
||||||
|
|
||||||
# The core rule of v1.1.
|
|
||||||
prs:DataElementMeaningShape a sh:NodeShape ;
|
|
||||||
sh:targetClass pr:DataElement ;
|
|
||||||
rdfs:comment "A Data Element reaches business meaning by exactly one of two routes: it represents a Business Concept (dimensional), or a Metric is computedBy it (measure). sh:xone rejects zero routes and two alike. A measure element must NOT also represent a Concept: the chain Metric -> measures -> Concept already carries the meaning, and carries more, since it names which metric it travels through." ;
|
|
||||||
sh:xone (
|
|
||||||
[ sh:property [ sh:path pr:represents ;
|
|
||||||
sh:minCount 1 ; sh:maxCount 1 ] ]
|
|
||||||
[ sh:property [ sh:path [ sh:inversePath pr:computedBy ] ;
|
|
||||||
sh:minCount 1 ] ]
|
|
||||||
) ;
|
|
||||||
sh:severity sh:Violation ;
|
|
||||||
sh:message "A Data Element must reach business meaning by exactly one route: represents a Business Concept (dimensional), or be computedBy a Metric (measure). Never both, never neither. A defined column with no business meaning is debt, not an acceptable state." .
|
|
||||||
|
|
||||||
|
|
||||||
#################################################################
|
|
||||||
# DELIVERY
|
|
||||||
#################################################################
|
|
||||||
|
|
||||||
prs:DataProductShape a sh:NodeShape ;
|
|
||||||
sh:targetClass pr:DataProduct ;
|
|
||||||
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:operatedBy ; sh:class pr:SubDomain ;
|
|
||||||
sh:minCount 1 ; sh:maxCount 1 ;
|
|
||||||
sh:severity sh:Violation ;
|
|
||||||
sh:message "A Data Product is operated by exactly one Sub-Domain. It spans several Business Objects and cannot inherit accountability, so it declares its own -- it is a consumable, and there must be no ambiguity about who runs it." ] ;
|
|
||||||
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:hasProductOwner ; sh:class pr:DataProductOwner ;
|
|
||||||
sh:maxCount 1 ;
|
|
||||||
sh:severity sh:Warning ;
|
|
||||||
sh:message "A Data Product should name exactly one Product Owner. A warning rather than a block: the SODH owner is genuinely not appointed, and a blocking rule that cannot be satisfied truthfully is how BR-004 produced padding." ] ;
|
|
||||||
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:packages ; sh:class pr:DataObject ;
|
|
||||||
sh:minCount 1 ;
|
|
||||||
sh:severity sh:Violation ;
|
|
||||||
sh:message "A Data Product packages at least one Data Object. The Business Objects it makes available are derived from these, which is why no direct link to a Business Object is needed." ] ;
|
|
||||||
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:governedBy ; sh:maxCount 1 ;
|
|
||||||
sh:severity sh:Warning ;
|
|
||||||
sh:message "A Data Product should be governed by a Data Contract." ] ;
|
|
||||||
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:exposes ; sh:class pr:DataInterface ;
|
|
||||||
sh:severity sh:Info ;
|
|
||||||
sh:message "The channels through which consumers actually get the data." ] .
|
|
||||||
|
|
||||||
|
|
||||||
prs:DataInterfaceShape a sh:NodeShape ;
|
|
||||||
sh:targetClass pr:DataInterface ;
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:servedBy ; sh:minCount 1 ;
|
|
||||||
sh:severity sh:Warning ;
|
|
||||||
sh:message "An interface should name what physically backs it. Unlike the old Data Asset test, this is verifiable: a grant, a share or an endpoint either exists or it does not." ] .
|
|
||||||
|
|
||||||
|
|
||||||
#################################################################
|
|
||||||
# CONSUMPTION
|
|
||||||
#################################################################
|
|
||||||
|
|
||||||
prs:BIFieldShape a sh:NodeShape ;
|
|
||||||
sh:targetClass pr:BIField ;
|
|
||||||
rdfs:comment "realizesMetric is a transitional control: asserted directly, with no rule written against the lineage chain. Making the assertion conditional on intermediate mappings existing would create confusion and discourage people from filling it in at all." ;
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:exposesMetric ; sh:class pr:Metric ;
|
|
||||||
sh:maxCount 1 ;
|
|
||||||
sh:severity sh:Info ;
|
|
||||||
sh:message "Which certified Metric this BI field implements. Comparing expressions across fields realizing one Metric is the harmonisation measure." ] .
|
|
||||||
|
|
||||||
|
|
||||||
#################################################################
|
|
||||||
# NAMING -- portable regex, no lookbehind
|
|
||||||
#################################################################
|
|
||||||
|
|
||||||
prs:NamingShape a sh:NodeShape ;
|
|
||||||
sh:targetClass pr:DefinedObject ;
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:name ;
|
|
||||||
sh:not [ sh:pattern "_NM$" ] ;
|
|
||||||
sh:severity sh:Warning ;
|
|
||||||
sh:message "NR-001 -- a canonical name must not end in a physical suffix. Written as sh:not[sh:pattern] rather than a lookbehind: sh:pattern is normatively XPath 2.0 regex, which has no lookbehind, so the v0.1.1 form only worked under pySHACL." ] ;
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:name ;
|
|
||||||
sh:not [ sh:pattern "^[a-z_]+$" ] ;
|
|
||||||
sh:severity sh:Warning ;
|
|
||||||
sh:message "NR-002 -- a canonical name is written for humans, not as a snake_case identifier." ] .
|
|
||||||
+337
-471
@@ -1,305 +1,307 @@
|
|||||||
@prefix sh: <http://www.w3.org/ns/shacl#> .
|
|
||||||
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
|
|
||||||
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
|
|
||||||
@prefix dcterms: <http://purl.org/dc/terms/> .
|
@prefix dcterms: <http://purl.org/dc/terms/> .
|
||||||
|
@prefix owl: <http://www.w3.org/2002/07/owl#> .
|
||||||
@prefix pr: <https://ontology.pernod-ricard.com/metamodel/> .
|
@prefix pr: <https://ontology.pernod-ricard.com/metamodel/> .
|
||||||
@prefix prs: <https://ontology.pernod-ricard.com/metamodel/shapes/> .
|
@prefix prs: <https://ontology.pernod-ricard.com/metamodel/shapes/> .
|
||||||
|
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
|
||||||
|
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
|
||||||
|
@prefix sh: <http://www.w3.org/ns/shacl#> .
|
||||||
|
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
|
||||||
|
|
||||||
#################################################################
|
prs: dcterms:modified "2026-07-30"^^xsd:date ;
|
||||||
# PR DATA META MODEL - SHACL SHAPES v0.2
|
|
||||||
# Aligned with T-Box v1.1 and Rulebook v0.3.x
|
|
||||||
#
|
|
||||||
# WHAT CHANGED vs v0.1.1
|
|
||||||
# ----------------------
|
|
||||||
# 1. SPLIT BY PROVENANCE. v0.1.1 targeted pr:MetaModelObject and demanded an
|
|
||||||
# identifier, a name, an owner, an owning domain, a status and a version on
|
|
||||||
# everything. Applied to the captured half of the graph that would fail on
|
|
||||||
# every one of ~50 000 Snowflake columns. Defined and Captured objects now
|
|
||||||
# have separate shapes with separate expectations.
|
|
||||||
#
|
|
||||||
# 2. SCOPE FOR THE UNIFIED RELATIONS. belongsTo and represents deliberately
|
|
||||||
# carry no rdfs:domain/range in the T-Box, because each serves two classes
|
|
||||||
# with different targets. The scope lives here instead, per class.
|
|
||||||
#
|
|
||||||
# 3. MEANING IS AN XOR, NOT A CHECKLIST. A Data Element reaches business
|
|
||||||
# meaning by exactly one route. sh:xone rejects both zero routes and two.
|
|
||||||
#
|
|
||||||
# 4. NO COVERAGE RULES. Nothing here requires a Business Concept to be
|
|
||||||
# realized, or a Business Object to hold N concepts. That was BR-004, and
|
|
||||||
# encoding a coverage expectation as a blocking constraint does not produce
|
|
||||||
# coverage -- it produces padding. Coverage is a dashboard metric.
|
|
||||||
#
|
|
||||||
# 5. PORTABLE REGEX. No lookbehind: sh:pattern is normatively XPath 2.0
|
|
||||||
# regex, which does not support it. Written as sh:not [ sh:pattern ... ].
|
|
||||||
#
|
|
||||||
# SEVERITY MAPPING
|
|
||||||
# sh:Violation -> BLOCKING, cannot reach PUBLISHED, fails CI (DQ-011)
|
|
||||||
# sh:Warning -> MAJOR, must be reviewed, does not block
|
|
||||||
# sh:Info -> GUIDELINE, informational
|
|
||||||
#################################################################
|
|
||||||
|
|
||||||
<https://ontology.pernod-ricard.com/metamodel/shapes/>
|
|
||||||
dcterms:title "PR Data Meta Model - SHACL Shapes" ;
|
dcterms:title "PR Data Meta Model - SHACL Shapes" ;
|
||||||
dcterms:modified "2026-07-30"^^xsd:date ;
|
|
||||||
rdfs:comment "Executable form of the rulebook. Validated by scripts/run_shacl_validation.py." .
|
rdfs:comment "Executable form of the rulebook. Validated by scripts/run_shacl_validation.py." .
|
||||||
|
|
||||||
|
prs:BIFieldShape a sh:NodeShape ;
|
||||||
#################################################################
|
rdfs:comment "realizesMetric is a transitional control: asserted directly, with no rule written against the lineage chain. Making the assertion conditional on intermediate mappings existing would create confusion and discourage people from filling it in at all." ;
|
||||||
# IDENTITY AND LIFECYCLE -- defined objects only
|
sh:property [ sh:class pr:Metric ;
|
||||||
#################################################################
|
|
||||||
|
|
||||||
prs:DefinedObjectShape a sh:NodeShape ;
|
|
||||||
sh:targetClass pr:DefinedObject ;
|
|
||||||
rdfs:comment "Applies to everything governance writes. Captured objects are deliberately exempt: see CapturedObjectShape." ;
|
|
||||||
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:identifier ; sh:datatype xsd:string ;
|
|
||||||
sh:minCount 1 ; sh:maxCount 1 ;
|
|
||||||
sh:severity sh:Violation ;
|
|
||||||
sh:message "DQ-001 -- every defined object carries exactly one identifier. NOTE: uniqueness across the graph is NOT tested here; SHACL checks cardinality only. The reconciliation job tests uniqueness." ] ;
|
|
||||||
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:name ; sh:datatype xsd:string ;
|
|
||||||
sh:minCount 1 ; sh:maxCount 1 ; sh:minLength 3 ;
|
|
||||||
sh:severity sh:Violation ;
|
|
||||||
sh:message "Every defined object carries exactly one canonical name." ] ;
|
|
||||||
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:status ; sh:minCount 1 ;
|
|
||||||
sh:in ( "DRAFT" "UNDER_REVIEW" "PUBLISHED" "DEPRECATED" "RETIRED" ) ;
|
|
||||||
sh:severity sh:Violation ;
|
|
||||||
sh:message "LC-001 -- status must be one of DRAFT, UNDER_REVIEW, PUBLISHED, DEPRECATED, RETIRED." ] ;
|
|
||||||
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:owningDomain ; sh:maxCount 1 ;
|
|
||||||
sh:severity sh:Violation ;
|
|
||||||
sh:message "OW-001 -- at most one owning Data Domain. On a Data Element this may be re-declared to differ from the parent chain: that marks a borrowed element." ] ;
|
|
||||||
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:version ; sh:maxCount 1 ;
|
|
||||||
sh:severity sh:Warning ;
|
|
||||||
sh:message "A published object should carry a version." ] .
|
|
||||||
|
|
||||||
|
|
||||||
prs:PublishedShape a sh:NodeShape ;
|
|
||||||
sh:targetClass pr:DefinedObject ;
|
|
||||||
rdfs:comment "Extra demands that only bite once an object claims to be PUBLISHED. Keeping these off the base shape is what lets a domain be documented progressively." ;
|
|
||||||
|
|
||||||
sh:sparql [
|
|
||||||
sh:severity sh:Violation ;
|
|
||||||
sh:message "OW-007 -- an object flagged TO_ARBITRATE cannot be PUBLISHED. Resolve at the Cross-Domain Council first." ;
|
|
||||||
sh:select """
|
|
||||||
PREFIX pr: <https://ontology.pernod-ricard.com/metamodel/>
|
|
||||||
SELECT $this WHERE {
|
|
||||||
$this pr:status "PUBLISHED" ;
|
|
||||||
pr:arbitrationStatus "TO_ARBITRATE" .
|
|
||||||
}""" ] ;
|
|
||||||
|
|
||||||
sh:sparql [
|
|
||||||
sh:severity sh:Violation ;
|
|
||||||
sh:message "OW-003 -- a PUBLISHED object must have an owning Data Domain." ;
|
|
||||||
sh:select """
|
|
||||||
PREFIX pr: <https://ontology.pernod-ricard.com/metamodel/>
|
|
||||||
SELECT $this WHERE {
|
|
||||||
$this pr:status "PUBLISHED" .
|
|
||||||
FILTER NOT EXISTS { $this pr:owningDomain ?d }
|
|
||||||
}""" ] .
|
|
||||||
|
|
||||||
|
|
||||||
#################################################################
|
|
||||||
# OBSERVED OBJECTS -- captured, never hand-edited
|
|
||||||
#################################################################
|
|
||||||
|
|
||||||
prs:CapturedObjectShape a sh:NodeShape ;
|
|
||||||
sh:targetClass pr:CapturedObject ;
|
|
||||||
rdfs:comment "No identifier, no name, no owner, no status, no version. A harvested column is attributed through the Data Element mapped to it, never asserted directly. What it must carry is enough to be re-found on the next run." ;
|
|
||||||
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:sourceIdentifier ; sh:datatype xsd:string ;
|
|
||||||
sh:maxCount 1 ;
|
sh:maxCount 1 ;
|
||||||
sh:severity sh:Warning ;
|
sh:message "Which certified Metric this BI field implements. Comparing expressions across fields realizing one Metric is the harmonisation measure." ;
|
||||||
sh:message "A native immutable source id makes reconciliation idempotent and survives renames, which names do not." ] ;
|
sh:path pr:exposesMetric ;
|
||||||
|
sh:severity sh:Info ] ;
|
||||||
sh:property [
|
sh:targetClass pr:BusinessIntelligenceField .
|
||||||
sh:path pr:harvestedOn ; sh:datatype xsd:dateTime ;
|
|
||||||
sh:minCount 1 ; sh:maxCount 1 ;
|
|
||||||
sh:severity sh:Violation ;
|
|
||||||
sh:message "Every captured object records when it was harvested. Without it there is no way to tell a current fact from a stale one." ] ;
|
|
||||||
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:owningDomain ; sh:maxCount 0 ;
|
|
||||||
sh:severity sh:Warning ;
|
|
||||||
sh:message "Captured objects are not attributed directly -- ownership flows from the Data Element mapped to them." ] .
|
|
||||||
|
|
||||||
|
|
||||||
# inSchema is deliberately NOT required anywhere. Neo4j has no schema and
|
|
||||||
# BigQuery calls the level a dataset; a minCount 1 here would break on the
|
|
||||||
# first harvest outside Snowflake.
|
|
||||||
prs:DataStructureShape a sh:NodeShape ;
|
|
||||||
sh:targetClass pr:DataStructure ;
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:inSchema ; sh:maxCount 1 ;
|
|
||||||
sh:severity sh:Info ;
|
|
||||||
sh:message "Optional on purpose: not every engine has a schema level." ] ;
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:sourcedFrom ; sh:maxCount 1 ;
|
|
||||||
sh:severity sh:Info ] .
|
|
||||||
|
|
||||||
|
|
||||||
prs:FieldShape a sh:NodeShape ;
|
|
||||||
sh:targetClass pr:Field ;
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:fullyQualifiedName ; sh:datatype xsd:string ;
|
|
||||||
sh:minCount 1 ; sh:maxCount 1 ;
|
|
||||||
sh:pattern "^[A-Za-z0-9_$]+\\\\.[A-Za-z0-9_$]+\\\\.[A-Za-z0-9_$]+\\\\.[A-Za-z0-9_$]+$" ;
|
|
||||||
sh:severity sh:Violation ;
|
|
||||||
sh:message "A Field carries DATABASE.SCHEMA.RELATION.COLUMN. A bare column name is ambiguous as soon as it exists in two tables, so the FQN is the real join key against INFORMATION_SCHEMA." ] ;
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:livesIn ; sh:minCount 1 ; sh:maxCount 1 ;
|
|
||||||
sh:severity sh:Violation ;
|
|
||||||
sh:message "Every Field lives in exactly one Physical Relation." ] .
|
|
||||||
|
|
||||||
|
|
||||||
#################################################################
|
|
||||||
# CATEGORIZATION TREE -- scope of the unified belongsTo
|
|
||||||
#################################################################
|
|
||||||
|
|
||||||
prs:SubDomainShape a sh:NodeShape ;
|
|
||||||
sh:targetClass pr:SubDomain ;
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:belongsTo ; sh:class pr:DataDomain ;
|
|
||||||
sh:minCount 1 ; sh:maxCount 1 ;
|
|
||||||
sh:severity sh:Violation ;
|
|
||||||
sh:message "A Sub-Domain belongs to exactly one Data Domain." ] ;
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:hasSubDomainOwner ; sh:maxCount 1 ;
|
|
||||||
sh:severity sh:Warning ;
|
|
||||||
sh:message "A Sub-Domain should name its owner." ] .
|
|
||||||
|
|
||||||
|
|
||||||
prs:DataDomainShape a sh:NodeShape ;
|
|
||||||
sh:targetClass pr:DataDomain ;
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:hasDomainOwner ; sh:maxCount 1 ;
|
|
||||||
sh:severity sh:Warning ;
|
|
||||||
sh:message "A Data Domain should name its owner." ] ;
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:hasDGL ; sh:maxCount 1 ;
|
|
||||||
sh:severity sh:Warning ;
|
|
||||||
sh:message "A Data Domain should name its Data Governance Lead." ] ;
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:hasActivationStatus ; sh:maxCount 1 ;
|
|
||||||
sh:severity sh:Info ] .
|
|
||||||
|
|
||||||
|
|
||||||
#################################################################
|
|
||||||
# BUSINESS OBJECT -- subject, perimeter, stewardship
|
|
||||||
#################################################################
|
|
||||||
|
|
||||||
prs:BusinessObjectShape a sh:NodeShape ;
|
|
||||||
sh:targetClass pr:BusinessObject ;
|
|
||||||
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:belongsTo ; sh:class pr:SubDomain ;
|
|
||||||
sh:minCount 1 ; sh:maxCount 1 ;
|
|
||||||
sh:severity sh:Violation ;
|
|
||||||
sh:message "A Business Object belongs to exactly one Sub-Domain." ] ;
|
|
||||||
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:aboutConcept ; sh:class pr:BusinessConcept ;
|
|
||||||
sh:minCount 1 ; sh:maxCount 1 ;
|
|
||||||
sh:severity sh:Violation ;
|
|
||||||
sh:message "BR-004 -- a Business Object is about exactly one Business Concept. Replaces the old 3..10 minimum, which was satisfied in v0.6 by attaching unrelated concepts. If the subject cannot be named, the object is not scoped." ] ;
|
|
||||||
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:usesConcept ; sh:class pr:BusinessConcept ;
|
|
||||||
sh:severity sh:Info ;
|
|
||||||
sh:message "Other notions needed to read this object. Zero is a legitimate answer." ] ;
|
|
||||||
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:monitoredBy ; sh:class pr:DataSteward ;
|
|
||||||
sh:minCount 1 ;
|
|
||||||
sh:severity sh:Warning ;
|
|
||||||
sh:message "The Business Object is where a steward is declared -- Data Objects and Data Elements inherit from here." ] ;
|
|
||||||
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:businessDefinition ; sh:maxCount 0 ;
|
|
||||||
sh:severity sh:Violation ;
|
|
||||||
sh:message "A Business Object carries no definition of its own: meaning belongs to the Business Concept named by aboutConcept. Two definitions for one notion are two definitions free to diverge. MIGRATION: move the text to that Concept." ] ;
|
|
||||||
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:sourcedFrom ; sh:maxCount 0 ;
|
|
||||||
sh:severity sh:Violation ;
|
|
||||||
sh:message "A Business Object has no system of truth. It is a governance grouping, not data: its data lives in the Data Objects that represent it, and those name the system. In v0.6 this property held a grab-bag on the 15 objects -- physical locations, migration notes, arbitration remarks -- all under one name." ] .
|
|
||||||
|
|
||||||
|
|
||||||
#################################################################
|
|
||||||
# BUSINESS CONCEPT AND METRIC
|
|
||||||
#################################################################
|
|
||||||
|
|
||||||
prs:BusinessConceptShape a sh:NodeShape ;
|
prs:BusinessConceptShape a sh:NodeShape ;
|
||||||
sh:targetClass pr:BusinessConcept ;
|
|
||||||
rdfs:comment "Note what is NOT here: no requirement that a Concept be realized by any Data Element or measured by any Metric. The business layer is allowed to run ahead of the data. Coverage is a dashboard metric, not a rule." ;
|
rdfs:comment "Note what is NOT here: no requirement that a Concept be realized by any Data Element or measured by any Metric. The business layer is allowed to run ahead of the data. Coverage is a dashboard metric, not a rule." ;
|
||||||
|
sh:property [ sh:datatype xsd:string ;
|
||||||
|
sh:maxCount 1 ;
|
||||||
|
sh:message "A Business Concept exists to carry meaning: it must hold exactly one business definition of at least 20 characters." ;
|
||||||
|
sh:minCount 1 ;
|
||||||
|
sh:minLength 20 ;
|
||||||
|
sh:path pr:businessDefinition ;
|
||||||
|
sh:severity sh:Violation ],
|
||||||
|
[ sh:maxCount 0 ;
|
||||||
|
sh:message "BR-003 -- the presence of a formula is what makes something a Metric. A Concept with a formula is a misclassified Metric." ;
|
||||||
|
sh:path pr:formula ;
|
||||||
|
sh:severity sh:Violation ],
|
||||||
|
[ sh:maxCount 0 ;
|
||||||
|
sh:message "A Business Concept has no system of truth. Its authority is its owning domain and its definition belongs in the glossary. Naming a system here would ask which of the systems that USE the concept defines it -- a question with no answer." ;
|
||||||
|
sh:path pr:sourcedFrom ;
|
||||||
|
sh:severity sh:Violation ] ;
|
||||||
|
sh:targetClass pr:BusinessConcept .
|
||||||
|
|
||||||
sh:property [
|
prs:BusinessObjectShape a sh:NodeShape ;
|
||||||
sh:path pr:businessDefinition ; sh:datatype xsd:string ;
|
sh:property [ sh:class pr:DataSubDomain ;
|
||||||
sh:minCount 1 ; sh:maxCount 1 ; sh:minLength 20 ;
|
sh:maxCount 1 ;
|
||||||
|
sh:message "A Business Object belongs to exactly one Sub-Domain." ;
|
||||||
|
sh:minCount 1 ;
|
||||||
|
sh:path pr:belongsTo ;
|
||||||
|
sh:severity sh:Violation ],
|
||||||
|
[ sh:class pr:BusinessConcept ;
|
||||||
|
sh:maxCount 1 ;
|
||||||
|
sh:message "BR-004 -- a Business Object is about exactly one Business Concept. Replaces the old 3..10 minimum, which was satisfied in v0.6 by attaching unrelated concepts. If the subject cannot be named, the object is not scoped." ;
|
||||||
|
sh:minCount 1 ;
|
||||||
|
sh:path pr:isAbout ;
|
||||||
|
sh:severity sh:Violation ],
|
||||||
|
[ sh:class pr:BusinessConcept ;
|
||||||
|
sh:message "Other notions needed to read this object. Zero is a legitimate answer." ;
|
||||||
|
sh:path pr:usesConcept ;
|
||||||
|
sh:severity sh:Info ],
|
||||||
|
[ sh:class pr:DataSteward ;
|
||||||
|
sh:message "The Business Object is where a steward is declared -- Data Objects and Data Elements inherit from here." ;
|
||||||
|
sh:minCount 1 ;
|
||||||
|
sh:path pr:monitoredBy ;
|
||||||
|
sh:severity sh:Warning ],
|
||||||
|
[ sh:maxCount 0 ;
|
||||||
|
sh:message "A Business Object carries no definition of its own: meaning belongs to the Business Concept named by aboutConcept. Two definitions for one notion are two definitions free to diverge. MIGRATION: move the text to that Concept." ;
|
||||||
|
sh:path pr:businessDefinition ;
|
||||||
|
sh:severity sh:Violation ],
|
||||||
|
[ sh:maxCount 0 ;
|
||||||
|
sh:message "A Business Object has no system of truth. It is a governance grouping, not data: its data lives in the Data Objects that represent it, and those name the system. In v0.6 this property held a grab-bag on the 15 objects -- physical locations, migration notes, arbitration remarks -- all under one name." ;
|
||||||
|
sh:path pr:sourcedFrom ;
|
||||||
|
sh:severity sh:Violation ] ;
|
||||||
|
sh:targetClass pr:BusinessObject .
|
||||||
|
|
||||||
|
prs:CapturedObjectShape a sh:NodeShape ;
|
||||||
|
rdfs:comment "No identifier, no name, no owner, no status, no version. A harvested column is attributed through the Data Element mapped to it, never asserted directly. What it must carry is enough to be re-found on the next run." ;
|
||||||
|
sh:property [ sh:datatype xsd:string ;
|
||||||
|
sh:maxCount 1 ;
|
||||||
|
sh:message "A native immutable source id makes reconciliation idempotent and survives renames, which names do not." ;
|
||||||
|
sh:path pr:sourceIdentifier ;
|
||||||
|
sh:severity sh:Warning ],
|
||||||
|
[ sh:datatype xsd:dateTime ;
|
||||||
|
sh:maxCount 1 ;
|
||||||
|
sh:message "Every captured object records when it was harvested. Without it there is no way to tell a current fact from a stale one." ;
|
||||||
|
sh:minCount 1 ;
|
||||||
|
sh:path pr:harvestDate ;
|
||||||
|
sh:severity sh:Violation ],
|
||||||
|
[ sh:maxCount 0 ;
|
||||||
|
sh:message "Captured objects are not attributed directly -- ownership flows from the Data Element mapped to them." ;
|
||||||
|
sh:path pr:ownedByDomain ;
|
||||||
|
sh:severity sh:Warning ] ;
|
||||||
|
sh:targetClass pr:CapturedObject .
|
||||||
|
|
||||||
|
prs:DataDomainShape a sh:NodeShape ;
|
||||||
|
sh:property [ sh:maxCount 1 ;
|
||||||
|
sh:message "A Data Domain should name its owner." ;
|
||||||
|
sh:path pr:hasDomainOwner ;
|
||||||
|
sh:severity sh:Warning ],
|
||||||
|
[ sh:maxCount 1 ;
|
||||||
|
sh:message "A Data Domain should name its Data Governance Lead." ;
|
||||||
|
sh:path pr:hasGovernanceLead ;
|
||||||
|
sh:severity sh:Warning ],
|
||||||
|
[ sh:maxCount 1 ;
|
||||||
|
sh:path pr:activationStatus ;
|
||||||
|
sh:severity sh:Info ] ;
|
||||||
|
sh:targetClass pr:DataDomain .
|
||||||
|
|
||||||
|
prs:DataElementMeaningShape a sh:NodeShape ;
|
||||||
|
rdfs:comment "A Data Element reaches business meaning by exactly one of two routes: it represents a Business Concept (dimensional), or a Metric is computedBy it (measure). sh:xone rejects zero routes and two alike. A measure element must NOT also represent a Concept: the chain Metric -> measures -> Concept already carries the meaning, and carries more, since it names which metric it travels through." ;
|
||||||
|
sh:message "A Data Element must reach business meaning by exactly one route: represents a Business Concept (dimensional), or be computedBy a Metric (measure). Never both, never neither. A defined column with no business meaning is debt, not an acceptable state." ;
|
||||||
sh:severity sh:Violation ;
|
sh:severity sh:Violation ;
|
||||||
sh:message "A Business Concept exists to carry meaning: it must hold exactly one business definition of at least 20 characters." ] ;
|
sh:targetClass pr:DataElement ;
|
||||||
|
sh:xone ( [ sh:property [ sh:maxCount 1 ;
|
||||||
|
sh:minCount 1 ;
|
||||||
|
sh:path pr:represents ] ] [ sh:property [ sh:minCount 1 ;
|
||||||
|
sh:path [ sh:inversePath pr:computedBy ] ] ] ) .
|
||||||
|
|
||||||
sh:property [
|
prs:DataElementShape a sh:NodeShape ;
|
||||||
sh:path pr:formula ; sh:maxCount 0 ;
|
sh:property [ sh:datatype xsd:string ;
|
||||||
sh:severity sh:Violation ;
|
sh:maxCount 1 ;
|
||||||
sh:message "BR-003 -- the presence of a formula is what makes something a Metric. A Concept with a formula is a misclassified Metric." ] ;
|
sh:message "NR-009 -- the physical name is recorded verbatim and never altered." ;
|
||||||
|
sh:path pr:physicalName ;
|
||||||
|
sh:severity sh:Warning ],
|
||||||
|
[ sh:maxCount 1 ;
|
||||||
|
sh:path pr:fullyQualifiedName ;
|
||||||
|
sh:severity sh:Info ],
|
||||||
|
[ sh:maxCount 1 ;
|
||||||
|
sh:message "A Data Element carries exactly one unit. This is the counterpart of the Metric rule: the mother metric lists the units its variants use, each variant states the single unit it is actually expressed in. An element with two units is two elements." ;
|
||||||
|
sh:path pr:unit ;
|
||||||
|
sh:severity sh:Violation ],
|
||||||
|
[ sh:class pr:BusinessConcept ;
|
||||||
|
sh:maxCount 1 ;
|
||||||
|
sh:message "A Data Element represents at most one Business Concept. If a column carries two business notions, it should almost certainly be split -- so the case where a steward cannot choose is exactly the one worth looking at." ;
|
||||||
|
sh:path pr:represents ;
|
||||||
|
sh:severity sh:Violation ],
|
||||||
|
[ sh:maxCount 0 ;
|
||||||
|
sh:message "Stewardship is inherited from the Business Object, not declared here." ;
|
||||||
|
sh:path pr:monitoredBy ;
|
||||||
|
sh:severity sh:Warning ] ;
|
||||||
|
sh:targetClass pr:DataElement .
|
||||||
|
|
||||||
sh:property [
|
prs:DataInterfaceShape a sh:NodeShape ;
|
||||||
sh:path pr:sourcedFrom ; sh:maxCount 0 ;
|
sh:property [ sh:message "An interface should name what physically backs it. Unlike the old Data Asset test, this is verifiable: a grant, a share or an endpoint either exists or it does not." ;
|
||||||
sh:severity sh:Violation ;
|
sh:minCount 1 ;
|
||||||
sh:message "A Business Concept has no system of truth. Its authority is its owning domain and its definition belongs in the glossary. Naming a system here would ask which of the systems that USE the concept defines it -- a question with no answer." ] .
|
sh:path pr:servedBy ;
|
||||||
|
sh:severity sh:Warning ] ;
|
||||||
|
sh:targetClass pr:DataInterface .
|
||||||
|
|
||||||
|
prs:DataObjectShape a sh:NodeShape ;
|
||||||
|
sh:property [ sh:class pr:BusinessObject ;
|
||||||
|
sh:maxCount 1 ;
|
||||||
|
sh:message "BR-006 -- a Data Object represents exactly one Business Object. This is the pivot: the single junction between governance and technology." ;
|
||||||
|
sh:minCount 1 ;
|
||||||
|
sh:path pr:represents ;
|
||||||
|
sh:severity sh:Violation ],
|
||||||
|
[ sh:class pr:DataElement ;
|
||||||
|
sh:message "A Data Object holds at least one Data Element." ;
|
||||||
|
sh:minCount 1 ;
|
||||||
|
sh:path pr:hasElement ;
|
||||||
|
sh:severity sh:Violation ],
|
||||||
|
[ sh:class pr:DataElement ;
|
||||||
|
sh:message "State the grain: the elements whose combination makes a row unique. Checkable against the harvested primary key." ;
|
||||||
|
sh:path pr:hasGrainElement ;
|
||||||
|
sh:severity sh:Warning ],
|
||||||
|
[ sh:maxCount 0 ;
|
||||||
|
sh:message "Stewardship is declared on the Business Object and inherited through represents. Declaring it here allows two answers to the same question." ;
|
||||||
|
sh:path pr:monitoredBy ;
|
||||||
|
sh:severity sh:Warning ],
|
||||||
|
[ sh:class pr:System ;
|
||||||
|
sh:maxCount 1 ;
|
||||||
|
sh:message "Name the System holding the authoritative data, as a reference rather than a string. A string could not answer 'what depends on MDM Product'." ;
|
||||||
|
sh:path pr:sourcedFrom ;
|
||||||
|
sh:severity sh:Warning ],
|
||||||
|
[ sh:class pr:DataStructure ;
|
||||||
|
sh:message "Where the object actually sits: a table, a view, later a node label. The seam between what governance defines and what a harvester captures -- proposed by the reconciliation job, confirmed by a steward." ;
|
||||||
|
sh:path pr:storedIn ;
|
||||||
|
sh:severity sh:Warning ],
|
||||||
|
[ sh:maxCount 0 ;
|
||||||
|
sh:message "Deprecated in v1.4: storedIn now serves both granularities." ;
|
||||||
|
sh:path pr:materializedAs ;
|
||||||
|
sh:severity sh:Warning ] ;
|
||||||
|
sh:targetClass pr:DataObject .
|
||||||
|
|
||||||
|
prs:DataProductShape a sh:NodeShape ;
|
||||||
|
sh:property [ sh:class pr:DataSubDomain ;
|
||||||
|
sh:maxCount 1 ;
|
||||||
|
sh:message "A Data Product is operated by exactly one Sub-Domain. It spans several Business Objects and cannot inherit accountability, so it declares its own -- it is a consumable, and there must be no ambiguity about who runs it." ;
|
||||||
|
sh:minCount 1 ;
|
||||||
|
sh:path pr:operatedBy ;
|
||||||
|
sh:severity sh:Violation ],
|
||||||
|
[ sh:class pr:DataProductOwner ;
|
||||||
|
sh:maxCount 1 ;
|
||||||
|
sh:message "A Data Product should name exactly one Product Owner. A warning rather than a block: the SODH owner is genuinely not appointed, and a blocking rule that cannot be satisfied truthfully is how BR-004 produced padding." ;
|
||||||
|
sh:path pr:hasProductOwner ;
|
||||||
|
sh:severity sh:Warning ],
|
||||||
|
[ sh:class pr:DataObject ;
|
||||||
|
sh:message "A Data Product packages at least one Data Object. The Business Objects it makes available are derived from these, which is why no direct link to a Business Object is needed." ;
|
||||||
|
sh:minCount 1 ;
|
||||||
|
sh:path pr:packages ;
|
||||||
|
sh:severity sh:Violation ],
|
||||||
|
[ sh:maxCount 1 ;
|
||||||
|
sh:message "A Data Product should be governed by a Data Contract." ;
|
||||||
|
sh:path pr:governedBy ;
|
||||||
|
sh:severity sh:Warning ],
|
||||||
|
[ sh:class pr:DataInterface ;
|
||||||
|
sh:message "The channels through which consumers actually get the data." ;
|
||||||
|
sh:path pr:exposes ;
|
||||||
|
sh:severity sh:Info ] ;
|
||||||
|
sh:targetClass pr:DataProduct .
|
||||||
|
|
||||||
|
prs:DataStructureShape a sh:NodeShape ;
|
||||||
|
sh:property [ sh:maxCount 1 ;
|
||||||
|
sh:message "Optional on purpose: not every engine has a schema level." ;
|
||||||
|
sh:path pr:isInSchema ;
|
||||||
|
sh:severity sh:Info ],
|
||||||
|
[ sh:maxCount 1 ;
|
||||||
|
sh:path pr:sourcedFrom ;
|
||||||
|
sh:severity sh:Info ] ;
|
||||||
|
sh:targetClass pr:DataStructure .
|
||||||
|
|
||||||
|
prs:DefinedObjectShape a sh:NodeShape ;
|
||||||
|
rdfs:comment "Applies to everything governance writes. Captured objects are deliberately exempt: see CapturedObjectShape." ;
|
||||||
|
sh:property [ sh:datatype xsd:string ;
|
||||||
|
sh:maxCount 1 ;
|
||||||
|
sh:message "DQ-001 -- every defined object carries exactly one identifier. NOTE: uniqueness across the graph is NOT tested here; SHACL checks cardinality only. The reconciliation job tests uniqueness." ;
|
||||||
|
sh:minCount 1 ;
|
||||||
|
sh:path pr:identifier ;
|
||||||
|
sh:severity sh:Violation ],
|
||||||
|
[ sh:maxCount 1 ;
|
||||||
|
sh:message "A published object should carry a version." ;
|
||||||
|
sh:path pr:version ;
|
||||||
|
sh:severity sh:Warning ],
|
||||||
|
[ sh:datatype xsd:string ;
|
||||||
|
sh:maxCount 1 ;
|
||||||
|
sh:message "Every defined object carries exactly one canonical name." ;
|
||||||
|
sh:minCount 1 ;
|
||||||
|
sh:minLength 3 ;
|
||||||
|
sh:path pr:canonicalName ;
|
||||||
|
sh:severity sh:Violation ],
|
||||||
|
[ sh:in ( "DRAFT" "UNDER_REVIEW" "PUBLISHED" "DEPRECATED" "RETIRED" ) ;
|
||||||
|
sh:message "LC-001 -- status must be one of DRAFT, UNDER_REVIEW, PUBLISHED, DEPRECATED, RETIRED." ;
|
||||||
|
sh:minCount 1 ;
|
||||||
|
sh:path pr:status ;
|
||||||
|
sh:severity sh:Violation ],
|
||||||
|
[ sh:maxCount 1 ;
|
||||||
|
sh:message "OW-001 -- at most one owning Data Domain. On a Data Element this may be re-declared to differ from the parent chain: that marks a borrowed element." ;
|
||||||
|
sh:path pr:ownedByDomain ;
|
||||||
|
sh:severity sh:Violation ] ;
|
||||||
|
sh:targetClass pr:DefinedObject .
|
||||||
|
|
||||||
|
prs:FieldShape a sh:NodeShape ;
|
||||||
|
sh:property [ sh:datatype xsd:string ;
|
||||||
|
sh:maxCount 1 ;
|
||||||
|
sh:message "A Field carries DATABASE.SCHEMA.RELATION.COLUMN. A bare column name is ambiguous as soon as it exists in two tables, so the FQN is the real join key against INFORMATION_SCHEMA." ;
|
||||||
|
sh:minCount 1 ;
|
||||||
|
sh:path pr:fullyQualifiedName ;
|
||||||
|
sh:pattern "^[A-Za-z0-9_$]+\\\\.[A-Za-z0-9_$]+\\\\.[A-Za-z0-9_$]+\\\\.[A-Za-z0-9_$]+$" ;
|
||||||
|
sh:severity sh:Violation ],
|
||||||
|
[ sh:maxCount 1 ;
|
||||||
|
sh:message "Every Field lives in exactly one Physical Relation." ;
|
||||||
|
sh:minCount 1 ;
|
||||||
|
sh:path pr:livesIn ;
|
||||||
|
sh:severity sh:Violation ] ;
|
||||||
|
sh:targetClass pr:Field .
|
||||||
|
|
||||||
|
prs:GrainConsistencyShape a sh:NodeShape ;
|
||||||
|
sh:sparql [ sh:message "A grain element must be one of the object's own Data Elements." ;
|
||||||
|
sh:select """
|
||||||
|
PREFIX pr: <https://ontology.pernod-ricard.com/metamodel/>
|
||||||
|
SELECT $this ?grain WHERE {
|
||||||
|
$this pr:hasGrainElement ?grain .
|
||||||
|
FILTER NOT EXISTS { $this pr:hasElement ?grain }
|
||||||
|
}""" ;
|
||||||
|
sh:severity sh:Violation ] ;
|
||||||
|
sh:targetClass pr:DataObject .
|
||||||
|
|
||||||
prs:MetricShape a sh:NodeShape ;
|
prs:MetricShape a sh:NodeShape ;
|
||||||
sh:targetClass pr:Metric ;
|
sh:property [ sh:datatype xsd:string ;
|
||||||
|
sh:maxCount 1 ;
|
||||||
sh:property [
|
sh:message "BR-003 -- a Metric is a measurement with a calculation rule. No formula, no Metric." ;
|
||||||
sh:path pr:formula ; sh:datatype xsd:string ;
|
|
||||||
sh:minCount 1 ; sh:maxCount 1 ;
|
|
||||||
sh:severity sh:Violation ;
|
|
||||||
sh:message "BR-003 -- a Metric is a measurement with a calculation rule. No formula, no Metric." ] ;
|
|
||||||
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:unit ; sh:minCount 1 ;
|
|
||||||
sh:severity sh:Violation ;
|
|
||||||
sh:message "A Metric states every unit its variants are expressed in. NOT capped at one: Sell Out Volume is computed in 9L and in L, Sell Out Value in EUR, USD and local currency. The single-unit rule belongs on the Data Element, where it holds -- see DataElementShape." ] ;
|
|
||||||
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:measures ; sh:class pr:BusinessConcept ;
|
|
||||||
sh:minCount 1 ;
|
sh:minCount 1 ;
|
||||||
sh:severity sh:Violation ;
|
sh:path pr:formula ;
|
||||||
sh:message "A Metric measures at least one Business Concept -- this is how a measure element reaches business meaning." ] ;
|
sh:severity sh:Violation ],
|
||||||
|
[ sh:message "A Metric states every unit its variants are expressed in. NOT capped at one: Sell Out Volume is computed in 9L and in L, Sell Out Value in EUR, USD and local currency. The single-unit rule belongs on the Data Element, where it holds -- see DataElementShape." ;
|
||||||
|
sh:minCount 1 ;
|
||||||
|
sh:path pr:unit ;
|
||||||
|
sh:severity sh:Violation ],
|
||||||
|
[ sh:class pr:BusinessConcept ;
|
||||||
|
sh:message "A Metric measures at least one Business Concept -- this is how a measure element reaches business meaning." ;
|
||||||
|
sh:minCount 1 ;
|
||||||
|
sh:path pr:measures ;
|
||||||
|
sh:severity sh:Violation ],
|
||||||
|
[ sh:maxCount 0 ;
|
||||||
|
sh:message "Grain moved off the Metric in v1.1. A Metric is a calculation rule and has no rows; grain belongs to the Data Object, stated with hasGrainElement and checkable against the real primary key." ;
|
||||||
|
sh:path pr:hasGranularity ;
|
||||||
|
sh:severity sh:Warning ],
|
||||||
|
[ sh:maxCount 0 ;
|
||||||
|
sh:message "A Metric must not carry a physical name -- that is a layer leak. Under BR-013 the physical name belongs on the Data Element." ;
|
||||||
|
sh:path pr:physicalName ;
|
||||||
|
sh:severity sh:Violation ] ;
|
||||||
|
sh:targetClass pr:Metric .
|
||||||
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:hasGranularity ; sh:maxCount 0 ;
|
|
||||||
sh:severity sh:Warning ;
|
|
||||||
sh:message "Grain moved off the Metric in v1.1. A Metric is a calculation rule and has no rows; grain belongs to the Data Object, stated with hasGrainElement and checkable against the real primary key." ] ;
|
|
||||||
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:physicalName ; sh:maxCount 0 ;
|
|
||||||
sh:severity sh:Violation ;
|
|
||||||
sh:message "A Metric must not carry a physical name -- that is a layer leak. Under BR-013 the physical name belongs on the Data Element." ] .
|
|
||||||
|
|
||||||
|
|
||||||
# The source of truth of a Metric is asserted by exception, then checked.
|
|
||||||
# "Where can I get this metric" is a first-order question and the derivation
|
|
||||||
# chain will have gaps before harvesting, so the assertion stands on its own --
|
|
||||||
# but it must agree with the elements that implement the metric, or the two
|
|
||||||
# will drift the day an element moves.
|
|
||||||
prs:MetricSourceShape a sh:NodeShape ;
|
prs:MetricSourceShape a sh:NodeShape ;
|
||||||
sh:targetClass pr:Metric ;
|
|
||||||
rdfs:comment "Rewritten in v1.4 to compare System references rather than strings. A string comparison would have called 'SODH Gold' and 'Snowflake (SODH Gold)' two different systems." ;
|
rdfs:comment "Rewritten in v1.4 to compare System references rather than strings. A string comparison would have called 'SODH Gold' and 'Snowflake (SODH Gold)' two different systems." ;
|
||||||
sh:sparql [
|
sh:sparql [ sh:message "A Metric's system of truth must match that of at least one element computing it. If they disagree, either the metric points at the wrong system or an element has moved." ;
|
||||||
sh:severity sh:Violation ;
|
|
||||||
sh:message "A Metric's system of truth must match that of at least one element computing it. If they disagree, either the metric points at the wrong system or an element has moved." ;
|
|
||||||
sh:select """
|
sh:select """
|
||||||
PREFIX pr: <https://ontology.pernod-ricard.com/metamodel/>
|
PREFIX pr: <https://ontology.pernod-ricard.com/metamodel/>
|
||||||
SELECT $this WHERE {
|
SELECT $this WHERE {
|
||||||
@@ -308,10 +310,9 @@ prs:MetricSourceShape a sh:NodeShape ;
|
|||||||
$this pr:computedBy ?any .
|
$this pr:computedBy ?any .
|
||||||
?any pr:sourcedFrom ?sys .
|
?any pr:sourcedFrom ?sys .
|
||||||
}
|
}
|
||||||
}""" ] ;
|
}""" ;
|
||||||
sh:sparql [
|
sh:severity sh:Violation ],
|
||||||
sh:severity sh:Warning ;
|
[ sh:message "Elements computing one Metric come from several systems. Legitimate for a metric blending sources, worth a look otherwise." ;
|
||||||
sh:message "Elements computing one Metric come from several systems. Legitimate for a metric blending sources, worth a look otherwise." ;
|
|
||||||
sh:select """
|
sh:select """
|
||||||
PREFIX pr: <https://ontology.pernod-ricard.com/metamodel/>
|
PREFIX pr: <https://ontology.pernod-ricard.com/metamodel/>
|
||||||
SELECT $this WHERE {
|
SELECT $this WHERE {
|
||||||
@@ -319,201 +320,66 @@ prs:MetricSourceShape a sh:NodeShape ;
|
|||||||
?a pr:sourcedFrom ?sa .
|
?a pr:sourcedFrom ?sa .
|
||||||
?b pr:sourcedFrom ?sb .
|
?b pr:sourcedFrom ?sb .
|
||||||
FILTER(?sa != ?sb)
|
FILTER(?sa != ?sb)
|
||||||
}""" ] .
|
}""" ;
|
||||||
|
sh:severity sh:Warning ] ;
|
||||||
|
sh:targetClass pr:Metric .
|
||||||
|
|
||||||
|
|
||||||
# BR-013, finally executable. v0.1.1 declared it BLOCKING with no shape.
|
|
||||||
prs:MotherMetricShape a sh:NodeShape ;
|
prs:MotherMetricShape a sh:NodeShape ;
|
||||||
sh:targetClass pr:Metric ;
|
sh:sparql [ sh:message "BR-013 -- a PUBLISHED Metric should be implemented by at least one Data Element through computedBy. A metric no element computes is a definition with nothing behind it." ;
|
||||||
sh:sparql [
|
|
||||||
sh:severity sh:Warning ;
|
|
||||||
sh:message "BR-013 -- a PUBLISHED Metric should be implemented by at least one Data Element through computedBy. A metric no element computes is a definition with nothing behind it." ;
|
|
||||||
sh:select """
|
sh:select """
|
||||||
PREFIX pr: <https://ontology.pernod-ricard.com/metamodel/>
|
PREFIX pr: <https://ontology.pernod-ricard.com/metamodel/>
|
||||||
SELECT $this WHERE {
|
SELECT $this WHERE {
|
||||||
$this pr:status "PUBLISHED" .
|
$this pr:status "PUBLISHED" .
|
||||||
FILTER NOT EXISTS { $this pr:computedBy ?de }
|
FILTER NOT EXISTS { $this pr:computedBy ?de }
|
||||||
}""" ] .
|
}""" ;
|
||||||
|
sh:severity sh:Warning ] ;
|
||||||
|
sh:targetClass pr:Metric .
|
||||||
#################################################################
|
|
||||||
# DATA OBJECT -- the pivot, and grain
|
|
||||||
#################################################################
|
|
||||||
|
|
||||||
prs:DataObjectShape a sh:NodeShape ;
|
|
||||||
sh:targetClass pr:DataObject ;
|
|
||||||
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:represents ; sh:class pr:BusinessObject ;
|
|
||||||
sh:minCount 1 ; sh:maxCount 1 ;
|
|
||||||
sh:severity sh:Violation ;
|
|
||||||
sh:message "BR-006 -- a Data Object represents exactly one Business Object. This is the pivot: the single junction between governance and technology." ] ;
|
|
||||||
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:hasElement ; sh:class pr:DataElement ;
|
|
||||||
sh:minCount 1 ;
|
|
||||||
sh:severity sh:Violation ;
|
|
||||||
sh:message "A Data Object holds at least one Data Element." ] ;
|
|
||||||
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:hasGrainElement ; sh:class pr:DataElement ;
|
|
||||||
sh:severity sh:Warning ;
|
|
||||||
sh:message "State the grain: the elements whose combination makes a row unique. Checkable against the harvested primary key." ] ;
|
|
||||||
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:monitoredBy ; sh:maxCount 0 ;
|
|
||||||
sh:severity sh:Warning ;
|
|
||||||
sh:message "Stewardship is declared on the Business Object and inherited through represents. Declaring it here allows two answers to the same question." ] ;
|
|
||||||
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:sourcedFrom ; sh:class pr:System ; sh:maxCount 1 ;
|
|
||||||
sh:severity sh:Warning ;
|
|
||||||
sh:message "Name the System holding the authoritative data, as a reference rather than a string. A string could not answer 'what depends on MDM Product'." ] ;
|
|
||||||
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:storedIn ; sh:class pr:DataStructure ;
|
|
||||||
sh:severity sh:Warning ;
|
|
||||||
sh:message "Where the object actually sits: a table, a view, later a node label. The seam between what governance defines and what a harvester captures -- proposed by the reconciliation job, confirmed by a steward." ] ;
|
|
||||||
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:materializedAs ; sh:maxCount 0 ;
|
|
||||||
sh:severity sh:Warning ;
|
|
||||||
sh:message "Deprecated in v1.4: storedIn now serves both granularities." ] .
|
|
||||||
|
|
||||||
|
|
||||||
# A declared grain element must actually belong to the object it grains.
|
|
||||||
prs:GrainConsistencyShape a sh:NodeShape ;
|
|
||||||
sh:targetClass pr:DataObject ;
|
|
||||||
sh:sparql [
|
|
||||||
sh:severity sh:Violation ;
|
|
||||||
sh:message "A grain element must be one of the object's own Data Elements." ;
|
|
||||||
sh:select """
|
|
||||||
PREFIX pr: <https://ontology.pernod-ricard.com/metamodel/>
|
|
||||||
SELECT $this ?grain WHERE {
|
|
||||||
$this pr:hasGrainElement ?grain .
|
|
||||||
FILTER NOT EXISTS { $this pr:hasElement ?grain }
|
|
||||||
}""" ] .
|
|
||||||
|
|
||||||
|
|
||||||
#################################################################
|
|
||||||
# DATA ELEMENT -- the XOR of meaning
|
|
||||||
#################################################################
|
|
||||||
|
|
||||||
prs:DataElementShape a sh:NodeShape ;
|
|
||||||
sh:targetClass pr:DataElement ;
|
|
||||||
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:physicalName ; sh:datatype xsd:string ;
|
|
||||||
sh:maxCount 1 ;
|
|
||||||
sh:severity sh:Warning ;
|
|
||||||
sh:message "NR-009 -- the physical name is recorded verbatim and never altered." ] ;
|
|
||||||
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:fullyQualifiedName ; sh:maxCount 1 ;
|
|
||||||
sh:severity sh:Info ] ;
|
|
||||||
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:unit ; sh:maxCount 1 ;
|
|
||||||
sh:severity sh:Violation ;
|
|
||||||
sh:message "A Data Element carries exactly one unit. This is the counterpart of the Metric rule: the mother metric lists the units its variants use, each variant states the single unit it is actually expressed in. An element with two units is two elements." ] ;
|
|
||||||
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:represents ; sh:class pr:BusinessConcept ;
|
|
||||||
sh:maxCount 1 ;
|
|
||||||
sh:severity sh:Violation ;
|
|
||||||
sh:message "A Data Element represents at most one Business Concept. If a column carries two business notions, it should almost certainly be split -- so the case where a steward cannot choose is exactly the one worth looking at." ] ;
|
|
||||||
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:monitoredBy ; sh:maxCount 0 ;
|
|
||||||
sh:severity sh:Warning ;
|
|
||||||
sh:message "Stewardship is inherited from the Business Object, not declared here." ] .
|
|
||||||
|
|
||||||
|
|
||||||
# The core rule of v1.1.
|
|
||||||
prs:DataElementMeaningShape a sh:NodeShape ;
|
|
||||||
sh:targetClass pr:DataElement ;
|
|
||||||
rdfs:comment "A Data Element reaches business meaning by exactly one of two routes: it represents a Business Concept (dimensional), or a Metric is computedBy it (measure). sh:xone rejects zero routes and two alike. A measure element must NOT also represent a Concept: the chain Metric -> measures -> Concept already carries the meaning, and carries more, since it names which metric it travels through." ;
|
|
||||||
sh:xone (
|
|
||||||
[ sh:property [ sh:path pr:represents ;
|
|
||||||
sh:minCount 1 ; sh:maxCount 1 ] ]
|
|
||||||
[ sh:property [ sh:path [ sh:inversePath pr:computedBy ] ;
|
|
||||||
sh:minCount 1 ] ]
|
|
||||||
) ;
|
|
||||||
sh:severity sh:Violation ;
|
|
||||||
sh:message "A Data Element must reach business meaning by exactly one route: represents a Business Concept (dimensional), or be computedBy a Metric (measure). Never both, never neither. A defined column with no business meaning is debt, not an acceptable state." .
|
|
||||||
|
|
||||||
|
|
||||||
#################################################################
|
|
||||||
# DELIVERY
|
|
||||||
#################################################################
|
|
||||||
|
|
||||||
prs:DataProductShape a sh:NodeShape ;
|
|
||||||
sh:targetClass pr:DataProduct ;
|
|
||||||
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:operatedBy ; sh:class pr:SubDomain ;
|
|
||||||
sh:minCount 1 ; sh:maxCount 1 ;
|
|
||||||
sh:severity sh:Violation ;
|
|
||||||
sh:message "A Data Product is operated by exactly one Sub-Domain. It spans several Business Objects and cannot inherit accountability, so it declares its own -- it is a consumable, and there must be no ambiguity about who runs it." ] ;
|
|
||||||
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:hasProductOwner ; sh:class pr:DataProductOwner ;
|
|
||||||
sh:maxCount 1 ;
|
|
||||||
sh:severity sh:Warning ;
|
|
||||||
sh:message "A Data Product should name exactly one Product Owner. A warning rather than a block: the SODH owner is genuinely not appointed, and a blocking rule that cannot be satisfied truthfully is how BR-004 produced padding." ] ;
|
|
||||||
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:packages ; sh:class pr:DataObject ;
|
|
||||||
sh:minCount 1 ;
|
|
||||||
sh:severity sh:Violation ;
|
|
||||||
sh:message "A Data Product packages at least one Data Object. The Business Objects it makes available are derived from these, which is why no direct link to a Business Object is needed." ] ;
|
|
||||||
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:governedBy ; sh:maxCount 1 ;
|
|
||||||
sh:severity sh:Warning ;
|
|
||||||
sh:message "A Data Product should be governed by a Data Contract." ] ;
|
|
||||||
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:exposes ; sh:class pr:DataInterface ;
|
|
||||||
sh:severity sh:Info ;
|
|
||||||
sh:message "The channels through which consumers actually get the data." ] .
|
|
||||||
|
|
||||||
|
|
||||||
prs:DataInterfaceShape a sh:NodeShape ;
|
|
||||||
sh:targetClass pr:DataInterface ;
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:servedBy ; sh:minCount 1 ;
|
|
||||||
sh:severity sh:Warning ;
|
|
||||||
sh:message "An interface should name what physically backs it. Unlike the old Data Asset test, this is verifiable: a grant, a share or an endpoint either exists or it does not." ] .
|
|
||||||
|
|
||||||
|
|
||||||
#################################################################
|
|
||||||
# CONSUMPTION
|
|
||||||
#################################################################
|
|
||||||
|
|
||||||
prs:BIFieldShape a sh:NodeShape ;
|
|
||||||
sh:targetClass pr:BIField ;
|
|
||||||
rdfs:comment "realizesMetric is a transitional control: asserted directly, with no rule written against the lineage chain. Making the assertion conditional on intermediate mappings existing would create confusion and discourage people from filling it in at all." ;
|
|
||||||
sh:property [
|
|
||||||
sh:path pr:exposesMetric ; sh:class pr:Metric ;
|
|
||||||
sh:maxCount 1 ;
|
|
||||||
sh:severity sh:Info ;
|
|
||||||
sh:message "Which certified Metric this BI field implements. Comparing expressions across fields realizing one Metric is the harmonisation measure." ] .
|
|
||||||
|
|
||||||
|
|
||||||
#################################################################
|
|
||||||
# NAMING -- portable regex, no lookbehind
|
|
||||||
#################################################################
|
|
||||||
|
|
||||||
prs:NamingShape a sh:NodeShape ;
|
prs:NamingShape a sh:NodeShape ;
|
||||||
sh:targetClass pr:DefinedObject ;
|
sh:property [ sh:message "NR-001 -- a canonical name must not end in a physical suffix. Written as sh:not[sh:pattern] rather than a lookbehind: sh:pattern is normatively XPath 2.0 regex, which has no lookbehind, so the v0.1.1 form only worked under pySHACL." ;
|
||||||
sh:property [
|
|
||||||
sh:path pr:name ;
|
|
||||||
sh:not [ sh:pattern "_NM$" ] ;
|
sh:not [ sh:pattern "_NM$" ] ;
|
||||||
sh:severity sh:Warning ;
|
sh:path pr:canonicalName ;
|
||||||
sh:message "NR-001 -- a canonical name must not end in a physical suffix. Written as sh:not[sh:pattern] rather than a lookbehind: sh:pattern is normatively XPath 2.0 regex, which has no lookbehind, so the v0.1.1 form only worked under pySHACL." ] ;
|
sh:severity sh:Warning ],
|
||||||
sh:property [
|
[ sh:message "NR-002 -- a canonical name is written for humans, not as a snake_case identifier." ;
|
||||||
sh:path pr:name ;
|
|
||||||
sh:not [ sh:pattern "^[a-z_]+$" ] ;
|
sh:not [ sh:pattern "^[a-z_]+$" ] ;
|
||||||
sh:severity sh:Warning ;
|
sh:path pr:canonicalName ;
|
||||||
sh:message "NR-002 -- a canonical name is written for humans, not as a snake_case identifier." ] .
|
sh:severity sh:Warning ] ;
|
||||||
|
sh:targetClass pr:DefinedObject .
|
||||||
|
|
||||||
|
prs:PublishedShape a sh:NodeShape ;
|
||||||
|
rdfs:comment "Extra demands that only bite once an object claims to be PUBLISHED. Keeping these off the base shape is what lets a domain be documented progressively." ;
|
||||||
|
sh:sparql [ sh:message "OW-007 -- an object flagged TO_ARBITRATE cannot be PUBLISHED. Resolve at the Cross-Domain Council first." ;
|
||||||
|
sh:select """
|
||||||
|
PREFIX pr: <https://ontology.pernod-ricard.com/metamodel/>
|
||||||
|
SELECT $this WHERE {
|
||||||
|
$this pr:status "PUBLISHED" ;
|
||||||
|
pr:arbitrationStatus "TO_ARBITRATE" .
|
||||||
|
}""" ;
|
||||||
|
sh:severity sh:Violation ],
|
||||||
|
[ sh:message "OW-003 -- a PUBLISHED object must have an owning Data Domain." ;
|
||||||
|
sh:select """
|
||||||
|
PREFIX pr: <https://ontology.pernod-ricard.com/metamodel/>
|
||||||
|
SELECT $this WHERE {
|
||||||
|
$this pr:status "PUBLISHED" .
|
||||||
|
FILTER NOT EXISTS { $this pr:ownedByDomain ?d }
|
||||||
|
}""" ;
|
||||||
|
sh:severity sh:Violation ] ;
|
||||||
|
sh:targetClass pr:DefinedObject .
|
||||||
|
|
||||||
|
prs:SubDomainShape a sh:NodeShape ;
|
||||||
|
sh:property [ sh:class pr:DataDomain ;
|
||||||
|
sh:maxCount 1 ;
|
||||||
|
sh:message "A Sub-Domain belongs to exactly one Data Domain." ;
|
||||||
|
sh:minCount 1 ;
|
||||||
|
sh:path pr:belongsTo ;
|
||||||
|
sh:severity sh:Violation ],
|
||||||
|
[ sh:maxCount 1 ;
|
||||||
|
sh:message "A Sub-Domain should name its owner." ;
|
||||||
|
sh:path pr:hasSubDomainOwner ;
|
||||||
|
sh:severity sh:Warning ] ;
|
||||||
|
sh:targetClass pr:DataSubDomain .
|
||||||
|
|
||||||
|
<https://ontology.pernod-ricard.com/shapes> a owl:Ontology ;
|
||||||
|
rdfs:label "PR Data MetaModel SHACL Shapes" ;
|
||||||
|
dcterms:conformsTo pr:2.0 .
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user