diff --git a/instances/sodh.ttl b/instances/sodh.ttl index e9ad9f0..74566bd 100644 --- a/instances/sodh.ttl +++ b/instances/sodh.ttl @@ -67,31 +67,60 @@ ex:SD_10_01 a pr:SubDomain ; 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:belongsTo ex:DD_04 ; - pr:hasStatus "PUBLISHED" ; pr:hasVersion "1.0" . + pr:hasName "Customer Structure" ; + pr:hasIdentifier "SD-04.01" ; + pr:owningDomain ex:DD_04 ; pr:ownedBy ex:ST_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:belongsTo ex:DD_16 ; - pr:hasStatus "PUBLISHED" ; pr:hasVersion "1.0" . + pr:hasName "Currency & Exchange Rate" ; + pr:hasIdentifier "SD-16.01" ; + pr:owningDomain ex:DD_16 ; pr:ownedBy ex:ST_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:belongsTo ex:DD_16 ; - pr:hasStatus "PUBLISHED" ; pr:hasVersion "1.0" . + pr:hasName "Fiscal Calendar" ; + pr:hasIdentifier "SD-16.02" ; + pr:owningDomain ex:DD_16 ; pr:ownedBy ex:ST_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:belongsTo ex:DD_21 ; - pr:hasStatus "PUBLISHED" ; pr:hasVersion "1.0" . + pr:hasName "Standard Calendar" ; + pr:hasIdentifier "SD-21.01" ; + pr:owningDomain ex:DD_21 ; pr:ownedBy ex:ST_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:belongsTo ex:DD_05 ; pr:hasStatus "PUBLISHED" ; pr:hasVersion "1.0" . + +# --- sub-domains from the v1.1 structure proposal -------------------- +# 'Customer & Outlet' and 'Marketing Entity & Fiscal Calendar' had the +# same defect as the compound Business Objects: a name joined by '&' +# has no single subject. Same rule, one level up. +ex:SD_04_02 a pr:SubDomain ; + pr:hasIdentifier "SD-04.02" ; pr:hasName "Outlet & Points of Sale" ; + pr:belongsTo ex:DD_04 ; pr:owningDomain ex:DD_04 ; + pr:arbitrationStatus "TO_ARBITRATE" ; + pr:hasStatus "DRAFT" ; pr:hasVersion "1.1" ; pr:hasSource "DGO proposal, split of a compound sub-domain" . +ex:SD_04_03 a pr:SubDomain ; + pr:hasIdentifier "SD-04.03" ; pr:hasName "Geography" ; + pr:belongsTo ex:DD_04 ; pr:owningDomain ex:DD_04 ; + pr:arbitrationStatus "TO_ARBITRATE" ; + pr:hasStatus "DRAFT" ; pr:hasVersion "1.1" ; pr:hasSource "DGO proposal, split of a compound sub-domain" . +ex:SD_16_03 a pr:SubDomain ; + pr:hasIdentifier "SD-16.03" ; pr:hasName "Organisation Structure" ; + pr:belongsTo ex:DD_16 ; pr:owningDomain ex:DD_16 ; + pr:arbitrationStatus "TO_ARBITRATE" ; + pr:hasStatus "DRAFT" ; pr:hasVersion "1.1" ; pr:hasSource "DGO proposal, split of a compound sub-domain" . + ex:BC_06_01_001 a pr:BusinessConcept ; pr:hasIdentifier "BC-06.01-001" ; pr:hasName "Sell Out" ; pr:owningDomain ex:DD_06 ; pr:ownedBy ex:ST_DD_06 ; @@ -168,20 +197,22 @@ ex:BC_21_01_001 a pr:BusinessConcept ; # --- concepts required by the meaning XOR --------------------------- # Country, Marketing Entity and Fiscal Period had Data Elements but no # notion behind them. The rule forced them to be named. -ex:BC_04_01_003 a pr:BusinessConcept ; - pr:hasIdentifier "BC-04.01-003" ; pr:hasName "Country" ; +ex:BC_04_03_001 a pr:BusinessConcept ; + pr:hasIdentifier "BC-04.03-001" ; pr:hasName "Country" ; pr:owningDomain ex:DD_04 ; pr:ownedBy ex:ST_DD_04 ; pr:hasBusinessDefinition "A sovereign territory used as the geographic frame for retail measurement, identified by its ISO 3166 code. Distinct from Customer and Outlet: it is where they operate, not what they are." ; 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_003 a pr:BusinessConcept ; - pr:hasIdentifier "BC-16.01-003" ; pr:hasName "Marketing Entity" ; - pr:owningDomain ex:DD_16 ; pr:ownedBy ex:ST_DD_16 ; - pr:hasBusinessDefinition "An organisational unit of the group holding commercial responsibility for a market, and the level at which financial results are consolidated." ; - 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_004 a pr:BusinessConcept ; - pr:hasIdentifier "BC-16.01-004" ; pr:hasName "Fiscal Period" ; +ex:BC_16_03_001 a pr:BusinessConcept ; + pr:hasBusinessRule "Named Marketing Entity, not Management Entity: the element is Marketing Entity Code and the column is MARKETING_ENTITY_CD. To be confirmed by DD-16." ; + pr:hasIdentifier "BC-16.03-001" ; pr:hasName "Marketing Entity" ; + pr:owningDomain ex:DD_16 ; pr:ownedBy ex:ST_DD_16 ; + pr:hasBusinessDefinition "An organisational unit of the group holding commercial responsibility for a market, and the level at which financial results are consolidated." ; + pr:arbitrationStatus "TO_ARBITRATE" ; + pr:hasStatus "DRAFT" ; pr:hasVersion "1.1" ; pr:hasSource "DGO proposal, pending ratification by the owning domain" + . +ex:BC_16_02_001 a pr:BusinessConcept ; + pr:hasIdentifier "BC-16.02-001" ; pr:hasName "Fiscal Period" ; pr:owningDomain ex:DD_16 ; pr:ownedBy ex:ST_DD_16 ; pr:hasBusinessDefinition "A reporting interval of the Pernod Ricard fiscal year, which runs July to June. Distinct from Calendar Date: the fiscal frame does not align with the Gregorian one." ; pr:arbitrationStatus "TO_ARBITRATE" ; @@ -193,8 +224,6 @@ ex:BO_06_01_001 a pr:BusinessObject ; 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: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 ; pr:hasStatus "DRAFT" ; pr:hasVersion "0.6" ; pr:hasSource "SODH Gold / F_SELL_OUT" . ex:BO_06_01_002 a pr:BusinessObject ; @@ -203,8 +232,6 @@ ex:BO_06_01_002 a pr:BusinessObject ; 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: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_002 ; pr:hasStatus "DRAFT" ; pr:hasVersion "0.6" ; pr:hasSource "SODH Gold / F_SELL_OUT" . ex:BO_06_01_003 a pr:BusinessObject ; @@ -213,8 +240,6 @@ ex:BO_06_01_003 a pr:BusinessObject ; 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:aboutConcept ex:BC_06_01_004 ; - pr:usesConcept ex:BC_06_01_003 ; pr:hasMetric ex:M_06_01_003 , ex:M_06_01_004 , ex:M_06_01_005 ; pr:hasStatus "DRAFT" ; pr:hasVersion "0.6" ; pr:hasSource "SODH Gold / F_SELL_OUT_DISTRIBUTION" . ex:BO_06_01_004 a pr:BusinessObject ; @@ -223,8 +248,6 @@ ex:BO_06_01_004 a pr:BusinessObject ; 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:aboutConcept ex:BC_06_01_002 ; - pr:usesConcept ex:BC_06_01_001 , ex:BC_06_01_003 ; pr:hasMetric ex:M_06_01_006 , ex:M_06_01_007 , ex:M_06_01_008 ; pr:hasStatus "DRAFT" ; pr:hasVersion "0.6" ; pr:hasSource "SODH Gold / F_SELL_OUT" . ex:BO_06_01_005 a pr:BusinessObject ; @@ -233,64 +256,95 @@ ex:BO_06_01_005 a pr:BusinessObject ; 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:aboutConcept ex:BC_06_01_005 ; - pr:usesConcept ex:BC_06_01_001 , ex:BC_05_01_001 ; pr:hasMetric ex:M_06_01_009 , ex:M_06_01_010 ; 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:belongsTo ex:SD_10_01 ; pr:monitoredBy ex:ST_DD_10 ; - pr:aboutConcept ex:BC_10_01_001 ; - pr:aboutConcept ex:BC_10_01_001 ; - pr:hasStatus "DRAFT" ; pr:hasVersion "0.6" ; pr:hasSource "MDM Product -> SODH [concepts TBC by DD-10]" . + pr:aboutConcept ex:BC_10_01_001 ; + pr:belongsTo ex:SD_10_01 ; + pr:hasName "Product Hierarchy" ; + pr:hasIdentifier "BO-10.01-001" ; + pr:owningDomain ex:DD_10 ; pr:ownedBy ex:ST_DD_10 ; pr:monitoredBy ex:ST_DD_10 ; + 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" ; - 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_001 ; - 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]" . + pr:aboutConcept ex:BC_04_01_001 ; + pr:belongsTo ex:SD_04_01 ; + pr:hasName "Customer Hierarchy" ; + pr:hasIdentifier "BO-04.01-001" ; + pr:owningDomain ex:DD_04 ; pr:ownedBy ex:ST_DD_04 ; pr:monitoredBy ex:ST_DD_04 ; + 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" ; - 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_001 ; - 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]" . + pr:aboutConcept ex:BC_16_01_001 ; + pr:belongsTo ex:SD_16_01 ; + pr:hasIdentifier "BO-16.01-001" ; pr:hasName "Currency" ; + pr:owningDomain ex:DD_16 ; pr:ownedBy ex:ST_DD_16 ; pr:monitoredBy ex:ST_DD_16 ; + 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:belongsTo ex:SD_21_01 ; pr:monitoredBy ex:ST_DD_21 ; - pr:aboutConcept ex:BC_21_01_001 ; - pr:aboutConcept ex:BC_21_01_001 ; - pr:hasStatus "DRAFT" ; pr:hasVersion "0.6" ; pr:hasSource "Standard Calendar -> SODH [concepts TBC by DD-21]" . + pr:aboutConcept ex:BC_21_01_001 ; + pr:belongsTo ex:SD_21_01 ; + pr:hasName "Analysis Period" ; + pr:hasIdentifier "BO-21.01-001" ; + pr:owningDomain ex:DD_21 ; pr:ownedBy ex:ST_DD_21 ; pr:monitoredBy ex:ST_DD_21 ; + 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:belongsTo ex:SD_05_01 ; pr:monitoredBy ex:ST_DD_05 ; pr:aboutConcept ex:BC_05_01_001 ; - 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_04_02_001 a pr:BusinessObject ; + pr:aboutConcept ex:BC_04_01_002 ; + pr:belongsTo ex:SD_04_02 ; + pr:hasIdentifier "BO-04.02-001" ; pr:hasName "Outlet" ; + pr:owningDomain ex:DD_04 ; pr:ownedBy ex:ST_DD_04 ; pr:monitoredBy ex:ST_DD_04 ; + 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]" . + pr:aboutConcept ex:BC_16_01_002 ; + pr:belongsTo ex:SD_16_01 ; + pr:hasIdentifier "BO-16.01-002" ; pr:hasName "Exchange Rate" ; + pr:owningDomain ex:DD_16 ; pr:ownedBy ex:ST_DD_16 ; pr:monitoredBy ex:ST_DD_16 ; + 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]" + . + + +# --- business objects for the borrowed concepts ----------------------- +# Country, Fiscal Period and Marketing Entity had a concept but nothing +# to hold it. Proposed to DD-04 and DD-16, hence TO_ARBITRATE. +ex:BO_04_03_001 a pr:BusinessObject ; + pr:aboutConcept ex:BC_04_03_001 ; + pr:belongsTo ex:SD_04_03 ; + pr:hasIdentifier "BO-04.03-001" ; pr:hasName "Country" ; + pr:owningDomain ex:DD_04 ; pr:ownedBy ex:ST_DD_04 ; pr:monitoredBy ex:ST_DD_04 ; + pr:arbitrationStatus "TO_ARBITRATE" ; + pr:hasStatus "DRAFT" ; pr:hasVersion "1.1" ; pr:hasSource "DGO proposal, pending ratification by the owning domain" + . +ex:BO_16_02_001 a pr:BusinessObject ; + pr:aboutConcept ex:BC_16_02_001 ; + pr:belongsTo ex:SD_16_02 ; + pr:hasIdentifier "BO-16.02-001" ; pr:hasName "Fiscal Period" ; + pr:owningDomain ex:DD_16 ; pr:ownedBy ex:ST_DD_16 ; pr:monitoredBy ex:ST_DD_16 ; + pr:arbitrationStatus "TO_ARBITRATE" ; + pr:hasStatus "DRAFT" ; pr:hasVersion "1.1" ; pr:hasSource "DGO proposal, pending ratification by the owning domain" + . +ex:BO_16_03_001 a pr:BusinessObject ; + pr:aboutConcept ex:BC_16_03_001 ; + pr:belongsTo ex:SD_16_03 ; + pr:hasIdentifier "BO-16.03-001" ; pr:hasName "Marketing Entity" ; + pr:owningDomain ex:DD_16 ; pr:ownedBy ex:ST_DD_16 ; pr:monitoredBy ex:ST_DD_16 ; + pr:arbitrationStatus "TO_ARBITRATE" ; + pr:hasStatus "DRAFT" ; pr:hasVersion "1.1" ; pr:hasSource "DGO proposal, pending ratification by the owning domain" + . ex:DO_06_01_001 a pr:DataObject ; pr:hasIdentifier "DO-06.01-001" ; pr:hasName "Sell Out Volume Dataset" ; @@ -585,14 +639,14 @@ ex:DE_04_01_0008 a pr:DataElement ; pr:owningDomain ex:DD_04 ; pr:hasFormat "STRING(2)" ; pr:physicalName "COUNTRY_CD" ; - pr:represents ex:BC_04_01_003 ; + pr:represents ex:BC_04_03_001 ; pr:hasStatus "DRAFT" ; pr:hasVersion "1.1" ; pr:hasSource "MDM Customer" . ex:DE_04_01_0009 a pr:DataElement ; pr:hasIdentifier "DE-04.01-0009" ; pr:hasName "Country Name" ; pr:owningDomain ex:DD_04 ; pr:hasFormat "STRING(100)" ; pr:physicalName "COUNTRY_NM" ; - pr:represents ex:BC_04_01_003 ; + pr:represents ex:BC_04_03_001 ; pr:hasStatus "DRAFT" ; pr:hasVersion "1.1" ; pr:hasSource "MDM Customer" . ex:DE_04_01_0010 a pr:DataElement ; pr:hasIdentifier "DE-04.01-0010" ; pr:hasName "Point of Sale Count (Count)" ; @@ -630,21 +684,21 @@ ex:DE_16_02_0001 a pr:DataElement ; pr:owningDomain ex:DD_16 ; pr:hasFormat "STRING(10)" ; pr:physicalName "MARKETING_ENTITY_CD" ; - pr:represents ex:BC_16_01_003 ; + pr:represents ex:BC_16_03_001 ; pr:hasStatus "DRAFT" ; pr:hasVersion "1.1" ; pr:hasSource "Group Finance" . ex:DE_16_02_0002 a pr:DataElement ; pr:hasIdentifier "DE-16.02-0002" ; pr:hasName "Fiscal Period Code" ; pr:owningDomain ex:DD_16 ; pr:hasFormat "STRING(10)" ; pr:physicalName "PR_FISCAL_PERIOD_CD" ; - pr:represents ex:BC_16_01_004 ; + pr:represents ex:BC_16_02_001 ; pr:hasStatus "DRAFT" ; pr:hasVersion "1.1" ; pr:hasSource "Group Finance" . ex:DE_16_02_0003 a pr:DataElement ; pr:hasIdentifier "DE-16.02-0003" ; pr:hasName "Fiscal Quarter Code" ; pr:owningDomain ex:DD_16 ; pr:hasFormat "STRING(10)" ; pr:physicalName "PR_FISCAL_QUARTER_CD" ; - pr:represents ex:BC_16_01_004 ; + pr:represents ex:BC_16_02_001 ; pr:hasStatus "DRAFT" ; pr:hasVersion "1.1" ; pr:hasSource "Group Finance" . ex:DE_21_01_0001 a pr:DataElement ; pr:hasIdentifier "DE-21.01-0001" ; pr:hasName "Calendar Date" ; diff --git a/scripts/apply_arbitration_sodh.py b/scripts/apply_arbitration_sodh.py index ea2fe35..1b267c9 100644 --- a/scripts/apply_arbitration_sodh.py +++ b/scripts/apply_arbitration_sodh.py @@ -95,7 +95,7 @@ NEW_CONCEPTS = [ # ------------------------------------------------------------------- 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:BO_04_02_001", "BO-04.02-001", "Outlet", "ex:DD_04", "ex:ST_DD_04", "ex:SD_04_02", "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]"), @@ -176,7 +176,13 @@ def main(): continue block = m.group(1) + # strip what a previous run may have written, so re-running is a no-op. + # Duplicated clauses carrying the SAME value merge under RDF set + # semantics: the graph stays valid and SHACL sees nothing, while the + # file quietly grows a redundant line on every pass. new_block = re.sub(r'[ \t]*pr:composedOf\s+[^";]*;[ \t]*\n', '', block) + new_block = re.sub(r'[ \t]*pr:(aboutConcept|usesConcept)\s+[^";]*;[ \t]*\n', + '', new_block) clause = ' pr:aboutConcept %s ;\n' % about if uses: clause += ' pr:usesConcept %s ;\n' % " , ".join(uses) @@ -244,6 +250,13 @@ def main(): for line in report: print(" " + line) print("=" * W) + doubled = [] + for m in re.finditer(r'^(ex:\w+) a pr:\w+ ;(.*?)\.\s*\n', text, re.S | re.M): + for prop in ("pr:hasName", "pr:aboutConcept", "pr:belongsTo"): + if len(re.findall(r'(? 1: + doubled.append("%s/%s" % (m.group(1), prop)) + print(" duplicated single-valued properties: %s" + % (", ".join(doubled) if doubled else "none")) 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" diff --git a/scripts/apply_br013_sodh.py b/scripts/apply_br013_sodh.py index 7540426..8bfce27 100644 --- a/scripts/apply_br013_sodh.py +++ b/scripts/apply_br013_sodh.py @@ -71,14 +71,14 @@ DE_CONCEPT_EXACT = { "DE-04.01-0006": "ex:BC_04_01_002", # Outlet Type -> Outlet "DE-04.01-0007": "ex:BC_04_01_002", # Channel -> Outlet "DE-04.01-0010": "ex:BC_04_01_002", # Point of Sale Count -> Outlet - "DE-04.01-0008": "ex:BC_04_01_003", # Country Code -> Country - "DE-04.01-0009": "ex:BC_04_01_003", # Country Name -> Country + "DE-04.01-0008": "ex:BC_04_03_001", # Country Code -> Country + "DE-04.01-0009": "ex:BC_04_03_001", # Country Name -> Country "DE-16.01-0001": "ex:BC_16_01_001", # Currency Code -> Currency "DE-16.01-0002": "ex:BC_16_01_002", # FX Rate to-Euro -> Exchange Rate "DE-16.01-0003": "ex:BC_16_01_002", # FX Rate to-USD -> Exchange Rate - "DE-16.02-0001": "ex:BC_16_01_003", # Marketing Entity -> Marketing Entity - "DE-16.02-0002": "ex:BC_16_01_004", # Fiscal Period Code -> Fiscal Period - "DE-16.02-0003": "ex:BC_16_01_004", # Fiscal Quarter Code -> Fiscal Period + "DE-16.02-0001": "ex:BC_16_03_001", # Marketing Entity -> Marketing Entity + "DE-16.02-0002": "ex:BC_16_02_001", # Fiscal Period Code -> Fiscal Period + "DE-16.02-0003": "ex:BC_16_02_001", # Fiscal Quarter Code -> Fiscal Period } DE_CONCEPT_PREFIX = [ ("DE-10.01", "ex:BC_10_01_001"), # product hierarchy -> Product @@ -89,14 +89,17 @@ DE_CONCEPT_PREFIX = [ # ---- concepts the XOR forces us to name ------------------------------------ NEW_CONCEPTS = [ - ("ex:BC_04_01_003", "BC-04.01-003", "Country", "ex:DD_04", "ex:ST_DD_04", + # Final identifiers from the outset. Creating them under one id and + # renumbering them later broke idempotence: the second run no longer found + # the original id and recreated the concept alongside the renamed one. + ("ex:BC_04_03_001", "BC-04.03-001", "Country", "ex:DD_04", "ex:ST_DD_04", "A sovereign territory used as the geographic frame for retail measurement, " "identified by its ISO 3166 code. Distinct from Customer and Outlet: it is where " "they operate, not what they are."), - ("ex:BC_16_01_003", "BC-16.01-003", "Marketing Entity", "ex:DD_16", "ex:ST_DD_16", + ("ex:BC_16_03_001", "BC-16.03-001", "Marketing Entity", "ex:DD_16", "ex:ST_DD_16", "An organisational unit of the group holding commercial responsibility for a market, " "and the level at which financial results are consolidated."), - ("ex:BC_16_01_004", "BC-16.01-004", "Fiscal Period", "ex:DD_16", "ex:ST_DD_16", + ("ex:BC_16_02_001", "BC-16.02-001", "Fiscal Period", "ex:DD_16", "ex:ST_DD_16", "A reporting interval of the Pernod Ricard fiscal year, which runs July to June. " "Distinct from Calendar Date: the fiscal frame does not align with the Gregorian one."), ] diff --git a/scripts/apply_structure_sodh.py b/scripts/apply_structure_sodh.py new file mode 100644 index 0000000..66525cb --- /dev/null +++ b/scripts/apply_structure_sodh.py @@ -0,0 +1,261 @@ +#!/usr/bin/env python3 +""" +SODH - structure proposed for the borrowed domains +=================================================== +Gives the nine TO_ARBITRATE concepts a home: a Business Object and a +Sub-Domain each, per the structure arbitrated with Bastien on 2026-07-27. + +USAGE + python3 scripts/apply_structure_sodh.py # dry run + python3 scripts/apply_structure_sodh.py --apply # rewrite, .bak kept + +STILL A PROPOSAL + Everything created here stays TO_ARBITRATE. DD-04, DD-10, DD-16 and DD-21 + own these perimeters; the DGO is proposing a coherent structure, not + ratifying one. Replacing padding with an equally unilateral structure + would trade one problem for a tidier version of the same problem. The flag + comes off when Helene Puchot, Anas El Kesri and Gaelle Seret say so, and + OW-007 blocks publication until then. + +IDENTIFIERS ARE FINAL FROM CREATION + Identifiers encode the sub-domain (BO-04.02-001 lives in SD-04.02), so the + upstream scripts now mint them correctly rather than having this one + renumber afterwards. Renaming after creation broke idempotence: the script + that created an object stopped recognising its own output and made a second + copy on the next run. + +COMPOUND SUB-DOMAINS SPLIT TOO + 'Customer & Outlet' and 'Marketing Entity & Fiscal Calendar' have the same + defect as the Business Objects that were split earlier: a name joined by + '&' has no single subject. Same rule, one level up. +""" +import os +import re +import shutil +import sys + +REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +TTL = os.path.join(REPO, "instances", "sodh.ttl") +W = 78 + +# No IRI renaming here. Objects are created with their final identifiers by +# the upstream scripts: renumbering after the fact broke idempotence, because +# the creating script then no longer recognised its own output. +# ---- sub-domains ---------------------------------------------------------- +SD_RENAME = { + "ex:SD_04_01": "Customer Structure", + "ex:SD_16_01": "Currency & Exchange Rate", + "ex:SD_16_02": "Fiscal Calendar", + "ex:SD_21_01": "Standard Calendar", +} +SD_NEW = [ + ("ex:SD_04_02", "SD-04.02", "Outlet & Points of Sale", "ex:DD_04"), + ("ex:SD_04_03", "SD-04.03", "Geography", "ex:DD_04"), + ("ex:SD_16_03", "SD-16.03", "Organisation Structure", "ex:DD_16"), +] + +# ---- business objects ----------------------------------------------------- +BO_RENAME = { + "ex:BO_10_01_001": "Product Hierarchy", + "ex:BO_04_01_001": "Customer Hierarchy", + "ex:BO_21_01_001": "Analysis Period", +} +BO_NEW = [ + ("ex:BO_04_03_001", "BO-04.03-001", "Country", "ex:DD_04", "ex:ST_DD_04", + "ex:SD_04_03", "ex:BC_04_03_001"), + ("ex:BO_16_02_001", "BO-16.02-001", "Fiscal Period", "ex:DD_16", "ex:ST_DD_16", + "ex:SD_16_02", "ex:BC_16_02_001"), + ("ex:BO_16_03_001", "BO-16.03-001", "Marketing Entity", "ex:DD_16", "ex:ST_DD_16", + "ex:SD_16_03", "ex:BC_16_03_001"), +] + +# ---- where each object finally sits --------------------------------------- +BELONGS = { + "ex:BO_10_01_001": "ex:SD_10_01", + "ex:BO_04_01_001": "ex:SD_04_01", + "ex:BO_04_02_001": "ex:SD_04_02", + "ex:BO_04_03_001": "ex:SD_04_03", + "ex:BO_16_01_001": "ex:SD_16_01", + "ex:BO_16_01_002": "ex:SD_16_01", + "ex:BO_16_02_001": "ex:SD_16_02", + "ex:BO_16_03_001": "ex:SD_16_03", + "ex:BO_21_01_001": "ex:SD_21_01", +} + +# ---- subject of each borrowed object -------------------------------------- +ABOUT = { + "ex:BO_10_01_001": "ex:BC_10_01_001", # Product Hierarchy -> Product + "ex:BO_04_01_001": "ex:BC_04_01_001", # Customer Hierarchy -> Customer + "ex:BO_04_02_001": "ex:BC_04_01_002", # Outlet -> Outlet + "ex:BO_04_03_001": "ex:BC_04_03_001", # Country -> Country + "ex:BO_16_01_001": "ex:BC_16_01_001", # Currency -> Currency + "ex:BO_16_01_002": "ex:BC_16_01_002", # Exchange Rate -> Exchange Rate + "ex:BO_16_02_001": "ex:BC_16_02_001", # Fiscal Period -> Fiscal Period + "ex:BO_16_03_001": "ex:BC_16_03_001", # Marketing Entity -> Marketing Entity + "ex:BO_21_01_001": "ex:BC_21_01_001", # Analysis Period -> Calendar Date +} + +MARKETING_NOTE = ("Named Marketing Entity, not Management Entity: the element is " + "Marketing Entity Code and the column is MARKETING_ENTITY_CD. " + "To be confirmed by DD-16.") + + +def sd_block(i, ident, name, domain): + return ('%s a pr:SubDomain ;\n' + ' pr:hasIdentifier "%s" ; pr:hasName "%s" ;\n' + ' pr:belongsTo %s ; pr:owningDomain %s ;\n' + ' pr:arbitrationStatus "TO_ARBITRATE" ;\n' + ' pr:hasStatus "DRAFT" ; pr:hasVersion "1.1" ; ' + 'pr:hasSource "DGO proposal, split of a compound sub-domain" .' + % (i, ident, name, domain, domain)) + + +def bo_block(i, ident, name, domain, steward, sd, bc): + return ('%s a pr:BusinessObject ;\n' + ' pr:hasIdentifier "%s" ; pr:hasName "%s" ;\n' + ' pr:owningDomain %s ; pr:ownedBy %s ;\n' + ' pr:belongsTo %s ; pr:monitoredBy %s ;\n' + ' pr:aboutConcept %s ;\n' + ' pr:arbitrationStatus "TO_ARBITRATE" ;\n' + ' pr:hasStatus "DRAFT" ; pr:hasVersion "1.1" ; ' + 'pr:hasSource "DGO proposal, pending ratification by the owning domain" .' + % (i, ident, name, domain, steward, sd, steward, bc)) + + +def _split_clauses(body): + """Split a Turtle predicate list on ';' that sit outside string literals.""" + out, buf, i, n = [], [], 0, len(body) + while i < n: + c = body[i] + if c == '"': + j = i + 1 + while j < n and body[j] != '"': + j += 2 if body[j] == "\\" else 1 + j = min(j + 1, n) + buf.append(body[i:j]); i = j; continue + if c == ";": + out.append("".join(buf)); buf = []; i += 1; continue + buf.append(c); i += 1 + out.append("".join(buf)) + return out + + +def set_prop(text, subject, prop, value): + """ + Set or replace a single-valued property inside a subject block. + + Clause-level, not regex-level: a property can sit mid-line + (pr:hasIdentifier "X" ; pr:hasName "Y" ;) and its value can contain a + semicolon inside the literal. A line-anchored pattern silently misses both + and APPENDS instead of replacing, which produces a second value and a + cardinality violation -- exactly what happened on the first run here. + """ + pat = re.compile(r'(^%s a pr:\w+ ;)(.*?)(\.\s*\n)' % re.escape(subject), re.S | re.M) + m = pat.search(text) + if not m: + return text, False + kept = [c for c in _split_clauses(m.group(2)) + if c.strip() and not re.match(r'\s*%s\s' % re.escape(prop), c)] + kept.insert(0, "\n %s %s " % (prop, value)) + body = " ;".join(kept) + if not body.endswith("\n"): + body = body.rstrip() + "\n " + return text[:m.start()] + m.group(1) + body + m.group(3) + text[m.end():], True + + +def main(): + apply_changes = "--apply" in sys.argv + if not os.path.exists(TTL): + print("Not found: %s" % TTL) + sys.exit(2) + text = original = open(TTL, encoding="utf-8").read() + report = [] + + # 2. sub-domains + for sd, name in SD_RENAME.items(): + text, ok = set_prop(text, sd, "pr:hasName", '"%s"' % name) + if ok: + report.append("renamed %s -> %s" % (sd.replace("ex:SD_", "SD-"), name)) + + todo_sd = [s for s in SD_NEW if not re.search(r'^%s a ' % re.escape(s[0]), text, re.M)] + if todo_sd: + anchor = re.search(r'\n(?=ex:BC_\w+ a pr:BusinessConcept ;|ex:BO_\w+ a pr:BusinessObject ;)', + text) + blocks = "\n".join(sd_block(*s) for s in todo_sd) + text = (text[:anchor.start()] + "\n\n" + + "# --- sub-domains from the v1.1 structure proposal --------------------\n" + + "# 'Customer & Outlet' and 'Marketing Entity & Fiscal Calendar' had the\n" + + "# same defect as the compound Business Objects: a name joined by '&'\n" + + "# has no single subject. Same rule, one level up.\n" + + blocks + "\n" + text[anchor.start():]) + report.append("created %d sub-domains%s" + % (len(todo_sd), "" if todo_sd else " (already present)")) + + # 3. new business objects + todo_bo = [b for b in BO_NEW if not re.search(r'^%s a ' % re.escape(b[0]), text, re.M)] + if todo_bo: + anchor = re.search(r'\n(?=ex:DO_\w+ a pr:DataObject ;|ex:M_\w+ a pr:Metric ;)', text) + blocks = "\n".join(bo_block(*b) for b in todo_bo) + text = (text[:anchor.start()] + "\n\n" + + "# --- business objects for the borrowed concepts -----------------------\n" + + "# Country, Fiscal Period and Marketing Entity had a concept but nothing\n" + + "# to hold it. Proposed to DD-04 and DD-16, hence TO_ARBITRATE.\n" + + blocks + "\n" + text[anchor.start():]) + report.append("created %d business objects%s" + % (len(todo_bo), "" if todo_bo else " (already present)")) + + # 4. renames, placement, subject + for bo, name in BO_RENAME.items(): + text, _ = set_prop(text, bo, "pr:hasName", '"%s"' % name) + report.append("renamed %d business objects to the agreed nomenclature" % len(BO_RENAME)) + + n_b = n_a = 0 + for bo, sd in BELONGS.items(): + text, ok = set_prop(text, bo, "pr:belongsTo", sd) + n_b += ok + for bo, bc in ABOUT.items(): + text, ok = set_prop(text, bo, "pr:aboutConcept", bc) + n_a += ok + report.append("placed %d objects in their sub-domain, %d subjects confirmed" % (n_b, n_a)) + + # 5. the naming caveat, recorded on the object rather than in a side note + text, ok = set_prop(text, "ex:BC_16_03_001", "pr:hasBusinessRule", '"%s"' % MARKETING_NOTE) + if ok: + report.append("recorded the Marketing/Management naming caveat on the concept") + + # ---- report + print() + print("SODH STRUCTURE PROPOSAL %s" % ("APPLY" if apply_changes else "DRY RUN")) + print("=" * W) + for line in report: + print(" " + line) + print("=" * W) + defined = set(re.findall(r'^(ex:\w+) a ', text, re.M)) + dangling = sorted(set(re.findall(r'\bex:\w+', text)) - defined) + print(" dangling references: %s" % (", ".join(dangling) if dangling else "none")) + for cls in ("SubDomain", "BusinessObject", "BusinessConcept"): + ids = re.findall(r'^(ex:\w+) a pr:%s\b' % cls, text, re.M) + dup = [i for i in set(ids) if ids.count(i) > 1] + print(" %-16s %3d duplicates %d" % (cls, len(ids), len(dup))) + doubled = [] + for m in re.finditer(r'^(ex:\w+) a pr:\w+ ;(.*?)\.\s*\n', text, re.S | re.M): + for prop in ("pr:hasName", "pr:hasIdentifier", "pr:belongsTo", "pr:aboutConcept"): + if len(re.findall(r'%s\s' % re.escape(prop), m.group(2))) > 1: + doubled.append("%s/%s" % (m.group(1), prop)) + print(" duplicated single-valued properties: %s" + % (", ".join(doubled) if doubled else "none")) + arb = len(re.findall(r'pr:arbitrationStatus "TO_ARBITRATE"', text)) + print(" TO_ARBITRATE %3d (nothing publishes until the domains ratify)" % arb) + print("=" * W) + + if text != original and apply_changes: + shutil.copy2(TTL, TTL + ".bak") + open(TTL, "w", encoding="utf-8").write(text) + print(" written, backup at %s.bak" % os.path.basename(TTL)) + elif text != original: + print(" dry run -- re-run with --apply to write") + print() + + +if __name__ == "__main__": + main()