PR Data Meta Model - SHACL Shapes
modified: 2026-07-30
comment: Executable form of the rulebook. Validated by scripts/run_shacl_validation.py.

BIFieldShape
type: NodeShape
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.
property:
  class: Metric
  max count: 1
  message: Which certified Metric this BI field implements. Comparing expressions across fields realizing one Metric is the harmonisation measure.
  path: exposesMetric
  severity: Info
target class: BusinessIntelligenceField

BusinessConceptShape
type: NodeShape
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.
property:
  datatype: string
  max count: 1
  message: A Business Concept exists to carry meaning: it must hold exactly one business definition of at least 20 characters.
  min count: 1
  min length: 20
  path: businessDefinition
  severity: Violation
property:
  max count: 0
  message: BR-003 -- the presence of a formula is what makes something a Metric. A Concept with a formula is a misclassified Metric.
  path: formula
  severity: Violation
property:
  max count: 0
  message: A Business Concept has no system of truth. Its authority is its owning domain and its definition belongs in the glossary. Naming a system here would ask which of the systems that USE the concept defines it -- a question with no answer.
  path: sourcedFrom
  severity: Violation
target class: BusinessConcept

BusinessObjectShape
type: NodeShape
property:
  class: DataSubDomain
  max count: 1
  message: A Business Object belongs to exactly one Sub-Domain.
  min count: 1
  path: belongsTo
  severity: Violation
property:
  class: BusinessConcept
  max count: 1
  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.
  min count: 1
  path: isAbout
  severity: Violation
property:
  class: BusinessConcept
  message: Other notions needed to read this object. Zero is a legitimate answer.
  path: usesConcept
  severity: Info
property:
  class: DataSteward
  message: The Business Object is where a steward is declared -- Data Objects and Data Elements inherit from here.
  min count: 1
  path: monitoredBy
  severity: Warning
property:
  max count: 0
  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.
  path: businessDefinition
  severity: Violation
property:
  max count: 0
  message: A Business Object has no system of truth. It is a governance grouping, not data: its data lives in the Data Objects that represent it, and those name the system. In v0.6 this property held a grab-bag on the 15 objects -- physical locations, migration notes, arbitration remarks -- all under one name.
  path: sourcedFrom
  severity: Violation
target class: BusinessObject

CapturedObjectShape
type: NodeShape
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.
property:
  datatype: string
  max count: 1
  message: A native immutable source id makes reconciliation idempotent and survives renames, which names do not.
  path: sourceIdentifier
  severity: Warning
property:
  datatype: dateTime
  max count: 1
  message: Every captured object records when it was harvested. Without it there is no way to tell a current fact from a stale one.
  min count: 1
  path: harvestDate
  severity: Violation
property:
  max count: 0
  message: Captured objects are not attributed directly -- ownership flows from the Data Element mapped to them.
  path: ownedByDomain
  severity: Warning
target class: CapturedObject

DataDomainShape
type: NodeShape
property:
  max count: 1
  message: A Data Domain should name its owner.
  path: hasDomainOwner
  severity: Warning
property:
  max count: 1
  message: A Data Domain should name its Data Governance Lead.
  path: hasGovernanceLead
  severity: Warning
property:
  max count: 1
  path: activationStatus
  severity: Info
target class: DataDomain

DataElementMeaningShape
type: NodeShape
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.
message: A Data Element must reach business meaning by exactly one route: represents a Business Concept (dimensional), or be computedBy a Metric (measure). Never both, never neither. A defined column with no business meaning is debt, not an acceptable state.
severity: Violation
target class: DataElement
xone:
  property:
    max count: 1
    min count: 1
    path: represents
xone:
  property:
    min count: 1
    path:
      inverse path: computedBy

DataElementShape
type: NodeShape
property:
  datatype: string
  max count: 1
  message: NR-009 -- the physical name is recorded verbatim and never altered.
  path: physicalName
  severity: Warning
property:
  max count: 1
  path: fullyQualifiedName
  severity: Info
property:
  max count: 1
  message: A Data Element carries exactly one unit. This is the counterpart of the Metric rule: the mother metric lists the units its variants use, each variant states the single unit it is actually expressed in. An element with two units is two elements.
  path: unit
  severity: Violation
property:
  class: BusinessConcept
  max count: 1
  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.
  path: represents
  severity: Violation
property:
  max count: 0
  message: Stewardship is inherited from the Business Object, not declared here.
  path: monitoredBy
  severity: Warning
target class: DataElement

DataInterfaceShape
type: NodeShape
property:
  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.
  min count: 1
  path: servedBy
  severity: Warning
target class: DataInterface

DataObjectShape
type: NodeShape
property:
  class: BusinessObject
  max count: 1
  message: BR-006 -- a Data Object represents exactly one Business Object. This is the pivot: the single junction between governance and technology.
  min count: 1
  path: represents
  severity: Violation
property:
  class: DataElement
  message: A Data Object holds at least one Data Element.
  min count: 1
  path: hasElement
  severity: Violation
property:
  class: DataElement
  message: State the grain: the elements whose combination makes a row unique. Checkable against the harvested primary key.
  path: hasGrainElement
  severity: Warning
property:
  max count: 0
  message: Stewardship is declared on the Business Object and inherited through represents. Declaring it here allows two answers to the same question.
  path: monitoredBy
  severity: Warning
property:
  class: System
  max count: 1
  message: Name the System holding the authoritative data, as a reference rather than a string. A string could not answer 'what depends on MDM Product'.
  path: sourcedFrom
  severity: Warning
property:
  class: DataStructure
  message: Where the object actually sits: a table, a view, later a node label. The seam between what governance defines and what a harvester captures -- proposed by the reconciliation job, confirmed by a steward.
  path: storedIn
  severity: Warning
property:
  max count: 0
  message: Deprecated in v1.4: storedIn now serves both granularities.
  path: materializedAs
  severity: Warning
target class: DataObject

DataProductShape
type: NodeShape
property:
  class: DataSubDomain
  max count: 1
  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.
  min count: 1
  path: operatedBy
  severity: Violation
property:
  class: DataProductOwner
  max count: 1
  message: A Data Product should name exactly one Product Owner. A warning rather than a block: the SODH owner is genuinely not appointed, and a blocking rule that cannot be satisfied truthfully is how BR-004 produced padding.
  path: hasProductOwner
  severity: Warning
property:
  class: DataObject
  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.
  min count: 1
  path: packages
  severity: Violation
property:
  max count: 1
  message: A Data Product should be governed by a Data Contract.
  path: governedBy
  severity: Warning
property:
  class: DataInterface
  message: The channels through which consumers actually get the data.
  path: exposes
  severity: Info
target class: DataProduct

DataStructureShape
type: NodeShape
property:
  max count: 1
  message: Optional on purpose: not every engine has a schema level.
  path: isInSchema
  severity: Info
property:
  max count: 1
  path: sourcedFrom
  severity: Info
target class: DataStructure

DefinedObjectShape
type: NodeShape
comment: Applies to everything governance writes. Captured objects are deliberately exempt: see CapturedObjectShape.
property:
  datatype: string
  max count: 1
  message: DQ-001 -- every defined object carries exactly one identifier. NOTE: uniqueness across the graph is NOT tested here; SHACL checks cardinality only. The reconciliation job tests uniqueness.
  min count: 1
  path: identifier
  severity: Violation
property:
  max count: 1
  message: A published object should carry a version.
  path: version
  severity: Warning
property:
  datatype: string
  max count: 1
  message: Every defined object carries exactly one canonical name.
  min count: 1
  min length: 3
  path: canonicalName
  severity: Violation
property:
  in: DRAFT
  in: UNDER_REVIEW
  in: PUBLISHED
  in: DEPRECATED
  in: RETIRED
  message: LC-001 -- status must be one of DRAFT, UNDER_REVIEW, PUBLISHED, DEPRECATED, RETIRED.
  min count: 1
  path: status
  severity: Violation
property:
  max count: 1
  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.
  path: ownedByDomain
  severity: Violation
target class: DefinedObject

FieldShape
type: NodeShape
property:
  datatype: string
  max count: 1
  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.
  min count: 1
  path: fullyQualifiedName
  pattern: ^[A-Za-z0-9_$]+\\.[A-Za-z0-9_$]+\\.[A-Za-z0-9_$]+\\.[A-Za-z0-9_$]+$
  severity: Violation
property:
  max count: 1
  message: Every Field lives in exactly one Physical Relation.
  min count: 1
  path: livesIn
  severity: Violation
target class: Field

GrainConsistencyShape
type: NodeShape
sparql:
  message: A grain element must be one of the object's own Data Elements.
  select: PREFIX pr: <https://ontology.pernod-ricard.com/metamodel/> SELECT $this ?grain WHERE { $this pr:hasGrainElement ?grain . FILTER NOT EXISTS { $this pr:hasElement ?grain } }
  severity: Violation
target class: DataObject

MetricShape
type: NodeShape
property:
  datatype: string
  max count: 1
  message: BR-003 -- a Metric is a measurement with a calculation rule. No formula, no Metric.
  min count: 1
  path: formula
  severity: Violation
property:
  message: A Metric states every unit its variants are expressed in. NOT capped at one: Sell Out Volume is computed in 9L and in L, Sell Out Value in EUR, USD and local currency. The single-unit rule belongs on the Data Element, where it holds -- see DataElementShape.
  min count: 1
  path: unit
  severity: Violation
property:
  class: BusinessConcept
  message: A Metric measures at least one Business Concept -- this is how a measure element reaches business meaning.
  min count: 1
  path: measures
  severity: Violation
property:
  max count: 0
  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.
  path: hasGranularity
  severity: Warning
property:
  max count: 0
  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.
  path: physicalName
  severity: Violation
target class: Metric

MetricSourceShape
type: NodeShape
comment: Rewritten in v1.4 to compare System references rather than strings. A string comparison would have called 'SODH Gold' and 'Snowflake (SODH Gold)' two different systems.
sparql:
  message: A Metric's system of truth must match that of at least one element computing it. If they disagree, either the metric points at the wrong system or an element has moved.
  select: PREFIX pr: <https://ontology.pernod-ricard.com/metamodel/> SELECT $this WHERE { $this pr:sourcedFrom ?sys ; pr:computedBy ?de . FILTER NOT EXISTS { $this pr:computedBy ?any . ?any pr:sourcedFrom ?sys . } }
  severity: Violation
sparql:
  message: Elements computing one Metric come from several systems. Legitimate for a metric blending sources, worth a look otherwise.
  select: PREFIX pr: <https://ontology.pernod-ricard.com/metamodel/> SELECT $this WHERE { $this pr:computedBy ?a , ?b . ?a pr:sourcedFrom ?sa . ?b pr:sourcedFrom ?sb . FILTER(?sa != ?sb) }
  severity: Warning
target class: Metric

MotherMetricShape
type: NodeShape
sparql:
  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.
  select: PREFIX pr: <https://ontology.pernod-ricard.com/metamodel/> SELECT $this WHERE { $this pr:status "PUBLISHED" . FILTER NOT EXISTS { $this pr:computedBy ?de } }
  severity: Warning
target class: Metric

NamingShape
type: NodeShape
property:
  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.
  not:
    pattern: _NM$
  path: canonicalName
  severity: Warning
property:
  message: NR-002 -- a canonical name is written for humans, not as a snake_case identifier.
  not:
    pattern: ^[a-z_]+$
  path: canonicalName
  severity: Warning
target class: DefinedObject

PublishedShape
type: NodeShape
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.
sparql:
  message: OW-007 -- an object flagged TO_ARBITRATE cannot be PUBLISHED. Resolve at the Cross-Domain Council first.
  select: PREFIX pr: <https://ontology.pernod-ricard.com/metamodel/> SELECT $this WHERE { $this pr:status "PUBLISHED" ; pr:arbitrationStatus "TO_ARBITRATE" . }
  severity: Violation
sparql:
  message: OW-003 -- a PUBLISHED object must have an owning Data Domain.
  select: PREFIX pr: <https://ontology.pernod-ricard.com/metamodel/> SELECT $this WHERE { $this pr:status "PUBLISHED" . FILTER NOT EXISTS { $this pr:ownedByDomain ?d } }
  severity: Violation
target class: DefinedObject

SubDomainShape
type: NodeShape
property:
  class: DataDomain
  max count: 1
  message: A Sub-Domain belongs to exactly one Data Domain.
  min count: 1
  path: belongsTo
  severity: Violation
property:
  max count: 1
  message: A Sub-Domain should name its owner.
  path: hasSubDomainOwner
  severity: Warning
target class: DataSubDomain

PR Data MetaModel SHACL Shapes
type: Ontology
conforms to: https://ontology.pernod-ricard.com/metamodel/2.0