tbox: v1.1 - belongsTo unifie, implements devient represents, aboutConcept/usesConcept remplacent composedOf, monitoredBy sur BO seul, grain deplace du Metric au DataObject, couche Delivery et DataInterface. shapes v0.2 scindees curated/observed avec XOR de sens sur DataElement. instances SODH migrees et arbitrees : 12 BO, 12 BC, 6 concepts empruntes en TO_ARBITRATE. Validation : 52 bloquants, dont 52 leves par la migration BR-013 restante
This commit is contained in:
+424
-109
@@ -1,127 +1,442 @@
|
||||
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
|
||||
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
|
||||
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
|
||||
@prefix sh: <http://www.w3.org/ns/shacl#> .
|
||||
@prefix pr: <https://ontology.pernod-ricard.com/metamodel/0.9/> .
|
||||
@prefix prsh: <https://ontology.pernod-ricard.com/metamodel/0.9/shapes/> .
|
||||
@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.1.1
|
||||
# Executable encoding of BLOCKING (sh:Violation) & MAJOR (sh:Warning) rules.
|
||||
# FIX v0.1.1 (2026-07-23): NR-002 uniqueness query now restricts ?cls to
|
||||
# LEAF classes (via FILTER ... IN) so that objects of different layers that
|
||||
# share a name (e.g. Sub-Domain "Sell Out" vs Concept "Sell Out") are NOT
|
||||
# reported as duplicates. Rule stays "unique WITHIN its class".
|
||||
# Run: python run_shacl_validation.py SODH_data.ttl pr_metamodel_shapes_v0.1.ttl pr_metamodel_v0.9.ttl
|
||||
# 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 harvested half of the graph that would fail on
|
||||
# every one of ~50 000 Snowflake columns. Curated and Observed 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
|
||||
#################################################################
|
||||
|
||||
prsh:MetaModelObjectShape a sh:NodeShape ;
|
||||
sh:targetClass pr:MetaModelObject ;
|
||||
sh:property [ sh:path pr:hasIdentifier ; sh:minCount 1 ; sh:maxCount 1 ; sh:datatype xsd:string ;
|
||||
sh:severity sh:Violation ; sh:message "DQ-001 (BLOCKING): exactly one hasIdentifier." ] ;
|
||||
sh:property [ sh:path pr:hasName ; sh:minCount 1 ; sh:maxCount 1 ; sh:datatype xsd:string ; sh:minLength 2 ;
|
||||
sh:severity sh:Violation ; sh:message "DQ-002 (BLOCKING): exactly one non-empty hasName." ] ;
|
||||
sh:property [ sh:path pr:ownedBy ; sh:minCount 1 ; sh:class pr:Actor ;
|
||||
sh:severity sh:Violation ; sh:message "OW-001 (BLOCKING): at least one owner (Actor)." ] ;
|
||||
sh:property [ sh:path pr:owningDomain ; sh:minCount 1 ; sh:maxCount 1 ; sh:class pr:DataDomain ;
|
||||
sh:severity sh:Violation ; sh:message "OW-001 (BLOCKING): exactly one owning Data Domain." ] ;
|
||||
sh:property [ sh:path pr:hasStatus ; sh:minCount 1 ; sh:in ( "DRAFT" "UNDER_REVIEW" "PUBLISHED" "DEPRECATED" ) ;
|
||||
sh:severity sh:Warning ; sh:message "DQ-012 (MAJOR): hasStatus in controlled list." ] ;
|
||||
sh:property [ sh:path pr:hasVersion ; sh:minCount 1 ;
|
||||
sh:severity sh:Warning ; sh:message "DQ-012 (MAJOR): hasVersion required." ] .
|
||||
<https://ontology.pernod-ricard.com/metamodel/shapes/>
|
||||
dcterms:title "PR Data Meta Model - SHACL Shapes" ;
|
||||
dcterms:modified "2026-07-26"^^xsd:date ;
|
||||
rdfs:comment "Executable form of the rulebook. Validated by scripts/run_shacl_validation.py." .
|
||||
|
||||
prsh:BackDocSourceShape a sh:NodeShape ;
|
||||
sh:targetClass pr:MetaModelObject ;
|
||||
sh:sparql [ sh:severity sh:Violation ;
|
||||
sh:message "DQ-006 (BLOCKING): a DRAFT back-doc object MUST carry hasSource." ;
|
||||
sh:select """
|
||||
PREFIX pr: <https://ontology.pernod-ricard.com/metamodel/0.9/>
|
||||
SELECT $this WHERE { $this pr:hasStatus "DRAFT" .
|
||||
FILTER NOT EXISTS { $this pr:hasSource ?s } }
|
||||
""" ] .
|
||||
|
||||
prsh:CanonicalNameShape a sh:NodeShape ;
|
||||
sh:targetClass pr:MetaModelObject ;
|
||||
sh:property [ sh:path pr:hasName ;
|
||||
sh:pattern "^(?!GL_)(?!ID_).*(?<!_NM)(?<!_CD)(?<!_FLG)(?<!_NR)(?<!_DT)$" ; sh:flags "i" ;
|
||||
#################################################################
|
||||
# IDENTITY AND LIFECYCLE -- curated objects only
|
||||
#################################################################
|
||||
|
||||
prs:CuratedObjectShape a sh:NodeShape ;
|
||||
sh:targetClass pr:CuratedObject ;
|
||||
rdfs:comment "Applies to everything governance asserts. Observed objects are deliberately exempt: see ObservedObjectShape." ;
|
||||
|
||||
sh:property [
|
||||
sh:path pr:hasIdentifier ; sh:datatype xsd:string ;
|
||||
sh:minCount 1 ; sh:maxCount 1 ;
|
||||
sh:severity sh:Violation ;
|
||||
sh:message "DQ-001 -- every curated 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:hasName ; sh:datatype xsd:string ;
|
||||
sh:minCount 1 ; sh:maxCount 1 ; sh:minLength 3 ;
|
||||
sh:severity sh:Violation ;
|
||||
sh:message "Every curated object carries exactly one canonical name." ] ;
|
||||
|
||||
sh:property [
|
||||
sh:path pr:hasStatus ; 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:hasVersion ; sh:maxCount 1 ;
|
||||
sh:severity sh:Warning ;
|
||||
sh:message "NR-001 (MAJOR): canonical name free of physical prefixes/suffixes." ] .
|
||||
sh:message "A published object should carry a version." ] .
|
||||
|
||||
prsh:MetricShape a sh:NodeShape ;
|
||||
sh:targetClass pr:Metric ;
|
||||
sh:property [ sh:path pr:hasFormula ; sh:minCount 1 ; sh:datatype xsd:string ; sh:minLength 2 ;
|
||||
sh:severity sh:Violation ; sh:message "BR-003 / DQ-004 (BLOCKING): a Metric MUST have a formula." ] ;
|
||||
sh:property [ sh:path pr:hasUnit ; sh:minCount 1 ;
|
||||
sh:severity sh:Violation ; sh:message "DQ-004 (BLOCKING): a Metric MUST have a hasUnit." ] ;
|
||||
sh:property [ sh:path pr:hasGranularity ; sh:minCount 1 ;
|
||||
sh:severity sh:Violation ; sh:message "DQ-004 (BLOCKING): a Metric MUST have a hasGranularity." ] .
|
||||
|
||||
prsh:BusinessConceptShape a sh:NodeShape ;
|
||||
sh:targetClass pr:BusinessConcept ;
|
||||
sh:property [ sh:path pr:hasFormula ; sh:maxCount 0 ;
|
||||
sh:severity sh:Violation ; sh:message "BR-003 (BLOCKING): a Business Concept MUST NOT have a formula." ] .
|
||||
prs:PublishedShape a sh:NodeShape ;
|
||||
sh:targetClass pr:CuratedObject ;
|
||||
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." ;
|
||||
|
||||
prsh:BusinessObjectShape a sh:NodeShape ;
|
||||
sh:targetClass pr:BusinessObject ;
|
||||
sh:property [ sh:path pr:composedOf ; sh:minCount 3 ; sh:maxCount 10 ; sh:class pr:BusinessConcept ;
|
||||
sh:severity sh:Warning ; sh:message "BR-004 (MAJOR): a Business Object MUST group 3 to 10 Business Concepts." ] ;
|
||||
sh:property [ sh:path pr:belongsToSubDomain ; sh:minCount 1 ; sh:maxCount 1 ; sh:class pr:SubDomain ;
|
||||
sh:severity sh:Warning ; sh:message "NR-012/OW (MAJOR): a Business Object belongs to exactly one Sub-Domain." ] ;
|
||||
sh:property [ sh:path pr:hasSteward ; sh:minCount 1 ; sh:class pr:DataSteward ;
|
||||
sh:severity sh:Warning ; sh:message "OW-005 (MAJOR): a Business Object MUST have a Data Steward." ] .
|
||||
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:hasStatus "PUBLISHED" ;
|
||||
pr:arbitrationStatus "TO_ARBITRATE" .
|
||||
}""" ] ;
|
||||
|
||||
prsh:DataObjectShape a sh:NodeShape ;
|
||||
sh:targetClass pr:DataObject ;
|
||||
sh:property [ sh:path pr:implements ; sh:minCount 1 ; sh:maxCount 1 ; sh:class pr:BusinessObject ;
|
||||
sh:severity sh:Violation ; sh:message "BR-006 (BLOCKING): a Data Object MUST implement exactly one Business Object." ] ;
|
||||
sh:property [ sh:path pr:hasElement ; sh:minCount 1 ; sh:class pr:DataElement ;
|
||||
sh:severity sh:Violation ; sh:message "BR-006 (BLOCKING): a Data Object MUST contain at least one Data Element." ] ;
|
||||
sh:property [ sh:path pr:hasSteward ; sh:minCount 1 ; sh:class pr:DataSteward ;
|
||||
sh:severity sh:Warning ; sh:message "OW-005 (MAJOR): a Data Object MUST have a Data Steward." ] .
|
||||
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:hasStatus "PUBLISHED" .
|
||||
FILTER NOT EXISTS { $this pr:owningDomain ?d }
|
||||
}""" ] .
|
||||
|
||||
prsh:DataElementShape a sh:NodeShape ;
|
||||
sh:targetClass pr:DataElement ;
|
||||
sh:property [ sh:path pr:hasFormat ; sh:minCount 1 ;
|
||||
sh:severity sh:Warning ; sh:message "DQ-005 (MAJOR): a Data Element MUST have a hasFormat." ] ;
|
||||
sh:property [ sh:path pr:hasSourceSystem ; sh:minCount 1 ;
|
||||
sh:severity sh:Warning ; sh:message "DQ-005 (MAJOR): a Data Element MUST have a hasSourceSystem." ] ;
|
||||
sh:property [ sh:path pr:physicalName ; sh:minCount 1 ; sh:maxCount 1 ; sh:datatype xsd:string ;
|
||||
sh:severity sh:Violation ; sh:message "DQ-010 (BLOCKING): a mapped Data Element MUST have a physical_name." ] .
|
||||
|
||||
prsh:DataDomainShape a sh:NodeShape ;
|
||||
#################################################################
|
||||
# OBSERVED OBJECTS -- harvested, never hand-edited
|
||||
#################################################################
|
||||
|
||||
prs:ObservedObjectShape a sh:NodeShape ;
|
||||
sh:targetClass pr:ObservedObject ;
|
||||
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 observed 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 "Observed objects are not attributed directly -- ownership flows from the Data Element mapped to them." ] .
|
||||
|
||||
|
||||
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:hasDGL ; sh:minCount 1 ; sh:maxCount 1 ;
|
||||
sh:severity sh:Violation ; sh:message "OW-004 (BLOCKING): a Data Domain MUST have exactly one DGL." ] ;
|
||||
sh:property [ sh:path pr:hasActivationStatus ; sh:minCount 1 ; sh:maxCount 1 ;
|
||||
sh:severity sh:Violation ; sh:message "OW-004 (BLOCKING): a Data Domain MUST have exactly one Activation Status." ] .
|
||||
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 ] .
|
||||
|
||||
# FIX v0.1.1 - NR-002 now restricts ?cls to LEAF classes so cross-layer
|
||||
# homonyms (Sub-Domain vs Concept, etc.) are not flagged as duplicates.
|
||||
prsh:NameUniquenessShape a sh:NodeShape ;
|
||||
sh:targetClass pr:MetaModelObject ;
|
||||
sh:sparql [ sh:severity sh:Violation ;
|
||||
sh:message "NR-002 (BLOCKING): canonical name MUST be unique within its (leaf) class and owning Data Domain." ;
|
||||
sh:select """
|
||||
PREFIX pr: <https://ontology.pernod-ricard.com/metamodel/0.9/>
|
||||
SELECT $this WHERE {
|
||||
$this a ?cls ; pr:hasName ?n ; pr:owningDomain ?d .
|
||||
?other a ?cls ; pr:hasName ?n ; pr:owningDomain ?d .
|
||||
FILTER ( $this != ?other )
|
||||
FILTER ( ?cls IN (
|
||||
pr:DataDomain, pr:SubDomain, pr:BusinessObject,
|
||||
pr:BusinessConcept, pr:Metric, pr:KPI,
|
||||
pr:DataAsset, pr:DataObject, pr:DataElement,
|
||||
pr:DataContract, pr:DataProduct, pr:Table, pr:Field ) )
|
||||
}
|
||||
""" ] .
|
||||
|
||||
prsh:ArbitrationGateShape a sh:NodeShape ;
|
||||
sh:targetClass pr:MetaModelObject ;
|
||||
sh:sparql [ sh:severity sh:Violation ;
|
||||
sh:message "OW-007 (BLOCKING): a TO_ARBITRATE object MUST NOT be PUBLISHED." ;
|
||||
#################################################################
|
||||
# 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:hasBusinessDefinition ; 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." ] .
|
||||
|
||||
|
||||
#################################################################
|
||||
# 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:hasBusinessDefinition ; 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:hasFormula ; 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." ] .
|
||||
|
||||
|
||||
prs:MetricShape a sh:NodeShape ;
|
||||
sh:targetClass pr:Metric ;
|
||||
|
||||
sh:property [
|
||||
sh:path pr:hasFormula ; 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:hasUnit ; sh:minCount 1 ; sh:maxCount 1 ;
|
||||
sh:severity sh:Violation ;
|
||||
sh:message "A Metric states its unit." ] ;
|
||||
|
||||
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." ] .
|
||||
|
||||
|
||||
# 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/0.9/>
|
||||
PREFIX pr: <https://ontology.pernod-ricard.com/metamodel/>
|
||||
SELECT $this WHERE {
|
||||
$this pr:hasStatus "PUBLISHED" .
|
||||
$this pr:arbitrationStatus "TO_ARBITRATE" . }
|
||||
""" ] .
|
||||
$this pr:hasStatus "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." ] .
|
||||
|
||||
|
||||
# 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: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 curated 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:minCount 1 ; sh:maxCount 1 ;
|
||||
sh:severity sh:Violation ;
|
||||
sh:message "A Data Product has exactly one Product Owner." ] ;
|
||||
|
||||
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:realizesMetric ; 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:CuratedObject ;
|
||||
sh:property [
|
||||
sh:path pr:hasName ;
|
||||
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:hasName ;
|
||||
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." ] .
|
||||
|
||||
Reference in New Issue
Block a user