Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c8693b63bb |
File diff suppressed because one or more lines are too long
+109
-49
@@ -1,7 +1,7 @@
|
||||
@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 pr: <https://ontology.pernod-ricard.com/metamodel/0.9/> .
|
||||
@prefix pr: <https://ontology.pernod-ricard.com/metamodel/> .
|
||||
@prefix ex: <https://data.pernod-ricard.com/sodh/> .
|
||||
|
||||
#################################################################
|
||||
@@ -59,37 +59,37 @@ ex:DD_05 a pr:DataDomain ;
|
||||
ex:SD_06_01 a pr:SubDomain ;
|
||||
pr:hasIdentifier "SD-06.01" ; pr:hasName "Sell Out" ;
|
||||
pr:owningDomain ex:DD_06 ; pr:ownedBy ex:ST_DD_06 ;
|
||||
pr:belongsToDomain ex:DD_06 ;
|
||||
pr:belongsTo ex:DD_06 ;
|
||||
pr:hasStatus "PUBLISHED" ; pr:hasVersion "1.0" .
|
||||
ex:SD_10_01 a pr:SubDomain ;
|
||||
pr:hasIdentifier "SD-10.01" ; pr:hasName "Product Structure" ;
|
||||
pr:owningDomain ex:DD_10 ; pr:ownedBy ex:ST_DD_10 ;
|
||||
pr:belongsToDomain ex:DD_10 ;
|
||||
pr:belongsTo ex:DD_10 ;
|
||||
pr:hasStatus "PUBLISHED" ; pr:hasVersion "1.0" .
|
||||
ex:SD_04_01 a pr:SubDomain ;
|
||||
pr:hasIdentifier "SD-04.01" ; pr:hasName "Customer & Outlet" ;
|
||||
pr:owningDomain ex:DD_04 ; pr:ownedBy ex:ST_DD_04 ;
|
||||
pr:belongsToDomain ex:DD_04 ;
|
||||
pr:belongsTo ex:DD_04 ;
|
||||
pr:hasStatus "PUBLISHED" ; pr:hasVersion "1.0" .
|
||||
ex:SD_16_01 a pr:SubDomain ;
|
||||
pr:hasIdentifier "SD-16.01" ; pr:hasName "Currency & Exchange Rates" ;
|
||||
pr:owningDomain ex:DD_16 ; pr:ownedBy ex:ST_DD_16 ;
|
||||
pr:belongsToDomain ex:DD_16 ;
|
||||
pr:belongsTo ex:DD_16 ;
|
||||
pr:hasStatus "PUBLISHED" ; pr:hasVersion "1.0" .
|
||||
ex:SD_16_02 a pr:SubDomain ;
|
||||
pr:hasIdentifier "SD-16.02" ; pr:hasName "Marketing Entity & Fiscal Calendar" ;
|
||||
pr:owningDomain ex:DD_16 ; pr:ownedBy ex:ST_DD_16 ;
|
||||
pr:belongsToDomain ex:DD_16 ;
|
||||
pr:belongsTo ex:DD_16 ;
|
||||
pr:hasStatus "PUBLISHED" ; pr:hasVersion "1.0" .
|
||||
ex:SD_21_01 a pr:SubDomain ;
|
||||
pr:hasIdentifier "SD-21.01" ; pr:hasName "Calendar" ;
|
||||
pr:owningDomain ex:DD_21 ; pr:ownedBy ex:ST_DD_21 ;
|
||||
pr:belongsToDomain ex:DD_21 ;
|
||||
pr:belongsTo ex:DD_21 ;
|
||||
pr:hasStatus "PUBLISHED" ; pr:hasVersion "1.0" .
|
||||
ex:SD_05_01 a pr:SubDomain ;
|
||||
pr:hasIdentifier "SD-05.01" ; pr:hasName "Promotion" ;
|
||||
pr:owningDomain ex:DD_05 ; pr:ownedBy ex:ST_DD_05 ;
|
||||
pr:belongsToDomain ex:DD_05 ;
|
||||
pr:belongsTo ex:DD_05 ;
|
||||
pr:hasStatus "PUBLISHED" ; pr:hasVersion "1.0" .
|
||||
|
||||
ex:BC_06_01_001 a pr:BusinessConcept ;
|
||||
@@ -108,7 +108,7 @@ ex:BC_06_01_003 a pr:BusinessConcept ;
|
||||
pr:hasBusinessDefinition "The scope of retail outlets observed by a panel provider, defining the universe over which distribution is computed." ;
|
||||
pr:hasStatus "PUBLISHED" ; pr:hasVersion "1.0" ; pr:hasSource "SODH Gold" .
|
||||
ex:BC_06_01_004 a pr:BusinessConcept ;
|
||||
pr:hasIdentifier "BC-06.01-004" ; pr:hasName "Distribution" ;
|
||||
pr:hasIdentifier "BC-06.01-004" ; pr:hasName "Retail Distribution" ;
|
||||
pr:owningDomain ex:DD_06 ; pr:ownedBy ex:ST_DD_06 ;
|
||||
pr:hasBusinessDefinition "The presence of a product across retail outlets: the number of points of sale carrying at least one Pernod Ricard product, related to the total number of points of sale in the Pernod Ricard universe." ;
|
||||
pr:hasStatus "PUBLISHED" ; pr:hasVersion "1.0" ; pr:hasSource "SODH Gold" .
|
||||
@@ -118,82 +118,142 @@ ex:BC_05_01_001 a pr:BusinessConcept ;
|
||||
pr:hasBusinessDefinition "A commercial mechanism applied at retail to stimulate sales (for example Temporary Price Reduction, Multibuy, Feature Display)." ;
|
||||
pr:hasStatus "PUBLISHED" ; pr:hasVersion "1.0" ; pr:hasSource "SODH Gold" .
|
||||
|
||||
|
||||
# --- concepts added in v1.1 migration -------------------------------
|
||||
# Six of these belong to other domains: DRAFT definitions proposed by the
|
||||
# DGO, flagged TO_ARBITRATE. OW-007 blocks publication until ratified.
|
||||
ex:BC_06_01_005 a pr:BusinessConcept ;
|
||||
pr:hasIdentifier "BC-06.01-005" ; pr:hasName "Sell Out Baseline" ;
|
||||
pr:owningDomain ex:DD_06 ; pr:ownedBy ex:ST_DD_06 ;
|
||||
pr:hasBusinessDefinition "The modelled level of retail sales that would have occurred without promotional activity, used as the reference against which promotional uplift is measured. Distinct from Sell Out, which is observed rather than modelled." ;
|
||||
pr:hasStatus "DRAFT" ; pr:hasVersion "1.1" ; pr:hasSource "SODH Gold" .
|
||||
ex:BC_10_01_001 a pr:BusinessConcept ;
|
||||
pr:hasIdentifier "BC-10.01-001" ; pr:hasName "Product" ;
|
||||
pr:owningDomain ex:DD_10 ; pr:ownedBy ex:ST_DD_10 ;
|
||||
pr:hasBusinessDefinition "A commercial item sold by Pernod Ricard, identified at Stock Keeping Unit level and positioned in the corporate hierarchy from Brand through Category, Sub-Category and Flavor." ;
|
||||
pr:arbitrationStatus "TO_ARBITRATE" ;
|
||||
pr:hasStatus "DRAFT" ; pr:hasVersion "1.1" ; pr:hasSource "DGO proposal, pending ratification by the owning domain" .
|
||||
ex:BC_04_01_001 a pr:BusinessConcept ;
|
||||
pr:hasIdentifier "BC-04.01-001" ; pr:hasName "Customer" ;
|
||||
pr:owningDomain ex:DD_04 ; pr:ownedBy ex:ST_DD_04 ;
|
||||
pr:hasBusinessDefinition "A commercial counterparty that buys Pernod Ricard products for resale, positioned in the trade hierarchy from Distributor through Tier-1 and Tier-2 Customer. Distinct from Outlet, which is a physical point of sale." ;
|
||||
pr:arbitrationStatus "TO_ARBITRATE" ;
|
||||
pr:hasStatus "DRAFT" ; pr:hasVersion "1.1" ; pr:hasSource "DGO proposal, pending ratification by the owning domain" .
|
||||
ex:BC_04_01_002 a pr:BusinessConcept ;
|
||||
pr:hasIdentifier "BC-04.01-002" ; pr:hasName "Outlet" ;
|
||||
pr:owningDomain ex:DD_04 ; pr:ownedBy ex:ST_DD_04 ;
|
||||
pr:hasBusinessDefinition "A physical point of sale where products are made available to end consumers, characterised by its Channel. An Outlet belongs to a Customer but is not the same notion: one Customer operates many Outlets." ;
|
||||
pr:arbitrationStatus "TO_ARBITRATE" ;
|
||||
pr:hasStatus "DRAFT" ; pr:hasVersion "1.1" ; pr:hasSource "DGO proposal, pending ratification by the owning domain" .
|
||||
ex:BC_16_01_001 a pr:BusinessConcept ;
|
||||
pr:hasIdentifier "BC-16.01-001" ; pr:hasName "Currency" ;
|
||||
pr:owningDomain ex:DD_16 ; pr:ownedBy ex:ST_DD_16 ;
|
||||
pr:hasBusinessDefinition "A monetary unit in which a transaction or a reported amount is denominated, identified by its ISO 4217 code." ;
|
||||
pr:arbitrationStatus "TO_ARBITRATE" ;
|
||||
pr:hasStatus "DRAFT" ; pr:hasVersion "1.1" ; pr:hasSource "DGO proposal, pending ratification by the owning domain" .
|
||||
ex:BC_16_01_002 a pr:BusinessConcept ;
|
||||
pr:hasIdentifier "BC-16.01-002" ; pr:hasName "Exchange Rate" ;
|
||||
pr:owningDomain ex:DD_16 ; pr:ownedBy ex:ST_DD_16 ;
|
||||
pr:hasBusinessDefinition "The conversion factor applied between two currencies at a given date, used to restate local amounts into group reporting currency. Distinct from Currency: a rate is a dated relationship between two of them." ;
|
||||
pr:arbitrationStatus "TO_ARBITRATE" ;
|
||||
pr:hasStatus "DRAFT" ; pr:hasVersion "1.1" ; pr:hasSource "DGO proposal, pending ratification by the owning domain" .
|
||||
ex:BC_21_01_001 a pr:BusinessConcept ;
|
||||
pr:hasIdentifier "BC-21.01-001" ; pr:hasName "Calendar Date" ;
|
||||
pr:owningDomain ex:DD_21 ; pr:ownedBy ex:ST_DD_21 ;
|
||||
pr:hasBusinessDefinition "A single day in the Gregorian calendar, the atomic unit from which Week, Month, Quarter and Year are derived and against which all time-based measures are positioned." ;
|
||||
pr:arbitrationStatus "TO_ARBITRATE" ;
|
||||
pr:hasStatus "DRAFT" ; pr:hasVersion "1.1" ; pr:hasSource "DGO proposal, pending ratification by the owning domain" .
|
||||
|
||||
ex:BO_06_01_001 a pr:BusinessObject ;
|
||||
pr:hasIdentifier "BO-06.01-001" ; pr:hasName "Sell Out Volume" ;
|
||||
pr:owningDomain ex:DD_06 ; pr:ownedBy ex:ST_DD_06 ;
|
||||
pr:belongsToSubDomain ex:SD_06_01 ; pr:hasSteward ex:ST_DD_06 ;
|
||||
pr:composedOf ex:BC_06_01_001 , ex:BC_06_01_003 , ex:BC_05_01_001 ;
|
||||
pr:belongsTo ex:SD_06_01 ; pr:monitoredBy ex:ST_DD_06 ;
|
||||
pr:aboutConcept ex:BC_06_01_001 ;
|
||||
pr:usesConcept ex:BC_06_01_003 , ex:BC_05_01_001 ;
|
||||
pr:hasMetric ex:M_06_01_001 , ex:M_06_01_002 , ex:M_06_01_003 , ex:M_06_01_004 ;
|
||||
pr:hasBusinessDefinition "Quantities of Pernod Ricard products sold by retail customers to end consumers, observed at retail." ;
|
||||
pr:hasStatus "DRAFT" ; pr:hasVersion "0.6" ; pr:hasSource "SODH Gold / F_SELL_OUT" .
|
||||
ex:BO_06_01_002 a pr:BusinessObject ;
|
||||
pr:hasIdentifier "BO-06.01-002" ; pr:hasName "Sell Out Value" ;
|
||||
pr:owningDomain ex:DD_06 ; pr:ownedBy ex:ST_DD_06 ;
|
||||
pr:belongsToSubDomain ex:SD_06_01 ; pr:hasSteward ex:ST_DD_06 ;
|
||||
pr:composedOf ex:BC_06_01_001 , ex:BC_06_01_003 , ex:BC_05_01_001 ;
|
||||
pr:belongsTo ex:SD_06_01 ; pr:monitoredBy ex:ST_DD_06 ;
|
||||
pr:aboutConcept ex:BC_06_01_001 ;
|
||||
pr:usesConcept ex:BC_06_01_003 , ex:BC_05_01_001 , ex:BC_16_01_001 ;
|
||||
pr:hasMetric ex:M_06_01_010 , ex:M_06_01_011 , ex:M_06_01_012 ;
|
||||
pr:hasBusinessDefinition "Monetary value of Pernod Ricard products sold to end consumers, expressed in multiple currencies (Euro, US Dollar, or local currency)." ;
|
||||
pr:hasStatus "DRAFT" ; pr:hasVersion "0.6" ; pr:hasSource "SODH Gold / F_SELL_OUT" .
|
||||
ex:BO_06_01_003 a pr:BusinessObject ;
|
||||
pr:hasIdentifier "BO-06.01-003" ; pr:hasName "Sell Out Distribution" ;
|
||||
pr:owningDomain ex:DD_06 ; pr:ownedBy ex:ST_DD_06 ;
|
||||
pr:belongsToSubDomain ex:SD_06_01 ; pr:hasSteward ex:ST_DD_06 ;
|
||||
pr:composedOf ex:BC_06_01_004 , ex:BC_06_01_003 , ex:BC_06_01_001 ;
|
||||
pr:belongsTo ex:SD_06_01 ; pr:monitoredBy ex:ST_DD_06 ;
|
||||
pr:aboutConcept ex:BC_06_01_004 ;
|
||||
pr:usesConcept ex:BC_06_01_003 ;
|
||||
pr:hasMetric ex:M_06_01_020 , ex:M_06_01_021 , ex:M_06_01_022 ;
|
||||
pr:hasBusinessDefinition "Presence of Pernod Ricard products in retail: the number of points of sale carrying at least one Pernod Ricard product, related to the total number of points of sale in the Pernod Ricard universe." ;
|
||||
pr:hasStatus "DRAFT" ; pr:hasVersion "0.6" ; pr:hasSource "SODH Gold / F_SELL_OUT_DISTRIBUTION" .
|
||||
ex:BO_06_01_004 a pr:BusinessObject ;
|
||||
pr:hasIdentifier "BO-06.01-004" ; pr:hasName "Retailer Stock" ;
|
||||
pr:owningDomain ex:DD_06 ; pr:ownedBy ex:ST_DD_06 ;
|
||||
pr:belongsToSubDomain ex:SD_06_01 ; pr:hasSteward ex:ST_DD_06 ;
|
||||
pr:composedOf ex:BC_06_01_002 , ex:BC_06_01_001 , ex:BC_06_01_003 ;
|
||||
pr:belongsTo ex:SD_06_01 ; pr:monitoredBy ex:ST_DD_06 ;
|
||||
pr:aboutConcept ex:BC_06_01_002 ;
|
||||
pr:usesConcept ex:BC_06_01_001 , ex:BC_06_01_003 ;
|
||||
pr:hasMetric ex:M_06_01_030 , ex:M_06_01_031 , ex:M_06_01_032 ;
|
||||
pr:hasBusinessDefinition "Inventory of Pernod Ricard products held at retailer level: forward stock volume, days of coverage and stock share." ;
|
||||
pr:hasStatus "DRAFT" ; pr:hasVersion "0.6" ; pr:hasSource "SODH Gold / F_SELL_OUT" .
|
||||
ex:BO_06_01_005 a pr:BusinessObject ;
|
||||
pr:hasIdentifier "BO-06.01-005" ; pr:hasName "Sell Out Baseline" ;
|
||||
pr:owningDomain ex:DD_06 ; pr:ownedBy ex:ST_DD_06 ;
|
||||
pr:belongsToSubDomain ex:SD_06_01 ; pr:hasSteward ex:ST_DD_06 ;
|
||||
pr:composedOf ex:BC_06_01_001 , ex:BC_05_01_001 , ex:BC_06_01_003 ;
|
||||
pr:belongsTo ex:SD_06_01 ; pr:monitoredBy ex:ST_DD_06 ;
|
||||
pr:aboutConcept ex:BC_06_01_005 ;
|
||||
pr:usesConcept ex:BC_06_01_001 , ex:BC_05_01_001 ;
|
||||
pr:hasMetric ex:M_06_01_040 , ex:M_06_01_041 ;
|
||||
pr:hasBusinessDefinition "Modelled non-promoted volume and the incremental effect attributed to promotions, computed from observed Sell Out." ;
|
||||
pr:hasStatus "DRAFT" ; pr:hasVersion "0.6" ; pr:hasSource "SODH Gold / F_SELL_OUT_BASELINE" .
|
||||
ex:BO_10_01_001 a pr:BusinessObject ;
|
||||
pr:hasIdentifier "BO-10.01-001" ; pr:hasName "Product Structure" ;
|
||||
pr:owningDomain ex:DD_10 ; pr:ownedBy ex:ST_DD_10 ;
|
||||
pr:belongsToSubDomain ex:SD_10_01 ; pr:hasSteward ex:ST_DD_10 ;
|
||||
pr:composedOf ex:BC_06_01_001 , ex:BC_06_01_003 , ex:BC_06_01_004 ;
|
||||
pr:hasBusinessDefinition "The corporate product hierarchy from Brand down to Stock Keeping Unit, with Category, Sub-Category, Flavor and unit-of-measure standards. Owned by Product & Material; consumed by SODH." ;
|
||||
pr:belongsTo ex:SD_10_01 ; pr:monitoredBy ex:ST_DD_10 ;
|
||||
pr:aboutConcept ex:BC_10_01_001 ;
|
||||
pr:hasStatus "DRAFT" ; pr:hasVersion "0.6" ; pr:hasSource "MDM Product -> SODH [concepts TBC by DD-10]" .
|
||||
ex:BO_04_01_001 a pr:BusinessObject ;
|
||||
pr:hasIdentifier "BO-04.01-001" ; pr:hasName "Customer & Outlet" ;
|
||||
pr:hasIdentifier "BO-04.01-001" ; pr:hasName "Customer" ;
|
||||
pr:owningDomain ex:DD_04 ; pr:ownedBy ex:ST_DD_04 ;
|
||||
pr:belongsToSubDomain ex:SD_04_01 ; pr:hasSteward ex:ST_DD_04 ;
|
||||
pr:composedOf ex:BC_06_01_001 , ex:BC_06_01_003 , ex:BC_06_01_004 ;
|
||||
pr:hasBusinessDefinition "Retail customer hierarchy: Distributor, Tier-1 and Tier-2 Customer, Outlet and Channel. Owned by Customer & Channels; consumed by SODH." ;
|
||||
pr:hasStatus "DRAFT" ; pr:hasVersion "0.6" ; pr:hasSource "MDM Customer -> SODH [concepts TBC by DD-04]" .
|
||||
pr:belongsTo ex:SD_04_01 ; pr:monitoredBy ex:ST_DD_04 ;
|
||||
pr:aboutConcept ex:BC_04_01_001 ;
|
||||
pr:hasStatus "DRAFT" ; pr:hasVersion "0.6" ; pr:hasSource "MDM -> SODH [split proposed, subject arbitrated 2026-07-27]" .
|
||||
ex:BO_16_01_001 a pr:BusinessObject ;
|
||||
pr:hasIdentifier "BO-16.01-001" ; pr:hasName "Currency & Exchange Rates" ;
|
||||
pr:hasIdentifier "BO-16.01-001" ; pr:hasName "Currency" ;
|
||||
pr:owningDomain ex:DD_16 ; pr:ownedBy ex:ST_DD_16 ;
|
||||
pr:belongsToSubDomain ex:SD_16_01 ; pr:hasSteward ex:ST_DD_16 ;
|
||||
pr:composedOf ex:BC_06_01_001 , ex:BC_06_01_003 , ex:BC_06_01_004 ;
|
||||
pr:hasBusinessDefinition "Currency codes and exchange rates used to convert local Sell Out Value into Euro or US Dollar for group reporting. Owned by Financial Performance; consumed by SODH." ;
|
||||
pr:hasStatus "DRAFT" ; pr:hasVersion "0.6" ; pr:hasSource "Group Finance -> SODH [concepts TBC by DD-16]" .
|
||||
pr:belongsTo ex:SD_16_01 ; pr:monitoredBy ex:ST_DD_16 ;
|
||||
pr:aboutConcept ex:BC_16_01_001 ;
|
||||
pr:hasStatus "DRAFT" ; pr:hasVersion "0.6" ; pr:hasSource "MDM -> SODH [split proposed, subject arbitrated 2026-07-27]" .
|
||||
ex:BO_21_01_001 a pr:BusinessObject ;
|
||||
pr:hasIdentifier "BO-21.01-001" ; pr:hasName "Calendar" ;
|
||||
pr:owningDomain ex:DD_21 ; pr:ownedBy ex:ST_DD_21 ;
|
||||
pr:belongsToSubDomain ex:SD_21_01 ; pr:hasSteward ex:ST_DD_21 ;
|
||||
pr:composedOf ex:BC_06_01_001 , ex:BC_06_01_003 , ex:BC_06_01_004 ;
|
||||
pr:hasBusinessDefinition "The Gregorian calendar dimension shared across all domains: Date, Day, Week, Month, Quarter and Public Holiday. Owned by Data Governance; consumed by SODH." ;
|
||||
pr:belongsTo ex:SD_21_01 ; pr:monitoredBy ex:ST_DD_21 ;
|
||||
pr:aboutConcept ex:BC_21_01_001 ;
|
||||
pr:hasStatus "DRAFT" ; pr:hasVersion "0.6" ; pr:hasSource "Standard Calendar -> SODH [concepts TBC by DD-21]" .
|
||||
ex:BO_05_01_001 a pr:BusinessObject ;
|
||||
pr:hasIdentifier "BO-05.01-001" ; pr:hasName "Promotion Type" ;
|
||||
pr:owningDomain ex:DD_05 ; pr:ownedBy ex:ST_DD_05 ;
|
||||
pr:belongsToSubDomain ex:SD_05_01 ; pr:hasSteward ex:ST_DD_05 ;
|
||||
pr:composedOf ex:BC_05_01_001 , ex:BC_06_01_001 , ex:BC_06_01_004 ;
|
||||
pr:hasBusinessDefinition "The controlled taxonomy of promotion mechanisms used to segment Sell Out measures. Owned by Sales Activation; consumed by SODH." ;
|
||||
pr:belongsTo ex:SD_05_01 ; pr:monitoredBy ex:ST_DD_05 ;
|
||||
pr:aboutConcept ex:BC_05_01_001 ;
|
||||
pr:hasStatus "DRAFT" ; pr:hasVersion "0.6" ; pr:hasSource "Sales Activation -> SODH [concepts TBC by DD-05]" .
|
||||
|
||||
|
||||
# --- Business Objects from the v1.1 splits ---------------------------
|
||||
# 'Customer & Outlet' and 'Currency & Exchange Rates' had no single
|
||||
# subject. Split proposed to DD-04 and DD-16, hence TO_ARBITRATE.
|
||||
ex:BO_04_01_002 a pr:BusinessObject ;
|
||||
pr:hasIdentifier "BO-04.01-002" ; pr:hasName "Outlet" ;
|
||||
pr:owningDomain ex:DD_04 ; pr:ownedBy ex:ST_DD_04 ;
|
||||
pr:belongsTo ex:SD_04_01 ; pr:monitoredBy ex:ST_DD_04 ;
|
||||
pr:aboutConcept ex:BC_04_01_002 ;
|
||||
pr:arbitrationStatus "TO_ARBITRATE" ;
|
||||
pr:hasStatus "DRAFT" ; pr:hasVersion "1.1" ; pr:hasSource "MDM Customer -> SODH [split from BO-04.01-001, proposed to DD-04]" .
|
||||
ex:BO_16_01_002 a pr:BusinessObject ;
|
||||
pr:hasIdentifier "BO-16.01-002" ; pr:hasName "Exchange Rate" ;
|
||||
pr:owningDomain ex:DD_16 ; pr:ownedBy ex:ST_DD_16 ;
|
||||
pr:belongsTo ex:SD_16_01 ; pr:monitoredBy ex:ST_DD_16 ;
|
||||
pr:aboutConcept ex:BC_16_01_002 ;
|
||||
pr:arbitrationStatus "TO_ARBITRATE" ;
|
||||
pr:hasStatus "DRAFT" ; pr:hasVersion "1.1" ; pr:hasSource "Group Finance -> SODH [split from BO-16.01-001, proposed to DD-16]" .
|
||||
|
||||
ex:M_06_01_001 a pr:Metric ;
|
||||
pr:hasIdentifier "M-06.01-001" ; pr:hasName "Sell Out Volume (9L)" ;
|
||||
pr:owningDomain ex:DD_06 ; pr:ownedBy ex:ST_DD_06 ;
|
||||
@@ -527,38 +587,38 @@ ex:DE_05_01_0004 a pr:DataElement ;
|
||||
ex:DO_06_01_001 a pr:DataObject ;
|
||||
pr:hasIdentifier "DO-06.01-001" ; pr:hasName "Sell Out Volume Dataset" ;
|
||||
pr:owningDomain ex:DD_06 ; pr:ownedBy ex:ST_DD_06 ;
|
||||
pr:implements ex:BO_06_01_001 ; pr:hasSteward ex:ST_DD_06 ;
|
||||
pr:represents ex:BO_06_01_001 ; pr:monitoredBy ex:ST_DD_06 ;
|
||||
pr:hasElement ex:DE_05_01_0001 , ex:DE_05_01_0002 , ex:DE_05_01_0003 , ex:DE_05_01_0004 ;
|
||||
pr:references ex:DO_04_01_001 , ex:DO_10_01_001 , ex:DO_21_01_001 , ex:DO_16_01_001 ;
|
||||
pr:hasStatus "DRAFT" ; pr:hasVersion "0.6" ; pr:hasSource "SODH Gold / F_SELL_OUT" .
|
||||
ex:DO_06_01_003 a pr:DataObject ;
|
||||
pr:hasIdentifier "DO-06.01-003" ; pr:hasName "Sell Out Distribution Dataset" ;
|
||||
pr:owningDomain ex:DD_06 ; pr:ownedBy ex:ST_DD_06 ;
|
||||
pr:implements ex:BO_06_01_003 ; pr:hasSteward ex:ST_DD_06 ;
|
||||
pr:represents ex:BO_06_01_003 ; pr:monitoredBy ex:ST_DD_06 ;
|
||||
pr:hasElement ex:DE_05_01_0001 , ex:DE_05_01_0002 , ex:DE_05_01_0003 , ex:DE_05_01_0004 ;
|
||||
pr:references ex:DO_04_01_001 , ex:DO_10_01_001 , ex:DO_21_01_001 ;
|
||||
pr:hasStatus "DRAFT" ; pr:hasVersion "0.6" ; pr:hasSource "SODH Gold / F_SELL_OUT_DISTRIBUTION" .
|
||||
ex:DO_10_01_001 a pr:DataObject ;
|
||||
pr:hasIdentifier "DO-10.01-001" ; pr:hasName "Product Dimension" ;
|
||||
pr:owningDomain ex:DD_10 ; pr:ownedBy ex:ST_DD_10 ;
|
||||
pr:implements ex:BO_10_01_001 ; pr:hasSteward ex:ST_DD_10 ;
|
||||
pr:represents ex:BO_10_01_001 ; pr:monitoredBy ex:ST_DD_10 ;
|
||||
pr:hasElement ex:DE_10_01_0001 , ex:DE_10_01_0002 , ex:DE_10_01_0003 , ex:DE_10_01_0004 , ex:DE_10_01_0005 , ex:DE_10_01_0006 , ex:DE_10_01_0007 , ex:DE_10_01_0008 , ex:DE_10_01_0009 , ex:DE_10_01_0010 , ex:DE_10_01_0011 , ex:DE_10_01_0012 ;
|
||||
pr:hasStatus "DRAFT" ; pr:hasVersion "0.6" ; pr:hasSource "SODH Gold / D_FINISHED_PRODUCT" .
|
||||
ex:DO_04_01_001 a pr:DataObject ;
|
||||
pr:hasIdentifier "DO-04.01-001" ; pr:hasName "Customer Dimension" ;
|
||||
pr:owningDomain ex:DD_04 ; pr:ownedBy ex:ST_DD_04 ;
|
||||
pr:implements ex:BO_04_01_001 ; pr:hasSteward ex:ST_DD_04 ;
|
||||
pr:represents ex:BO_04_01_001 ; pr:monitoredBy ex:ST_DD_04 ;
|
||||
pr:hasElement ex:DE_04_01_0001 , ex:DE_04_01_0002 , ex:DE_04_01_0003 , ex:DE_04_01_0004 , ex:DE_04_01_0005 , ex:DE_04_01_0006 , ex:DE_04_01_0007 , ex:DE_04_01_0008 , ex:DE_04_01_0009 , ex:DE_04_01_0010 ;
|
||||
pr:hasStatus "DRAFT" ; pr:hasVersion "0.6" ; pr:hasSource "SODH Gold / D_CUSTOMER" .
|
||||
ex:DO_16_01_001 a pr:DataObject ;
|
||||
pr:hasIdentifier "DO-16.01-001" ; pr:hasName "Currency Dimension" ;
|
||||
pr:owningDomain ex:DD_16 ; pr:ownedBy ex:ST_DD_16 ;
|
||||
pr:implements ex:BO_16_01_001 ; pr:hasSteward ex:ST_DD_16 ;
|
||||
pr:represents ex:BO_16_01_001 ; pr:monitoredBy ex:ST_DD_16 ;
|
||||
pr:hasElement ex:DE_16_01_0001 , ex:DE_16_01_0002 , ex:DE_16_01_0003 , ex:DE_16_02_0001 , ex:DE_16_02_0002 , ex:DE_16_02_0003 ;
|
||||
pr:hasStatus "DRAFT" ; pr:hasVersion "0.6" ; pr:hasSource "SODH Gold / D_CURRENCY" .
|
||||
ex:DO_21_01_001 a pr:DataObject ;
|
||||
pr:hasIdentifier "DO-21.01-001" ; pr:hasName "Calendar Dimension" ;
|
||||
pr:owningDomain ex:DD_21 ; pr:ownedBy ex:ST_DD_21 ;
|
||||
pr:implements ex:BO_21_01_001 ; pr:hasSteward ex:ST_DD_21 ;
|
||||
pr:represents ex:BO_21_01_001 ; pr:monitoredBy ex:ST_DD_21 ;
|
||||
pr:hasElement ex:DE_21_01_0001 , ex:DE_21_01_0002 , ex:DE_21_01_0003 , ex:DE_21_01_0004 , ex:DE_21_01_0005 ;
|
||||
pr:hasStatus "DRAFT" ; pr:hasVersion "0.6" ; pr:hasSource "SODH Gold / D_DATE" .
|
||||
|
||||
+241
-55
@@ -7,11 +7,79 @@
|
||||
@prefix pr: <https://ontology.pernod-ricard.com/metamodel/> .
|
||||
|
||||
#################################################################
|
||||
# PERNOD RICARD DATA META MODEL - T-Box v1.0
|
||||
# PERNOD RICARD DATA META MODEL - T-Box v1.1
|
||||
# Owner: Bastien Gourdon - Data Governance Office
|
||||
#
|
||||
# WHAT CHANGED vs v0.9.2
|
||||
# ----------------------
|
||||
# WHAT CHANGED vs v1.0
|
||||
# --------------------
|
||||
# A. RELATIONS SIMPLIFIED AND GIVEN HONEST NAMES
|
||||
# belongsToDomain + belongsToSubDomain -> belongsTo (one property,
|
||||
# scoped per class by SHACL; reusable if a domain family appears).
|
||||
# implements -> represents
|
||||
# composedOf -> aboutConcept (exactly 1, the
|
||||
# subject) + usesConcept (0..n, notions needed to read the object).
|
||||
# composedOf carried three different meanings depending on the object
|
||||
# (dependency, subject, part-whole); that vagueness is what let BR-004
|
||||
# be satisfied with padding in v0.6.
|
||||
#
|
||||
# B. represents WORKS AT TWO GRANULARITIES
|
||||
# DataObject represents BusinessObject
|
||||
# DataElement represents BusinessConcept
|
||||
# Same relation -- "this logical artefact is the data-side counterpart of
|
||||
# that business notion" -- at two levels of detail.
|
||||
# A Data Element reaches business meaning by EXACTLY ONE of two routes:
|
||||
# dimensional -> represents a BusinessConcept
|
||||
# measure -> is computedBy a Metric, which measures the Concept
|
||||
# Never both, never neither. Enforced by sh:xone.
|
||||
#
|
||||
# C. STEWARDSHIP DECLARED ONCE, INHERITED EVERYWHERE
|
||||
# monitoredBy sits on BusinessObject only (plus DataProduct, which
|
||||
# spans several Business Objects and cannot inherit). Data Objects and
|
||||
# Data Elements inherit through represents / hasElement. Declaring a
|
||||
# steward at several levels means two answers to "who watches this
|
||||
# column". BusinessObject therefore STAYS in the Ownership layer: it is
|
||||
# the single anchor of accountability, which is what that layer is for.
|
||||
# owningDomain is the exception: inherited by default, but re-declarable
|
||||
# on a Data Element, because OW-002 attributes an element to the domain
|
||||
# that defines its rule. A DE that re-declares is a borrowed element --
|
||||
# a signal to check against its represents, not an anomaly.
|
||||
#
|
||||
# D. BUSINESS MEANING LIVES IN ONE PLACE
|
||||
# hasBusinessDefinition is removed from BusinessObject. Meaning belongs to
|
||||
# the BusinessConcept; a Business Object gets its meaning through
|
||||
# aboutConcept. Two definitions for one notion are two definitions free to
|
||||
# diverge.
|
||||
#
|
||||
# E. GRAIN MOVED OFF THE METRIC
|
||||
# hasGrainDimension (Metric -> DataObject) and hasGranularity are gone.
|
||||
# Grain is a property of a DATASET, not of a calculation rule: a Metric
|
||||
# has no rows. hasGrainElement (DataObject -> DataElement) replaces them
|
||||
# and is CHECKABLE against the harvested PrimaryKey. A Metric's grain
|
||||
# becomes derivable from where it lands.
|
||||
#
|
||||
# F. DELIVERY IS ITS OWN LAYER
|
||||
# DataProduct / DataInterface / DataContract answer "what do we promise,
|
||||
# to whom, until when" -- a different question from "how is it
|
||||
# structured". DataAsset is renamed DataInterface and redefined as the
|
||||
# CHANNEL through which a consumer actually gets data (a Snowflake share
|
||||
# or grant, a file extract, an API), which is verifiable, rather than
|
||||
# "directly consumable by a business user", which was a judgement call and
|
||||
# produced zero instances in a year.
|
||||
# Every Data Product is operated by exactly one Sub-Domain and has exactly
|
||||
# one Product Owner: it is a consumable, and there must be no ambiguity
|
||||
# about who runs it.
|
||||
#
|
||||
# G. realizesMetric KEPT AS A TRANSITIONAL CONTROL
|
||||
# Declared, required on certified BI fields, and that is all. No rule is
|
||||
# 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.
|
||||
#
|
||||
# Deprecated terms are kept, never deleted (LC-002): existing assertions keep
|
||||
# classifying, and the migration can be staged.
|
||||
#
|
||||
# WHAT CHANGED vs v0.9.2 (kept for reference)
|
||||
# -------------------------------------------
|
||||
# 1. TERM NAMESPACE IS NOW VERSION-INDEPENDENT.
|
||||
# Terms live at .../metamodel/ (no version segment).
|
||||
# The VERSION lives in owl:versionIRI only.
|
||||
@@ -49,13 +117,13 @@
|
||||
|
||||
<https://ontology.pernod-ricard.com/metamodel/>
|
||||
a owl:Ontology ;
|
||||
owl:versionIRI <https://ontology.pernod-ricard.com/metamodel/1.0> ;
|
||||
owl:priorVersion <https://ontology.pernod-ricard.com/metamodel/0.9> ;
|
||||
owl:versionIRI <https://ontology.pernod-ricard.com/metamodel/1.1> ;
|
||||
owl:priorVersion <https://ontology.pernod-ricard.com/metamodel/1.0> ;
|
||||
dcterms:title "Pernod Ricard Data Meta Model" ;
|
||||
dcterms:creator "Bastien Gourdon" ;
|
||||
dcterms:modified "2026-07-25"^^xsd:date ;
|
||||
owl:versionInfo "1.0.0-draft-2026-07-25" ;
|
||||
rdfs:comment """OWL T-Box. Five layers (Ownership, Business, Logical, Physical, Consumption) crossed with a provenance axis (Curated vs Observed). Curated objects are asserted by governance and reviewed; Observed objects are harvested from Snowflake, Tableau and Looker and are rebuilt, never edited. Bridge annotation properties are declared for future alignment with domain ontologies.""" .
|
||||
dcterms:modified "2026-07-26"^^xsd:date ;
|
||||
owl:versionInfo "1.1.0-draft-2026-07-26" ;
|
||||
rdfs:comment """OWL T-Box. Six layers (Ownership, Business, Logical, Delivery, Physical, Consumption) crossed with a provenance axis (Curated vs Observed). Curated objects are asserted by governance and reviewed; Observed objects are harvested from Snowflake, Tableau and Looker and are rebuilt, never edited. Bridge annotation properties are declared for future alignment with domain ontologies.""" .
|
||||
|
||||
|
||||
#################################################################
|
||||
@@ -115,7 +183,11 @@ pr:OwnershipLayerObject a owl:Class ; rdfs:subClassOf pr:CuratedObject ;
|
||||
pr:BusinessLayerObject a owl:Class ; rdfs:subClassOf pr:CuratedObject ;
|
||||
rdfs:label "Business Layer Object" .
|
||||
pr:LogicalLayerObject a owl:Class ; rdfs:subClassOf pr:CuratedObject ;
|
||||
rdfs:label "Logical Layer Object" .
|
||||
rdfs:label "Logical Layer Object" ;
|
||||
rdfs:comment "How the data is structured." .
|
||||
pr:DeliveryLayerObject a owl:Class ; rdfs:subClassOf pr:CuratedObject ;
|
||||
rdfs:label "Delivery Layer Object" ;
|
||||
rdfs:comment "What we promise, to whom, until when. A different question from how the data is structured, hence a layer of its own: version, owner, changelog, support channel, consumers to warn." .
|
||||
pr:PhysicalLayerObject a owl:Class ; rdfs:subClassOf pr:ObservedObject ;
|
||||
rdfs:label "Physical Layer Object" .
|
||||
pr:ConsumptionLayerObject a owl:Class ; rdfs:subClassOf pr:ObservedObject ;
|
||||
@@ -135,7 +207,7 @@ pr:SubDomain a owl:Class ; rdfs:subClassOf pr:OwnershipLayerObject ;
|
||||
|
||||
pr:BusinessObject a owl:Class ; rdfs:subClassOf pr:OwnershipLayerObject ;
|
||||
rdfs:label "Business Object" ;
|
||||
rdfs:comment "The unit of ownership attribution. Sits in the Ownership layer rather than the Business layer on purpose: a Business Object is what a Data Domain is accountable for, and what a Data Object implements. Its business meaning is carried by the Business Concepts it groups." .
|
||||
rdfs:comment "The single anchor of accountability, which is why it stays in the Ownership layer: it is the one object where a steward is declared, and Data Objects and Data Elements inherit from it. It carries NO business definition of its own -- its meaning comes from its aboutConcept. What it does carry is a perimeter: the subject it is about, and the notions needed to read it." .
|
||||
|
||||
|
||||
#################################################################
|
||||
@@ -161,7 +233,7 @@ pr:KPI a owl:Class ; rdfs:subClassOf pr:Metric ;
|
||||
|
||||
pr:DataObject a owl:Class ; rdfs:subClassOf pr:LogicalLayerObject ;
|
||||
rdfs:label "Data Object" ;
|
||||
rdfs:comment "The pivot: implements exactly one Business Object, is composed of Data Elements, and is materialized as one or more Physical Relations. The single junction between governance and technology." .
|
||||
rdfs:comment "The pivot: represents exactly one Business Object, is composed of Data Elements, and is materialized as one or more Physical Relations. The single junction between governance and technology." .
|
||||
|
||||
pr:DataElement a owl:Class ; rdfs:subClassOf pr:LogicalLayerObject ;
|
||||
rdfs:label "Data Element" ;
|
||||
@@ -169,20 +241,31 @@ pr:DataElement a owl:Class ; rdfs:subClassOf pr:LogicalLayerObject ;
|
||||
rdfs:comment "The finest curated unit. Its mapping to a physical Field is the ONLY place where the curated graph and the observed graph meet: proposed automatically by name and type matching, confirmed by a Data Steward." .
|
||||
|
||||
pr:DataAsset a owl:Class ; rdfs:subClassOf pr:LogicalLayerObject ;
|
||||
rdfs:label "Data Asset" ;
|
||||
rdfs:comment "Directly consumable by a business user. A reusable building block is a Data Object, not a Data Asset (rule BR-009)." .
|
||||
|
||||
pr:DataProduct a owl:Class ; rdfs:subClassOf pr:LogicalLayerObject ;
|
||||
rdfs:label "Data Product" ;
|
||||
rdfs:comment "The unit of delivery: packages Data Assets and is governed by a Data Contract. Candidate alignment: dcat:Dataset / DPROD." .
|
||||
|
||||
pr:DataContract a owl:Class ; rdfs:subClassOf pr:LogicalLayerObject ;
|
||||
rdfs:label "Data Contract" ;
|
||||
rdfs:comment "The service commitment attached to a Data Product: schema, freshness, quality thresholds, support. Candidate alignment: ODCS (Bitol)." .
|
||||
rdfs:label "Data Asset (deprecated)" ;
|
||||
owl:deprecated true ;
|
||||
dcterms:isReplacedBy pr:DataInterface ;
|
||||
rdfs:comment "v1.0 term, defined as 'directly consumable by a business user' -- a judgement, not a structural test, which is why it collected zero instances in a year. Use DataInterface." .
|
||||
|
||||
|
||||
#################################################################
|
||||
# LAYER 4 - PHYSICAL (observed)
|
||||
# LAYER 4 - DELIVERY (curated)
|
||||
#################################################################
|
||||
|
||||
pr:DataProduct a owl:Class ; rdfs:subClassOf pr:DeliveryLayerObject ;
|
||||
rdfs:label "Data Product" ;
|
||||
rdfs:comment "The unit of delivery: what carries a version number, an owner, a changelog, a support channel and a list of consumers to warn. It has no single technical counterpart, and should not: physically it often lands on a Snowflake schema or a dbt project, but that is not what defines it. Operated by exactly one Sub-Domain -- it is a consumable, and there must be no ambiguity about who runs it. Candidate alignment: dcat:Dataset / DPROD." .
|
||||
|
||||
pr:DataInterface a owl:Class ; rdfs:subClassOf pr:DeliveryLayerObject ;
|
||||
rdfs:label "Data Interface" ;
|
||||
rdfs:comment "The channel through which a consumer actually gets the data: a Snowflake secure share or listing, a consumption schema with its grants, a file extract to a panel partner, an API or reverse-ETL feed. Not a Data Object (that is structure) and not a BI Data Source (that is one consumer among others). Verifiable: a grant, a share or an endpoint either exists or it does not. The output port in data-mesh terms." .
|
||||
|
||||
pr:DataContract a owl:Class ; rdfs:subClassOf pr:DeliveryLayerObject ;
|
||||
rdfs:label "Data Contract" ;
|
||||
rdfs:comment "The service commitment attached to a Data Product: schema, freshness, quality thresholds, support, breaking-change notice period. Candidate alignment: ODCS (Bitol)." .
|
||||
|
||||
|
||||
#################################################################
|
||||
# LAYER 5 - PHYSICAL (observed)
|
||||
#################################################################
|
||||
|
||||
pr:System a owl:Class ; rdfs:subClassOf pr:PhysicalLayerObject ;
|
||||
@@ -246,7 +329,7 @@ pr:Transformation a owl:Class ; rdfs:subClassOf pr:PhysicalLayerObject , prov:Ac
|
||||
|
||||
|
||||
#################################################################
|
||||
# LAYER 5 - CONSUMPTION (observed)
|
||||
# LAYER 6 - CONSUMPTION (observed)
|
||||
#################################################################
|
||||
|
||||
pr:BIWorkspace a owl:Class ; rdfs:subClassOf pr:ConsumptionLayerObject ;
|
||||
@@ -276,9 +359,26 @@ pr:BIReport a owl:Class ; rdfs:subClassOf pr:ConsumptionLayerObject ;
|
||||
pr:Actor a owl:Class ; rdfs:label "Actor" ;
|
||||
rdfs:comment "A person or a team. Deliberately NOT a MetaModelObject: people are not subject to ownership and lifecycle rules." .
|
||||
|
||||
pr:DataDomainOwner a owl:Class ; rdfs:subClassOf pr:Actor ; rdfs:label "Data Domain Owner" .
|
||||
pr:DataGovernanceLead a owl:Class ; rdfs:subClassOf pr:Actor ; rdfs:label "Data Governance Lead (DGL)" .
|
||||
pr:DataSteward a owl:Class ; rdfs:subClassOf pr:Actor ; rdfs:label "Data Steward" .
|
||||
pr:DataDomainOwner a owl:Class ; rdfs:subClassOf pr:Actor ;
|
||||
rdfs:label "Data Domain Owner" ;
|
||||
rdfs:comment "Accountable for a Data Domain. Exactly one per domain." .
|
||||
|
||||
pr:SubDomainOwner a owl:Class ; rdfs:subClassOf pr:Actor ;
|
||||
rdfs:label "Sub-Domain Owner" ;
|
||||
rdfs:comment "Accountable for a Sub-Domain. Exactly one per sub-domain." .
|
||||
|
||||
pr:DataGovernanceLead a owl:Class ; rdfs:subClassOf pr:Actor ;
|
||||
rdfs:label "Data Governance Lead" ;
|
||||
pr:hasAcronym "DGL" ;
|
||||
rdfs:comment "Runs governance for a Data Domain. The acronym belongs in hasAcronym, not in the label (rule NR-005)." .
|
||||
|
||||
pr:DataProductOwner a owl:Class ; rdfs:subClassOf pr:Actor ;
|
||||
rdfs:label "Data Product Owner" ;
|
||||
rdfs:comment "Runs a Data Product day to day: releases, changelog, support, consumer communication. Belongs to the Sub-Domain that operates the product." .
|
||||
|
||||
pr:DataSteward a owl:Class ; rdfs:subClassOf pr:Actor ;
|
||||
rdfs:label "Data Steward" ;
|
||||
rdfs:comment "Monitors a Business Object. Declared there and nowhere else: Data Objects and Data Elements inherit their steward, so that no column can ever have two answers to 'who watches this'." .
|
||||
|
||||
|
||||
#################################################################
|
||||
@@ -308,23 +408,36 @@ pr:ExternalProvider a pr:SystemType ; rdfs:label "External Provider" .
|
||||
# OBJECT PROPERTIES - Ownership & Business
|
||||
#################################################################
|
||||
|
||||
pr:belongsTo a owl:ObjectProperty , owl:FunctionalProperty ;
|
||||
rdfs:label "belongs to" ;
|
||||
rdfs:comment "Position in the categorization tree. One property for the whole tree: SubDomain belongs to DataDomain, BusinessObject belongs to SubDomain. Scope is enforced per class by SHACL rather than by two near-identical properties, and the same relation will serve if a domain family ever appears. Deliberately NOT transitive: entailment would produce two values and break the maxCount 1 shape. To walk the ancestry, use the property path belongsTo+ in the query." .
|
||||
|
||||
pr:belongsToDomain a owl:ObjectProperty , owl:FunctionalProperty ;
|
||||
rdfs:label "belongs to domain" ;
|
||||
rdfs:domain pr:SubDomain ; rdfs:range pr:DataDomain .
|
||||
rdfs:label "belongs to domain (deprecated)" ;
|
||||
owl:deprecated true ; dcterms:isReplacedBy pr:belongsTo .
|
||||
|
||||
pr:belongsToSubDomain a owl:ObjectProperty , owl:FunctionalProperty ;
|
||||
rdfs:label "belongs to sub-domain" ;
|
||||
rdfs:domain pr:BusinessObject ; rdfs:range pr:SubDomain .
|
||||
rdfs:label "belongs to sub-domain (deprecated)" ;
|
||||
owl:deprecated true ; dcterms:isReplacedBy pr:belongsTo .
|
||||
|
||||
pr:aboutConcept a owl:ObjectProperty , owl:FunctionalProperty ;
|
||||
rdfs:label "is about" ;
|
||||
rdfs:domain pr:BusinessObject ; rdfs:range pr:BusinessConcept ;
|
||||
rdfs:comment "The single subject of this Business Object, and where its meaning comes from -- a Business Object carries no definition of its own. Exactly one. If the subject cannot be named, the object is not scoped, and that is the most useful thing the rule can tell you. Replaces the 3..10 minimum of BR-004, which was satisfied in v0.6 by attaching unrelated concepts." .
|
||||
|
||||
pr:usesConcept a owl:ObjectProperty ;
|
||||
rdfs:label "uses concept" ;
|
||||
rdfs:domain pr:BusinessObject ; rdfs:range pr:BusinessConcept ;
|
||||
rdfs:comment "The other notions required to read this object correctly. Zero or more, and zero is a legitimate answer." .
|
||||
|
||||
pr:composedOf a owl:ObjectProperty ;
|
||||
rdfs:label "composed of" ;
|
||||
rdfs:domain pr:BusinessObject ; rdfs:range pr:BusinessConcept ;
|
||||
owl:inverseOf pr:isComponentOf ;
|
||||
rdfs:comment "Part-whole. In v0.9 this was declared owl:inverseOf pr:describes, which was semantically wrong: composition and aboutness are different relations, and a reasoner would infer that every Concept describing an Object is also a component of it." .
|
||||
rdfs:label "composed of (deprecated)" ;
|
||||
owl:deprecated true ; dcterms:isReplacedBy pr:usesConcept ;
|
||||
rdfs:comment "Carried three different meanings depending on the object: dependency (Sell Out Volume), subject (Retailer Stock), part-whole (Calendar). One word for three relations is what allowed the v0.6 padding. Split into aboutConcept and usesConcept." .
|
||||
|
||||
pr:isComponentOf a owl:ObjectProperty ;
|
||||
rdfs:label "is component of" ;
|
||||
rdfs:domain pr:BusinessConcept ; rdfs:range pr:BusinessObject .
|
||||
rdfs:label "is component of (deprecated)" ;
|
||||
owl:deprecated true ; dcterms:isReplacedBy pr:usesConcept .
|
||||
|
||||
pr:describes a owl:ObjectProperty ;
|
||||
rdfs:label "describes (deprecated)" ;
|
||||
@@ -340,10 +453,15 @@ pr:measures a owl:ObjectProperty ;
|
||||
rdfs:label "measures" ;
|
||||
rdfs:domain pr:Metric ; rdfs:range pr:BusinessConcept .
|
||||
|
||||
pr:hasGrainElement a owl:ObjectProperty ;
|
||||
rdfs:label "has grain element" ;
|
||||
rdfs:domain pr:DataObject ; rdfs:range pr:DataElement ;
|
||||
rdfs:comment "The elements whose combination makes a row unique. Grain belongs to a DATASET, not to a calculation rule: a Metric has no rows, so it cannot have a grain of its own -- its grain is derivable from the Data Objects it lands in. Stated at element level rather than object level because it is then CHECKABLE against the harvested PrimaryKey: a declared grain that does not match the real key is an automatic finding." .
|
||||
|
||||
pr:hasGrainDimension a owl:ObjectProperty ;
|
||||
rdfs:label "has grain dimension" ;
|
||||
rdfs:domain pr:Metric ; rdfs:range pr:DataObject ;
|
||||
rdfs:comment "The dimensional Data Objects that define the Metric's grain. Replaces reading grain out of the free-text hasGranularity string: only a real reference lets you detect that two Metrics cannot be aggregated together." .
|
||||
rdfs:label "has grain dimension (deprecated)" ;
|
||||
owl:deprecated true ; dcterms:isReplacedBy pr:hasGrainElement ;
|
||||
rdfs:comment "Put grain on the Metric, which confounded a declarative statement with a structural fact. See hasGrainElement." .
|
||||
|
||||
pr:computedBy a owl:ObjectProperty ;
|
||||
rdfs:subPropertyOf prov:wasDerivedFrom ;
|
||||
@@ -356,10 +474,22 @@ pr:computedBy a owl:ObjectProperty ;
|
||||
# OBJECT PROPERTIES - Logical
|
||||
#################################################################
|
||||
|
||||
pr:represents a owl:ObjectProperty ;
|
||||
rdfs:label "represents" ;
|
||||
rdfs:comment """The business notion a logical artefact is the data-side counterpart of. One relation at two granularities:
|
||||
DataObject represents BusinessObject (exactly one -- the pivot, BR-006)
|
||||
DataElement represents BusinessConcept (at most one)
|
||||
Renamed from 'implements', which carried software-engineering baggage: a Data Object does not implement a Business Object the way a class implements an interface.
|
||||
|
||||
Range is deliberately left open and scoped by SHACL per class, because the two uses have different targets.
|
||||
|
||||
A Data Element uses this ONLY when it is dimensional. A measure element reaches meaning through Metric -> computedBy, which says more: it names WHICH metric the meaning travels through. Never both routes on one element.
|
||||
|
||||
Functional at DataObject level only -- not declared owl:FunctionalProperty here, since a single global cardinality would be wrong for one of the two uses.""" .
|
||||
|
||||
pr:implements a owl:ObjectProperty , owl:FunctionalProperty ;
|
||||
rdfs:label "implements" ;
|
||||
rdfs:domain pr:DataObject ; rdfs:range pr:BusinessObject ;
|
||||
rdfs:comment "The pivot. Exactly one, enforced by SHACL (BR-006)." .
|
||||
rdfs:label "implements (deprecated)" ;
|
||||
owl:deprecated true ; dcterms:isReplacedBy pr:represents .
|
||||
|
||||
pr:hasElement a owl:ObjectProperty ;
|
||||
rdfs:label "has element" ;
|
||||
@@ -370,18 +500,43 @@ pr:references a owl:ObjectProperty ;
|
||||
rdfs:domain pr:DataObject ; rdfs:range pr:DataObject ;
|
||||
rdfs:comment "Foreign-key reference between Data Objects. NOT ownership. Now checkable against the harvested ForeignKeyConstraint objects." .
|
||||
|
||||
pr:usesDataObject a owl:ObjectProperty ;
|
||||
rdfs:label "uses Data Object" ;
|
||||
rdfs:domain pr:DataAsset ; rdfs:range pr:DataObject .
|
||||
#################################################################
|
||||
# OBJECT PROPERTIES - Delivery
|
||||
#################################################################
|
||||
|
||||
pr:packages a owl:ObjectProperty ;
|
||||
rdfs:label "packages" ;
|
||||
rdfs:domain pr:DataProduct ; rdfs:range pr:DataAsset .
|
||||
rdfs:domain pr:DataProduct ; rdfs:range pr:DataObject ;
|
||||
rdfs:comment "The scope of the product. Through these Data Objects and their represents, the Business Objects a product makes available are derivable -- which is why a product needs no direct link to a Business Object." .
|
||||
|
||||
pr:governedBy a owl:ObjectProperty ;
|
||||
pr:exposes a owl:ObjectProperty ;
|
||||
rdfs:label "exposes" ;
|
||||
rdfs:domain pr:DataProduct ; rdfs:range pr:DataInterface ;
|
||||
rdfs:comment "The channels through which the product is actually consumed." .
|
||||
|
||||
pr:servedBy a owl:ObjectProperty ;
|
||||
rdfs:label "served by" ;
|
||||
rdfs:domain pr:DataInterface ; rdfs:range pr:PhysicalRelation ;
|
||||
rdfs:comment "What physically backs the interface: the shared relations, the extract source." .
|
||||
|
||||
pr:governedBy a owl:ObjectProperty , owl:FunctionalProperty ;
|
||||
rdfs:label "governed by" ;
|
||||
rdfs:domain pr:DataProduct ; rdfs:range pr:DataContract .
|
||||
|
||||
pr:operatedBy a owl:ObjectProperty , owl:FunctionalProperty ;
|
||||
rdfs:label "operated by" ;
|
||||
rdfs:domain pr:DataProduct ; rdfs:range pr:SubDomain ;
|
||||
rdfs:comment "The single Sub-Domain that runs this product. A Data Product spans several Business Objects and therefore cannot inherit accountability the way a Data Object does -- so it declares its own, and exactly one. It is a consumable: there must be no ambiguity about who runs it." .
|
||||
|
||||
pr:hasProductOwner a owl:ObjectProperty , owl:FunctionalProperty ;
|
||||
rdfs:label "has product owner" ;
|
||||
rdfs:domain pr:DataProduct ; rdfs:range pr:DataProductOwner ;
|
||||
rdfs:comment "Exactly one. Belongs to the operating Sub-Domain -- checkable, not just conventional." .
|
||||
|
||||
pr:usesDataObject a owl:ObjectProperty ;
|
||||
rdfs:label "uses Data Object (deprecated)" ;
|
||||
owl:deprecated true ; dcterms:isReplacedBy pr:packages .
|
||||
|
||||
|
||||
#################################################################
|
||||
# OBJECT PROPERTIES - the curated / observed seam
|
||||
@@ -513,7 +668,13 @@ pr:realizesMetric a owl:ObjectProperty , owl:FunctionalProperty ;
|
||||
rdfs:label "realizes metric" ;
|
||||
rdfs:domain pr:BIField ; rdfs:range pr:Metric ;
|
||||
pr:curationMode "PROPOSED_CONFIRMED" ;
|
||||
rdfs:comment "The harmonisation proof. Counting distinct BI Fields realizing one certified Metric, and comparing their expressions, turns 'we harmonised the definitions' from a claim into a measurement over the real BI estate." .
|
||||
rdfs:comment """The harmonisation measure: how many distinct BI Fields realize one certified Metric, and do their expressions agree. Turns 'we harmonised the definitions' from a claim into a count over the real BI estate.
|
||||
|
||||
In principle this is derivable from the lineage chain Metric -> DataElement -> Field -> BIField, since lineage carries semantic inheritance. In practice that chain will have holes -- four hops, one of them PROPOSED_CONFIRMED, another harvested from an API that does not resolve custom SQL, extracts or cascaded calculated fields -- and the question it answers is too important to depend on them.
|
||||
|
||||
So: TRANSITIONAL CONTROL. Asserted directly, and that is all. No rule is written against the lineage chain: making the assertion conditional on intermediate mappings would create confusion and discourage people from filling it in.
|
||||
|
||||
Retirement criterion, to be measured rather than debated: when the lineage chain reproduces the asserted link for more than 95% of certified BI fields across three consecutive reconciliation cycles, the direct link is derivable and can go.""" .
|
||||
|
||||
pr:usesBIDataSource a owl:ObjectProperty ;
|
||||
rdfs:label "uses BI data source" ;
|
||||
@@ -534,15 +695,34 @@ pr:ownedBy a owl:ObjectProperty ;
|
||||
|
||||
pr:owningDomain a owl:ObjectProperty , owl:FunctionalProperty ;
|
||||
rdfs:label "owning Data Domain" ; rdfs:range pr:DataDomain ;
|
||||
rdfs:comment "The single Data Domain with authority to define the object's rule. Required on Curated Objects, absent on Observed Objects: a Snowflake column is attributed through the Data Element mapped to it, never asserted directly." .
|
||||
rdfs:comment """The single Data Domain with authority to define the object's rule. Absent on Observed Objects: a Snowflake column is attributed through the Data Element mapped to it, never asserted directly.
|
||||
|
||||
Unlike the steward, this one is RE-DECLARABLE on a Data Element. It is inherited by default from the parent chain, but OW-002 attributes an element to the domain that defines its RULE, and a borrowed element -- Brand Code sitting in a Sell Out fact object -- has its rule elsewhere. A Data Element whose owningDomain differs from its parent's is a borrowed element: a signal to check against its represents, not an anomaly to fix.""" .
|
||||
|
||||
pr:monitoredBy a owl:ObjectProperty ;
|
||||
rdfs:label "monitored by" ; rdfs:range pr:DataSteward ;
|
||||
rdfs:comment """Declared on BusinessObject, and on DataProduct, and NOWHERE ELSE. Data Objects inherit through represents, Data Elements through hasElement. Declaring a steward at several levels would allow two answers to 'who watches this column'.
|
||||
|
||||
Says what the steward DOES, where the deprecated hasSteward only stated a membership.
|
||||
|
||||
DataProduct is the one exception to inheritance: it packages Data Objects belonging to several Business Objects, so it cannot inherit a single steward and declares its own.""" .
|
||||
|
||||
pr:hasSteward a owl:ObjectProperty ;
|
||||
rdfs:label "has Data Steward" ; rdfs:range pr:DataSteward .
|
||||
rdfs:label "has Data Steward (deprecated)" ;
|
||||
owl:deprecated true ; dcterms:isReplacedBy pr:monitoredBy .
|
||||
|
||||
pr:hasDGL a owl:ObjectProperty , owl:FunctionalProperty ;
|
||||
rdfs:label "has DGL" ;
|
||||
rdfs:domain pr:DataDomain ; rdfs:range pr:DataGovernanceLead .
|
||||
|
||||
pr:hasDomainOwner a owl:ObjectProperty , owl:FunctionalProperty ;
|
||||
rdfs:label "has domain owner" ;
|
||||
rdfs:domain pr:DataDomain ; rdfs:range pr:DataDomainOwner .
|
||||
|
||||
pr:hasSubDomainOwner a owl:ObjectProperty , owl:FunctionalProperty ;
|
||||
rdfs:label "has sub-domain owner" ;
|
||||
rdfs:domain pr:SubDomain ; rdfs:range pr:SubDomainOwner .
|
||||
|
||||
pr:hasActivationStatus a owl:ObjectProperty , owl:FunctionalProperty ;
|
||||
rdfs:label "activation status" ;
|
||||
rdfs:domain pr:DataDomain ; rdfs:range pr:ActivationStatus .
|
||||
@@ -563,7 +743,12 @@ pr:arbitrationStatus a owl:DatatypeProperty ; rdfs:label "arbitration status" ;
|
||||
rdfs:comment "TO_ARBITRATE blocks publication (OW-007). Use this on the object rather than tracking arbitration in a side spreadsheet." .
|
||||
|
||||
# ---- Business semantics
|
||||
pr:hasBusinessDefinition a owl:DatatypeProperty ; rdfs:label "business definition" ; rdfs:range xsd:string .
|
||||
pr:hasBusinessDefinition a owl:DatatypeProperty ; rdfs:label "business definition" ; rdfs:range xsd:string ;
|
||||
rdfs:comment """The meaning of a business notion. Belongs to the BusinessConcept.
|
||||
|
||||
MUST NOT be carried by a BusinessObject: an object gets its meaning through aboutConcept. Two definitions for one notion are two definitions free to diverge. Enforced by SHACL rather than by rdfs:domain, because Data Elements legitimately carry one too and a declared domain would make them entail BusinessConcept -- which the disjointness axioms would then reject.
|
||||
|
||||
MIGRATION: text currently on a Business Object moves to the Concept named by its aboutConcept.""" .
|
||||
pr:hasTechnicalDefinition a owl:DatatypeProperty ; rdfs:label "technical definition" ; rdfs:range xsd:string .
|
||||
pr:hasAcronym a owl:DatatypeProperty ; rdfs:label "acronym" ; rdfs:range xsd:string .
|
||||
pr:hasSynonym a owl:DatatypeProperty ; rdfs:label "synonym" ; rdfs:range xsd:string .
|
||||
@@ -573,8 +758,9 @@ pr:hasBusinessRule a owl:DatatypeProperty ; rdfs:label "business rule" ; rdfs:ra
|
||||
pr:hasFormula a owl:DatatypeProperty ; rdfs:label "formula" ; rdfs:range xsd:string .
|
||||
pr:hasUnit a owl:DatatypeProperty ; rdfs:label "unit" ; rdfs:range xsd:string ;
|
||||
rdfs:comment "From the Units LoV. Note the LoV currently mixes physical units, currencies (ISO 4217) and value types (Index, Rate, Count) - three different concepts that should eventually be split." .
|
||||
pr:hasGranularity a owl:DatatypeProperty ; rdfs:label "granularity (label)" ; rdfs:range xsd:string ;
|
||||
rdfs:comment "Human-readable grain label, e.g. 'SKU x Customer x Day'. Keep in sync with hasGrainDimension, which is the machine-readable form." .
|
||||
pr:hasGranularity a owl:DatatypeProperty ; rdfs:label "granularity (deprecated)" ; rdfs:range xsd:string ;
|
||||
owl:deprecated true ; dcterms:isReplacedBy pr:hasGrainElement ;
|
||||
rdfs:comment "Free-text grain label on a Metric, e.g. 'SKU x Customer x Day'. Kept a second, unverifiable copy of something now stated structurally on the Data Object. Grain is a property of a dataset, not of a calculation rule." .
|
||||
pr:hasTimeAggregation a owl:DatatypeProperty ; rdfs:label "time aggregation" ; rdfs:range xsd:string .
|
||||
|
||||
pr:isKPI a owl:DatatypeProperty ; rdfs:label "is KPI (deprecated)" ; rdfs:range xsd:boolean ;
|
||||
@@ -636,10 +822,10 @@ pr:hasExpression a owl:DatatypeProperty ; rdfs:label "BI expression" ; rdfs:rang
|
||||
|
||||
[] a owl:AllDisjointClasses ; owl:members (
|
||||
pr:OwnershipLayerObject pr:BusinessLayerObject pr:LogicalLayerObject
|
||||
pr:PhysicalLayerObject pr:ConsumptionLayerObject ) .
|
||||
pr:DeliveryLayerObject pr:PhysicalLayerObject pr:ConsumptionLayerObject ) .
|
||||
|
||||
[] a owl:AllDisjointClasses ; owl:members (
|
||||
pr:BusinessObject pr:DataObject pr:DataAsset pr:DataProduct ) .
|
||||
pr:BusinessObject pr:DataObject pr:DataInterface pr:DataProduct ) .
|
||||
|
||||
[] a owl:AllDisjointClasses ; owl:members ( pr:BusinessConcept pr:Metric ) .
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# PR Data Meta Model - validation stack
|
||||
#
|
||||
# PINNED ON PURPOSE. The venv on GrosseBertha runs Python 3.9
|
||||
# (/usr/local/bin/python3.9, the only stable interpreter on DSM 7.4).
|
||||
#
|
||||
# Newer pyshacl releases advertise 3.9 support but use PEP 604 syntax
|
||||
# (str | X) in runtime annotations, which needs 3.10+. pip installs them
|
||||
# without complaint and the import then dies with:
|
||||
# TypeError: unsupported operand type(s) for |: 'type' and 'ABCMeta'
|
||||
#
|
||||
# 0.26.0 is the version verified working on this interpreter (2026-07-26).
|
||||
# Do not unpin without rebuilding the venv and re-running the validator.
|
||||
#
|
||||
# /usr/local/bin/python3.9 -m venv venv
|
||||
# . venv/bin/activate && pip install -r requirements.txt
|
||||
|
||||
pyshacl==0.26.0
|
||||
rdflib>=6.3.2,<7.1
|
||||
@@ -0,0 +1,255 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
SODH arbitration - composedOf -> aboutConcept + usesConcept
|
||||
============================================================
|
||||
Second half of the v1.0 -> v1.1 migration. migrate_instances_v1_1.py did the
|
||||
mechanical renames and deliberately stopped here, because everything below is a
|
||||
judgement someone had to make.
|
||||
|
||||
USAGE
|
||||
python3 scripts/apply_arbitration_sodh.py # dry run
|
||||
python3 scripts/apply_arbitration_sodh.py --apply # rewrite, .bak kept
|
||||
|
||||
WHAT WAS ARBITRATED (Bastien, 2026-07-27)
|
||||
-----------------------------------------
|
||||
The v0.6 padding is what made this necessary: four Business Objects belonging
|
||||
to other domains -- Product Structure, Customer & Outlet, Currency, Calendar --
|
||||
were all declared composedOf the same three Sell Out concepts, because BR-004
|
||||
demanded three and there were none to give. The subject rule replaces that
|
||||
minimum: exactly one subject, and if it cannot be named the object is not
|
||||
scoped.
|
||||
|
||||
1. Distribution -> Retail Distribution.
|
||||
"Distribution" is too generic to survive contact with other domains, and
|
||||
it is NOT a kind of sell out: sell out counts units sold, distribution
|
||||
measures product presence in store. Retail Distribution follows panel
|
||||
vocabulary (Nielsen/Circana), under which Numeric and Weighted
|
||||
Distribution are the metrics.
|
||||
|
||||
2. Sell Out Baseline becomes a concept in its own right, not a flavour of
|
||||
Sell Out: modelled non-promoted volume is a different notion from
|
||||
observed volume.
|
||||
|
||||
3. Customer & Outlet splits into two Business Objects, and Currency &
|
||||
Exchange Rates likewise. An object named "X & Y" has no single subject,
|
||||
which is precisely what the rule is meant to surface. The split is
|
||||
PROPOSED here; DD-04 and DD-16 own the decision.
|
||||
|
||||
4. Six concepts belonging to other domains are created with a DRAFT
|
||||
definition and arbitrationStatus TO_ARBITRATE. A draft to correct beats a
|
||||
blank to fill, and OW-007 blocks publication until the owning domain has
|
||||
ratified. These definitions are the DGO's proposal, not DD-04/10/16/21's
|
||||
word.
|
||||
|
||||
NOT DONE HERE
|
||||
hasGranularity on Metrics still needs re-expressing as hasGrainElement on
|
||||
Data Objects, and the BR-013 alignment (10 mother metrics, computedBy)
|
||||
is a separate migration. Until that lands, every Data Element will fail
|
||||
the XOR-of-meaning shape -- which is the correct, informative result.
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
|
||||
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
TARGET = os.path.join(REPO, "instances", "sodh.ttl")
|
||||
W = 78
|
||||
|
||||
# ---------------------------------------------------------------- new concepts
|
||||
# (iri, identifier, name, domain, steward, definition, arbitrate)
|
||||
NEW_CONCEPTS = [
|
||||
("ex:BC_06_01_005", "BC-06.01-005", "Sell Out Baseline", "ex:DD_06", "ex:ST_DD_06",
|
||||
"The modelled level of retail sales that would have occurred without promotional activity, "
|
||||
"used as the reference against which promotional uplift is measured. Distinct from Sell Out, "
|
||||
"which is observed rather than modelled.",
|
||||
False),
|
||||
("ex:BC_10_01_001", "BC-10.01-001", "Product", "ex:DD_10", "ex:ST_DD_10",
|
||||
"A commercial item sold by Pernod Ricard, identified at Stock Keeping Unit level and "
|
||||
"positioned in the corporate hierarchy from Brand through Category, Sub-Category and Flavor.",
|
||||
True),
|
||||
("ex:BC_04_01_001", "BC-04.01-001", "Customer", "ex:DD_04", "ex:ST_DD_04",
|
||||
"A commercial counterparty that buys Pernod Ricard products for resale, positioned in the "
|
||||
"trade hierarchy from Distributor through Tier-1 and Tier-2 Customer. Distinct from Outlet, "
|
||||
"which is a physical point of sale.",
|
||||
True),
|
||||
("ex:BC_04_01_002", "BC-04.01-002", "Outlet", "ex:DD_04", "ex:ST_DD_04",
|
||||
"A physical point of sale where products are made available to end consumers, characterised "
|
||||
"by its Channel. An Outlet belongs to a Customer but is not the same notion: one Customer "
|
||||
"operates many Outlets.",
|
||||
True),
|
||||
("ex:BC_16_01_001", "BC-16.01-001", "Currency", "ex:DD_16", "ex:ST_DD_16",
|
||||
"A monetary unit in which a transaction or a reported amount is denominated, identified by "
|
||||
"its ISO 4217 code.",
|
||||
True),
|
||||
("ex:BC_16_01_002", "BC-16.01-002", "Exchange Rate", "ex:DD_16", "ex:ST_DD_16",
|
||||
"The conversion factor applied between two currencies at a given date, used to restate local "
|
||||
"amounts into group reporting currency. Distinct from Currency: a rate is a dated "
|
||||
"relationship between two of them.",
|
||||
True),
|
||||
("ex:BC_21_01_001", "BC-21.01-001", "Calendar Date", "ex:DD_21", "ex:ST_DD_21",
|
||||
"A single day in the Gregorian calendar, the atomic unit from which Week, Month, Quarter and "
|
||||
"Year are derived and against which all time-based measures are positioned.",
|
||||
True),
|
||||
]
|
||||
|
||||
# ------------------------------------------------------------------- new object
|
||||
NEW_OBJECTS = [
|
||||
("ex:BO_04_01_002", "BO-04.01-002", "Outlet", "ex:DD_04", "ex:ST_DD_04", "ex:SD_04_01",
|
||||
"ex:BC_04_01_002", [], "MDM Customer -> SODH [split from BO-04.01-001, proposed to DD-04]"),
|
||||
("ex:BO_16_01_002", "BO-16.01-002", "Exchange Rate", "ex:DD_16", "ex:ST_DD_16", "ex:SD_16_01",
|
||||
"ex:BC_16_01_002", [], "Group Finance -> SODH [split from BO-16.01-001, proposed to DD-16]"),
|
||||
]
|
||||
|
||||
# ---------------------------------------------------- subject / used per object
|
||||
# iri -> (aboutConcept, [usesConcept...], new name or None)
|
||||
ARBITRATION = {
|
||||
"ex:BO_06_01_001": ("ex:BC_06_01_001", ["ex:BC_06_01_003", "ex:BC_05_01_001"], None),
|
||||
"ex:BO_06_01_002": ("ex:BC_06_01_001", ["ex:BC_06_01_003", "ex:BC_05_01_001",
|
||||
"ex:BC_16_01_001"], None),
|
||||
"ex:BO_06_01_003": ("ex:BC_06_01_004", ["ex:BC_06_01_003"], None),
|
||||
"ex:BO_06_01_004": ("ex:BC_06_01_002", ["ex:BC_06_01_001", "ex:BC_06_01_003"], None),
|
||||
"ex:BO_06_01_005": ("ex:BC_06_01_005", ["ex:BC_06_01_001", "ex:BC_05_01_001"], None),
|
||||
"ex:BO_10_01_001": ("ex:BC_10_01_001", [], None),
|
||||
"ex:BO_04_01_001": ("ex:BC_04_01_001", [], "Customer"),
|
||||
"ex:BO_16_01_001": ("ex:BC_16_01_001", [], "Currency"),
|
||||
"ex:BO_21_01_001": ("ex:BC_21_01_001", [], None),
|
||||
"ex:BO_05_01_001": ("ex:BC_05_01_001", [], None),
|
||||
}
|
||||
|
||||
RENAME_CONCEPT = {"ex:BC_06_01_004": ("Distribution", "Retail Distribution")}
|
||||
|
||||
|
||||
def concept_block(iri, ident, name, domain, steward, definition, arbitrate):
|
||||
lines = [
|
||||
'%s a pr:BusinessConcept ;' % iri,
|
||||
' pr:hasIdentifier "%s" ; pr:hasName "%s" ;' % (ident, name),
|
||||
' pr:owningDomain %s ; pr:ownedBy %s ;' % (domain, steward),
|
||||
' pr:hasBusinessDefinition "%s" ;' % definition,
|
||||
]
|
||||
if arbitrate:
|
||||
lines.append(' pr:arbitrationStatus "TO_ARBITRATE" ;')
|
||||
src = "DGO proposal, pending ratification by the owning domain"
|
||||
else:
|
||||
src = "SODH Gold"
|
||||
lines.append(' pr:hasStatus "DRAFT" ; pr:hasVersion "1.1" ; pr:hasSource "%s" .' % src)
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def object_block(iri, ident, name, domain, steward, subdomain, about, uses, source):
|
||||
lines = [
|
||||
'%s a pr:BusinessObject ;' % iri,
|
||||
' pr:hasIdentifier "%s" ; pr:hasName "%s" ;' % (ident, name),
|
||||
' pr:owningDomain %s ; pr:ownedBy %s ;' % (domain, steward),
|
||||
' pr:belongsTo %s ; pr:monitoredBy %s ;' % (subdomain, steward),
|
||||
' pr:aboutConcept %s ;' % about,
|
||||
]
|
||||
if uses:
|
||||
lines.append(' pr:usesConcept %s ;' % " , ".join(uses))
|
||||
lines.append(' pr:arbitrationStatus "TO_ARBITRATE" ;')
|
||||
lines.append(' pr:hasStatus "DRAFT" ; pr:hasVersion "1.1" ; pr:hasSource "%s" .' % source)
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def main():
|
||||
apply_changes = "--apply" in sys.argv
|
||||
if not os.path.exists(TARGET):
|
||||
print("Not found: %s" % TARGET)
|
||||
sys.exit(2)
|
||||
|
||||
text = original = open(TARGET, encoding="utf-8").read()
|
||||
report = []
|
||||
|
||||
# ---- 1. rename the over-generic concept ----------------------------------
|
||||
for iri, (old, new) in RENAME_CONCEPT.items():
|
||||
pat = re.compile(r'(%s\b.*?pr:hasName )"%s"' % (re.escape(iri), re.escape(old)), re.S)
|
||||
text, n = pat.subn(r'\1"%s"' % new, text, count=1)
|
||||
if n:
|
||||
report.append("renamed concept %s: %s -> %s" % (iri, old, new))
|
||||
|
||||
# ---- 2. rewrite each Business Object -------------------------------------
|
||||
for iri, (about, uses, newname) in ARBITRATION.items():
|
||||
m = re.search(r'(^%s a pr:BusinessObject ;.*?\n)(?=^ex:|\Z)' % re.escape(iri),
|
||||
text, re.S | re.M)
|
||||
if not m:
|
||||
report.append("!! block not found: %s" % iri)
|
||||
continue
|
||||
block = m.group(1)
|
||||
|
||||
new_block = re.sub(r'[ \t]*pr:composedOf\s+[^";]*;[ \t]*\n', '', block)
|
||||
clause = ' pr:aboutConcept %s ;\n' % about
|
||||
if uses:
|
||||
clause += ' pr:usesConcept %s ;\n' % " , ".join(uses)
|
||||
new_block = re.sub(r'(\n)([ \t]*pr:hasMetric|[ \t]*pr:hasBusinessDefinition|[ \t]*pr:hasStatus)',
|
||||
r'\1%s\2' % clause, new_block, count=1)
|
||||
|
||||
# meaning belongs to the Concept, never to the Object
|
||||
# match the full quoted literal: several definitions contain a semicolon
|
||||
# inside the string ("Owned by Product & Material; consumed by SODH."),
|
||||
# which a naive [^;]* would stop at, silently leaving the property behind
|
||||
new_block, ndef = re.subn(
|
||||
r'[ \t]*pr:hasBusinessDefinition\s+"(?:[^"\\]|\\.)*"\s*;[ \t]*\n',
|
||||
'', new_block)
|
||||
|
||||
if newname:
|
||||
new_block = re.sub(r'(pr:hasName )"[^"]*"', r'\1"%s"' % newname, new_block, count=1)
|
||||
new_block = re.sub(r'(pr:hasSource )"[^"]*"',
|
||||
r'\1"MDM -> SODH [split proposed, subject arbitrated 2026-07-27]"',
|
||||
new_block, count=1)
|
||||
|
||||
text = text[:m.start(1)] + new_block + text[m.end(1):]
|
||||
report.append("%-18s about=%s uses=%d%s%s"
|
||||
% (iri, about.replace("ex:BC_", "BC-"), len(uses),
|
||||
" def->concept" if ndef else "",
|
||||
" renamed=%s" % newname if newname else ""))
|
||||
|
||||
# ---- 3. insert the missing concepts --------------------------------------
|
||||
anchor = re.search(r'\n(?=ex:BO_\w+ a pr:BusinessObject ;)', text)
|
||||
if anchor:
|
||||
blocks = "\n".join(concept_block(*c) for c in NEW_CONCEPTS)
|
||||
text = (text[:anchor.start()] + "\n\n" +
|
||||
"# --- concepts added in v1.1 migration -------------------------------\n" +
|
||||
"# Six of these belong to other domains: DRAFT definitions proposed by the\n" +
|
||||
"# DGO, flagged TO_ARBITRATE. OW-007 blocks publication until ratified.\n" +
|
||||
blocks + "\n" + text[anchor.start():])
|
||||
report.append("inserted %d Business Concepts (%d TO_ARBITRATE)"
|
||||
% (len(NEW_CONCEPTS), sum(1 for c in NEW_CONCEPTS if c[6])))
|
||||
|
||||
# ---- 4. append the objects produced by the splits ------------------------
|
||||
tail = re.search(r'\n(?=ex:M_\w+ a pr:Metric ;|ex:DO_\w+ a pr:DataObject ;)', text)
|
||||
if tail:
|
||||
blocks = "\n".join(object_block(*o) for o in NEW_OBJECTS)
|
||||
text = (text[:tail.start()] + "\n\n" +
|
||||
"# --- Business Objects from the v1.1 splits ---------------------------\n" +
|
||||
"# 'Customer & Outlet' and 'Currency & Exchange Rates' had no single\n" +
|
||||
"# subject. Split proposed to DD-04 and DD-16, hence TO_ARBITRATE.\n" +
|
||||
blocks + "\n" + text[tail.start():])
|
||||
report.append("inserted %d Business Objects from splits" % len(NEW_OBJECTS))
|
||||
|
||||
# ---- report --------------------------------------------------------------
|
||||
print()
|
||||
print("SODH ARBITRATION %s" % ("APPLY" if apply_changes else "DRY RUN"))
|
||||
print("=" * W)
|
||||
for line in report:
|
||||
print(" " + line)
|
||||
print("=" * W)
|
||||
bo = len(re.findall(r'a pr:BusinessObject', text))
|
||||
bc = len(re.findall(r'a pr:BusinessConcept', text))
|
||||
print(" Business Objects %d | Business Concepts %d | composedOf left %d"
|
||||
% (bo, bc, len(re.findall(r'pr:composedOf', text))))
|
||||
print(" Business Objects still carrying a definition: %d"
|
||||
% len([b for b in re.split(r'\.\s*\n(?=\S)', text)
|
||||
if "pr:BusinessObject" in b and "pr:hasBusinessDefinition" in b]))
|
||||
print("=" * W)
|
||||
|
||||
if text != original and apply_changes:
|
||||
shutil.copy2(TARGET, TARGET + ".bak")
|
||||
open(TARGET, "w", encoding="utf-8").write(text)
|
||||
print(" written, backup at %s.bak" % os.path.basename(TARGET))
|
||||
elif text != original:
|
||||
print(" dry run -- re-run with --apply to write")
|
||||
print()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -183,7 +183,8 @@ BANDS = [
|
||||
("roots", "Model roots", "How a term got here", "#0A1F44"),
|
||||
("ownership", "Ownership & Categorization", "Who is accountable", "#B8935A"),
|
||||
("business", "Business", "What the business means", "#8B3A3A"),
|
||||
("logical", "Logical", "What we commit to deliver", "#7A4E8B"),
|
||||
("logical", "Logical", "How the data is structured", "#7A4E8B"),
|
||||
("delivery", "Delivery", "What we promise, to whom", "#B8935A"),
|
||||
("physical", "Physical", "What actually runs", "#1E4B9B"),
|
||||
("consumption", "Consumption", "What people open in the morning", "#1E7A5F"),
|
||||
("context", "Actors & vocabularies", "Outside the object stack", "#6B6B6B"),
|
||||
@@ -193,6 +194,7 @@ LAYER_ROOT = {
|
||||
"pr:OwnershipLayerObject": "ownership",
|
||||
"pr:BusinessLayerObject": "business",
|
||||
"pr:LogicalLayerObject": "logical",
|
||||
"pr:DeliveryLayerObject": "delivery",
|
||||
"pr:PhysicalLayerObject": "physical",
|
||||
"pr:ConsumptionLayerObject": "consumption",
|
||||
}
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
PR Data Meta Model - instance migration v1.0 -> v1.1
|
||||
=====================================================
|
||||
Rewrites instance files for the T-Box v1.1 vocabulary. Text-level rewriting on
|
||||
purpose: it preserves comments, ordering and formatting, which a parse-and-
|
||||
reserialise round trip through rdflib would destroy -- and those comments carry
|
||||
the arbitration notes.
|
||||
|
||||
USAGE
|
||||
python3 scripts/migrate_instances_v1_1.py # dry run, report only
|
||||
python3 scripts/migrate_instances_v1_1.py --apply # rewrite in place, .bak kept
|
||||
python3 scripts/migrate_instances_v1_1.py --apply f.ttl # a single file
|
||||
|
||||
WHAT IT DOES AUTOMATICALLY
|
||||
namespace /metamodel/0.9/ -> /metamodel/ (unversioned terms)
|
||||
belongsToDomain, belongsToSubDomain -> belongsTo
|
||||
implements -> represents
|
||||
hasSteward -> monitoredBy
|
||||
usesDataObject -> packages
|
||||
DataAsset -> DataInterface
|
||||
hasGrainDimension, hasGranularity -> flagged, not converted
|
||||
|
||||
WHAT IT REFUSES TO DO
|
||||
composedOf -> aboutConcept / usesConcept is NOT automated. Splitting it
|
||||
requires deciding which concept is the SUBJECT of each Business Object, and
|
||||
guessing that is exactly how v0.6 ended up with Calendar declared as being
|
||||
composed of Sell Out. The script lists the choices to be made and leaves
|
||||
composedOf in place until a human answers.
|
||||
|
||||
hasBusinessDefinition on a Business Object is likewise reported, not moved:
|
||||
the text must go to the Concept named by aboutConcept, which does not exist
|
||||
yet at the time this runs.
|
||||
"""
|
||||
import glob
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
|
||||
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
INSTANCES = os.path.join(REPO, "instances")
|
||||
W = 78
|
||||
|
||||
# (pattern, replacement, label) -- applied in order, word-bounded
|
||||
RENAMES = [
|
||||
(r"https://ontology\.pernod-ricard\.com/metamodel/0\.9/",
|
||||
"https://ontology.pernod-ricard.com/metamodel/", "namespace 0.9 -> unversioned"),
|
||||
(r"\bpr:belongsToDomain\b", "pr:belongsTo", "belongsToDomain -> belongsTo"),
|
||||
(r"\bpr:belongsToSubDomain\b", "pr:belongsTo", "belongsToSubDomain -> belongsTo"),
|
||||
(r"\bpr:implements\b", "pr:represents", "implements -> represents"),
|
||||
(r"\bpr:hasSteward\b", "pr:monitoredBy", "hasSteward -> monitoredBy"),
|
||||
(r"\bpr:usesDataObject\b", "pr:packages", "usesDataObject -> packages"),
|
||||
(r"\bpr:DataAsset\b", "pr:DataInterface", "DataAsset -> DataInterface"),
|
||||
]
|
||||
|
||||
# things a human must decide, reported per file
|
||||
MANUAL = [
|
||||
(r"\bpr:composedOf\b", "composedOf",
|
||||
"Split into aboutConcept (exactly one -- the SUBJECT) and usesConcept "
|
||||
"(the rest). Not automated: guessing the subject is how v0.6 ended up "
|
||||
"with Calendar composed of Sell Out."),
|
||||
(r"\bpr:isComponentOf\b", "isComponentOf",
|
||||
"Inverse of the above. Re-express as usesConcept from the Business Object."),
|
||||
(r"\bpr:hasGranularity\b", "hasGranularity",
|
||||
"Grain moved off the Metric. Re-express as hasGrainElement on the Data "
|
||||
"Object, pointing at the Data Elements that make a row unique."),
|
||||
(r"\bpr:hasGrainDimension\b", "hasGrainDimension",
|
||||
"Same: hasGrainElement on the Data Object, targeting Data Elements."),
|
||||
]
|
||||
|
||||
|
||||
def subject_blocks(text):
|
||||
"""Yield (subject, block) for each top-level statement."""
|
||||
for block in re.split(r"\.\s*\n(?=\S)", text):
|
||||
m = re.match(r"\s*(\S+)", block)
|
||||
if m:
|
||||
yield m.group(1), block
|
||||
|
||||
|
||||
def business_objects_with_definition(text):
|
||||
"""Business Objects still carrying hasBusinessDefinition -- must move to the Concept."""
|
||||
out = []
|
||||
for subj, block in subject_blocks(text):
|
||||
if "pr:BusinessObject" in block and "pr:hasBusinessDefinition" in block:
|
||||
out.append(subj)
|
||||
return out
|
||||
|
||||
|
||||
def composed_of_map(text):
|
||||
"""Business Object -> list of concepts, so the subject can be chosen."""
|
||||
out = {}
|
||||
for subj, block in subject_blocks(text):
|
||||
if "pr:composedOf" not in block:
|
||||
continue
|
||||
m = re.search(r"pr:composedOf\s+([^;.]+)", block)
|
||||
if m:
|
||||
concepts = [c.strip() for c in m.group(1).split(",") if c.strip()]
|
||||
out[subj] = concepts
|
||||
return out
|
||||
|
||||
|
||||
def process(path, apply_changes):
|
||||
original = open(path, encoding="utf-8").read()
|
||||
text = original
|
||||
applied = []
|
||||
|
||||
for pattern, repl, label in RENAMES:
|
||||
text, n = re.subn(pattern, repl, text)
|
||||
if n:
|
||||
applied.append((label, n))
|
||||
|
||||
pending = []
|
||||
for pattern, label, advice in MANUAL:
|
||||
n = len(re.findall(pattern, original))
|
||||
if n:
|
||||
pending.append((label, n, advice))
|
||||
|
||||
bo_defs = business_objects_with_definition(original)
|
||||
comp = composed_of_map(original)
|
||||
|
||||
print("=" * W)
|
||||
print(os.path.relpath(path, REPO))
|
||||
print("=" * W)
|
||||
|
||||
if applied:
|
||||
print(" AUTOMATIC")
|
||||
for label, n in applied:
|
||||
print(" %-42s %4d occurrence(s)" % (label, n))
|
||||
else:
|
||||
print(" AUTOMATIC nothing to rewrite")
|
||||
|
||||
if pending:
|
||||
print(" MANUAL")
|
||||
for label, n, advice in pending:
|
||||
print(" %-42s %4d occurrence(s)" % (label, n))
|
||||
for line in _wrap(advice, 68):
|
||||
print(" " + line)
|
||||
|
||||
if bo_defs:
|
||||
print(" BUSINESS DEFINITIONS TO MOVE")
|
||||
print(" %d Business Object(s) still carry hasBusinessDefinition." % len(bo_defs))
|
||||
print(" Meaning belongs to the Concept. Move each text to the Concept")
|
||||
print(" named by that object's aboutConcept, then delete it here.")
|
||||
for s in bo_defs[:12]:
|
||||
print(" - %s" % s)
|
||||
if len(bo_defs) > 12:
|
||||
print(" ... and %d more" % (len(bo_defs) - 12))
|
||||
|
||||
if comp:
|
||||
print(" SUBJECT TO CHOOSE (composedOf -> aboutConcept + usesConcept)")
|
||||
for subj, concepts in list(comp.items())[:12]:
|
||||
print(" %s" % subj)
|
||||
print(" candidates: %s" % ", ".join(concepts))
|
||||
if len(comp) > 12:
|
||||
print(" ... and %d more" % (len(comp) - 12))
|
||||
|
||||
changed = text != original
|
||||
if changed and apply_changes:
|
||||
shutil.copy2(path, path + ".bak")
|
||||
open(path, "w", encoding="utf-8").write(text)
|
||||
print(" WRITTEN (backup at %s.bak)" % os.path.basename(path))
|
||||
elif changed:
|
||||
print(" DRY RUN -- re-run with --apply to write")
|
||||
print()
|
||||
return changed, len(pending) + len(bo_defs) + len(comp)
|
||||
|
||||
|
||||
def _wrap(text, width):
|
||||
words, line, out = text.split(), "", []
|
||||
for w in words:
|
||||
if len(line) + len(w) + 1 > width:
|
||||
out.append(line)
|
||||
line = w
|
||||
else:
|
||||
line = (line + " " + w).strip()
|
||||
if line:
|
||||
out.append(line)
|
||||
return out
|
||||
|
||||
|
||||
def main():
|
||||
apply_changes = "--apply" in sys.argv
|
||||
args = [a for a in sys.argv[1:] if not a.startswith("-")]
|
||||
files = ([a if os.path.isabs(a) else os.path.join(REPO, a) for a in args]
|
||||
or sorted(glob.glob(os.path.join(INSTANCES, "*.ttl"))))
|
||||
files = [f for f in files if os.path.exists(f)]
|
||||
|
||||
if not files:
|
||||
print("No instance file found.")
|
||||
sys.exit(2)
|
||||
|
||||
print()
|
||||
print("PR META MODEL - INSTANCE MIGRATION v1.0 -> v1.1 %s"
|
||||
% ("APPLY" if apply_changes else "DRY RUN"))
|
||||
print()
|
||||
|
||||
total_changed = total_manual = 0
|
||||
for f in files:
|
||||
changed, manual = process(f, apply_changes)
|
||||
total_changed += int(changed)
|
||||
total_manual += manual
|
||||
|
||||
print("=" * W)
|
||||
print("%d file(s) rewritten, %d decision(s) left to a human."
|
||||
% (total_changed, total_manual))
|
||||
if total_manual:
|
||||
print("Run the validator after the manual work: the XOR-of-meaning shape")
|
||||
print("on Data Elements will not pass until the concepts are wired.")
|
||||
print("=" * W)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,284 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
PR Data Meta Model - SHACL validation runner
|
||||
=============================================
|
||||
The automated gate behind rule DQ-011. Validates an instance graph against the
|
||||
executable rulebook (SHACL shapes), with the T-Box supplying subclass
|
||||
entailment.
|
||||
|
||||
USAGE
|
||||
python3 scripts/run_shacl_validation.py # all instances
|
||||
python3 scripts/run_shacl_validation.py instances/sodh.ttl # one file
|
||||
python3 scripts/run_shacl_validation.py --quiet # CI mode
|
||||
|
||||
Paths resolve against the repository root (the parent of scripts/), so the
|
||||
script runs correctly from anywhere.
|
||||
|
||||
EXIT CODES
|
||||
0 conforms, no blocking violation -> eligible for PUBLISHED
|
||||
1 violations found -> stays DRAFT / UNDER_REVIEW
|
||||
2 the run itself is not trustworthy -> a green result would be meaningless
|
||||
|
||||
Exit code 2 is the important one. A validation gate that passes because no
|
||||
shape matched anything is worse than one that fails: it reports success while
|
||||
checking nothing. The pre-flight below refuses to let that happen.
|
||||
|
||||
DEPENDENCIES
|
||||
pyshacl, rdflib. On GrosseBertha these need a venv built with the STABLE
|
||||
python (never the DSM system python, which moves between DSM releases):
|
||||
|
||||
/usr/local/bin/python3.9 -m venv venv
|
||||
. venv/bin/activate
|
||||
pip install pyshacl rdflib
|
||||
"""
|
||||
import glob
|
||||
import os
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
|
||||
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
ONTOLOGY = os.path.join(REPO, "ontology", "pr_metamodel.ttl")
|
||||
SHAPES = os.path.join(REPO, "shapes", "pr_metamodel_shapes.ttl")
|
||||
INSTANCES = os.path.join(REPO, "instances")
|
||||
|
||||
W = 78
|
||||
BAR = "=" * W
|
||||
|
||||
|
||||
def rel(path):
|
||||
"""Path relative to the repo root, for readable output."""
|
||||
try:
|
||||
return os.path.relpath(path, REPO)
|
||||
except ValueError:
|
||||
return path
|
||||
|
||||
|
||||
def die(code, title, lines):
|
||||
print(BAR)
|
||||
print("PR META MODEL - SHACL VALIDATION")
|
||||
print(BAR)
|
||||
print(title)
|
||||
print("-" * W)
|
||||
for line in lines:
|
||||
print(" " + line)
|
||||
print(BAR)
|
||||
sys.exit(code)
|
||||
|
||||
|
||||
def load_deps():
|
||||
"""
|
||||
Import the validation stack.
|
||||
|
||||
Catches every exception, not just ImportError: a dependency that installs
|
||||
but cannot be imported (pyshacl built for a newer Python than the venv,
|
||||
an rdflib major-version clash) raises TypeError or AttributeError at import
|
||||
time. Letting those escape would end the process with an exit code CI reads
|
||||
as "the model has violations", when in fact validation never started.
|
||||
Anything wrong here is exit 2 -- the run is not trustworthy.
|
||||
"""
|
||||
try:
|
||||
from pyshacl import validate
|
||||
from rdflib import Graph
|
||||
from rdflib.namespace import RDF, SH
|
||||
return validate, Graph, RDF, SH
|
||||
except Exception as exc:
|
||||
hint = []
|
||||
if "unsupported operand type" in str(exc) or "|" in str(exc):
|
||||
hint = [
|
||||
"This is PEP 604 syntax (str | X) evaluated at import time:",
|
||||
"the installed pyshacl needs Python 3.10+, this venv is %d.%d."
|
||||
% (sys.version_info[0], sys.version_info[1]),
|
||||
"pyshacl advertises 3.9 support but its code does not honour it,",
|
||||
"so pip installs a version that cannot be imported.",
|
||||
"",
|
||||
"Pin a release that really supports this interpreter:",
|
||||
' pip install "pyshacl==0.26.0" "rdflib>=6.3.2,<7.1"',
|
||||
"or rebuild the venv on a newer Python if one exists.",
|
||||
]
|
||||
else:
|
||||
hint = [
|
||||
"Build the venv with the stable python, never the DSM system one:",
|
||||
" /usr/local/bin/python3.9 -m venv venv",
|
||||
" . venv/bin/activate && pip install pyshacl rdflib",
|
||||
]
|
||||
die(2, "DEPENDENCY UNUSABLE -- validation did not run",
|
||||
["%s: %s" % (type(exc).__name__, exc), ""] + hint)
|
||||
|
||||
|
||||
def term_namespaces(graph, predicate=None):
|
||||
"""
|
||||
Namespaces actually used by pr: terms in a graph, with usage counts.
|
||||
|
||||
Only real IRIs count. Literals are skipped: SPARQL constraint bodies
|
||||
(sh:select) embed their own PREFIX declarations, and counting those would
|
||||
put raw query text in the diagnostic table.
|
||||
"""
|
||||
counts = defaultdict(int)
|
||||
for s, p, o in graph:
|
||||
for term in (s, p, o):
|
||||
text = str(term)
|
||||
if "ontology.pernod-ricard.com/metamodel" not in text:
|
||||
continue
|
||||
if not text.startswith("http") or any(c in text for c in " \t\n\r<>\""):
|
||||
continue # literal, not an IRI
|
||||
counts[text.rsplit("/", 1)[0] + "/"] += 1
|
||||
return counts
|
||||
|
||||
|
||||
def preflight(data, shapes, onto, data_files):
|
||||
"""
|
||||
Refuse to produce a green result that means nothing.
|
||||
|
||||
Two ways a SHACL run passes while checking nothing:
|
||||
1. the shapes target classes in a namespace the instances never use;
|
||||
2. the ontology declares its classes in a third namespace, so subclass
|
||||
entailment cannot reach the instances and every shape targeting an
|
||||
abstract parent (MetaModelObject) finds zero focus nodes.
|
||||
Both look identical to a caller reading only "CONFORMS: True".
|
||||
"""
|
||||
problems = []
|
||||
ns_problem = False
|
||||
|
||||
if not len(data):
|
||||
problems.append("The instance graph is empty -- nothing to validate.")
|
||||
|
||||
ns_data = term_namespaces(data)
|
||||
ns_shapes = term_namespaces(shapes)
|
||||
ns_onto = term_namespaces(onto) if onto is not None else {}
|
||||
|
||||
if ns_data and ns_shapes and not (set(ns_data) & set(ns_shapes)):
|
||||
ns_problem = True
|
||||
problems.append(
|
||||
"Shapes and instances use disjoint term namespaces -- no shape can "
|
||||
"match any node.")
|
||||
|
||||
if ns_onto and ns_data and not (set(ns_onto) & set(ns_data)):
|
||||
ns_problem = True
|
||||
problems.append(
|
||||
"The T-Box declares its terms in a namespace the instances never "
|
||||
"use. Subclass entailment cannot reach the data, so every shape "
|
||||
"targeting an abstract parent finds zero focus nodes.")
|
||||
|
||||
if problems:
|
||||
lines = list(problems)
|
||||
lines.append("")
|
||||
lines.append("Namespaces in use:")
|
||||
for label, counts in (("instances", ns_data), ("shapes", ns_shapes),
|
||||
("ontology", ns_onto)):
|
||||
if counts:
|
||||
for ns, n in sorted(counts.items(), key=lambda kv: -kv[1]):
|
||||
lines.append(" %-10s %-52s %6d refs" % (label, ns, n))
|
||||
else:
|
||||
lines.append(" %-10s (none)" % label)
|
||||
if ns_problem:
|
||||
lines.append("")
|
||||
lines.append("Fix: align the namespaces before trusting any result.")
|
||||
lines.append(" sed -i 's|/metamodel/0.9/|/metamodel/|g' "
|
||||
"instances/*.ttl shapes/*.ttl")
|
||||
die(2, "PRE-FLIGHT FAILED -- a result here would be meaningless", lines)
|
||||
|
||||
|
||||
def main():
|
||||
args = [a for a in sys.argv[1:] if not a.startswith("-")]
|
||||
quiet = "--quiet" in sys.argv or "-q" in sys.argv
|
||||
|
||||
validate, Graph, RDF, SH = load_deps()
|
||||
|
||||
# ---- resolve inputs ---------------------------------------------------
|
||||
if args:
|
||||
data_files = [a if os.path.isabs(a) else os.path.join(REPO, a) for a in args]
|
||||
else:
|
||||
data_files = sorted(glob.glob(os.path.join(INSTANCES, "*.ttl")))
|
||||
|
||||
missing = [f for f in data_files if not os.path.exists(f)]
|
||||
if missing or not data_files:
|
||||
die(2, "NO INSTANCE DATA -- validation did not run",
|
||||
[rel(f) + " not found" for f in missing] or
|
||||
["instances/ contains no .ttl file"])
|
||||
|
||||
if not os.path.exists(SHAPES):
|
||||
die(2, "NO SHAPES -- validation did not run",
|
||||
[rel(SHAPES) + " not found"])
|
||||
|
||||
# ---- parse ------------------------------------------------------------
|
||||
data = Graph()
|
||||
for f in data_files:
|
||||
try:
|
||||
data.parse(f, format="turtle")
|
||||
except Exception as exc:
|
||||
die(2, "PARSE ERROR -- validation did not run",
|
||||
[rel(f), str(exc)])
|
||||
|
||||
try:
|
||||
shapes = Graph().parse(SHAPES, format="turtle")
|
||||
except Exception as exc:
|
||||
die(2, "PARSE ERROR in shapes -- validation did not run",
|
||||
[rel(SHAPES), str(exc)])
|
||||
|
||||
onto = None
|
||||
if os.path.exists(ONTOLOGY):
|
||||
try:
|
||||
onto = Graph().parse(ONTOLOGY, format="turtle")
|
||||
except Exception as exc:
|
||||
die(2, "PARSE ERROR in ontology -- validation did not run",
|
||||
[rel(ONTOLOGY), str(exc)])
|
||||
|
||||
preflight(data, shapes, onto, data_files)
|
||||
|
||||
# ---- validate ---------------------------------------------------------
|
||||
conforms, results, _ = validate(
|
||||
data_graph=data,
|
||||
shacl_graph=shapes,
|
||||
ont_graph=onto,
|
||||
inference="rdfs", # a Metric is also a MetaModelObject
|
||||
advanced=True, # SPARQL constraints (NR-002, OW-007, DQ-006)
|
||||
abort_on_first=False,
|
||||
meta_shacl=False,
|
||||
)
|
||||
|
||||
rows = []
|
||||
for res in results.subjects(RDF.type, SH.ValidationResult):
|
||||
sev = str(results.value(res, SH.resultSeverity)).rsplit("#", 1)[-1]
|
||||
focus = str(results.value(res, SH.focusNode)).rsplit("/", 1)[-1]
|
||||
msg = str(results.value(res, SH.resultMessage) or "")
|
||||
src = str(results.value(res, SH.sourceShape) or "").rsplit("/", 1)[-1]
|
||||
rows.append((sev, src, focus, msg))
|
||||
|
||||
violations = sum(1 for r in rows if r[0] == "Violation")
|
||||
warnings = len(rows) - violations
|
||||
|
||||
# ---- report -----------------------------------------------------------
|
||||
print(BAR)
|
||||
print("PR META MODEL - SHACL VALIDATION")
|
||||
print(" data : " + ", ".join(rel(f) for f in data_files))
|
||||
print(" shapes : " + rel(SHAPES))
|
||||
print(" ontology : " + (rel(ONTOLOGY) if onto is not None else "(absent - no entailment)"))
|
||||
print(" triples : %d data / %d shapes" % (len(data), len(shapes)))
|
||||
print(BAR)
|
||||
print("CONFORMS : %s" % conforms)
|
||||
print("RESULTS : %d total | %d BLOCKING | %d MAJOR (warning)"
|
||||
% (len(rows), violations, warnings))
|
||||
|
||||
if rows and not quiet:
|
||||
print("-" * W)
|
||||
by_shape = defaultdict(list)
|
||||
for sev, src, focus, msg in rows:
|
||||
by_shape[(sev, src)].append((focus, msg))
|
||||
# blocking first, then by volume: the rules to fix first come first
|
||||
order = sorted(by_shape.items(),
|
||||
key=lambda kv: (kv[0][0] != "Violation", -len(kv[1])))
|
||||
for (sev, src), items in order:
|
||||
tag = "BLOCK" if sev == "Violation" else "WARN "
|
||||
print("[%s] %-42s %3d node(s)" % (tag, src or "(unnamed shape)", len(items)))
|
||||
print(" %s" % (items[0][1][:66] or "(no message)"))
|
||||
for focus, _ in items[:6]:
|
||||
print(" - %s" % focus)
|
||||
if len(items) > 6:
|
||||
print(" ... and %d more" % (len(items) - 6))
|
||||
|
||||
print(BAR)
|
||||
sys.exit(0 if violations == 0 else 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+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