9 Commits

Author SHA1 Message Date
Bastien Gourdon d90d2a316d t tag -a v2.0 -m "T-Box v2.0"
git push origin main
git push origin v2.0:Merge branch 'migration/tbox-v2.0'
2026-08-05 14:32:49 +02:00
Bastien Gourdon 908826601a tbox: v2.0 - 47 renommages de vocabulaire, alignement T-Box/A-Box sur les attributs transverses, GovernanceLayerObject inseree au-dessus d'Actor, isAbstract declare sur les 44 classes, ActivationStatus et Environment fermes en litteraux, labels derives de l'IRI, conformsTo pose sur les shapes, requetes SPARQL embarquees reecrites. Validation SHACL sans violation bloquante, rejeu a zero modification. Viewer T-Box regenere mais encore porteur d'IRI en dur, cf backlog 2026-08-05 14:32:49 +02:00
Bastien Gourdon 1719ec1934 governance: rulebook v1.2 - TN-022 restreint le shortLabel aux cas ou il differe du label, annexe Display exhaustive sur 44 classes ; retrait de la copie dupliquee des shapes 2026-08-05 09:29:23 +02:00
Bastien Gourdon ea21b7b300 shapes: retrait de la copie dupliquee, shapes/ est le seul emplacement de reference 2026-08-05 09:16:27 +02:00
Bastien Gourdon fbc63e55d8 tbox: v1.6 ... 2026-08-05 09:14:34 +02:00
Bastien Gourdon 1c8ab77003 tbox: v1.6 - provenance portee par les classes concretes, isAbstract declare et non plus infere, ForeignKeyConstraint devient ForeignKey, les dix classes sans rdfs:comment completees 2026-08-05 09:13:12 +02:00
Bastien Gourdon f83b7d7491 tbox: v1.5 - PhysicalRelation devient DataStructure et point d'extension declare pour le non-tabulaire, hasSource devient sourcedFrom vers un System instancie, storedIn sert deux granularites et deprecie materializedAs, references devient qualifiedBy, primaryLocation sous-propriete de storedIn, hasShortLabel sur 34 classes. CuratedObject devient DefinedObject et ObservedObject devient CapturedObject jusque dans les IRI, curationMode devient authoringMode. Correction de fond : PhysicalLayerObject ne porte plus la provenance, chaque classe de la couche la declare - un System est defined, tout ce qui est en dessous est captured. instances: 5 systemes crees, 151 sourcedFrom, 15 BO nettoyes de hasSource, labels courts, versions alignees 2026-07-30 15:13:03 +02:00
Bastien Gourdon 76e95de471 tbox: v1.2 - physicalizedIn devient storedIn, hasSource ramene a un seul sens (systeme detenant la donnee faisant autorite) et interdit sur les Business Concepts. Table supprime, MaterializedView / constrainsRelation / approximateRowCount / hasVendor deprecies. shapes: controle que la source d'une metrique correspond a celle de ses elements, unite multiple sur Metric et unique sur DataElement, hasProductOwner en avertissement. instances: Data Product SODH avec contrat et deux interfaces, 6 proprietaires de domaine nommes, Sell Out Distribution devient Retail Distribution. generated: les deux viewers partagent desormais le meme moteur 2026-07-29 09:03:59 +02:00
Bastien Gourdon 7793def268 instances: structure proposee pour les domaines empruntes. 10 sous-domaines dont 3 nouveaux (Outlet & Points of Sale, Geography, Organisation Structure), 15 business objects chacun avec un sujet unique, 15 concepts. Les sous-domaines composes 'Customer & Outlet' et 'Marketing Entity & Fiscal Calendar' sont scindes, meme regle que pour les objets. 17 flags TO_ARBITRATE : DD-04, DD-10, DD-16 et DD-21 doivent ratifier. scripts: chaine rendue pleinement idempotente, identifiants definitifs emis des la creation au lieu d'etre renumerotes ensuite 2026-07-27 22:26:56 +02:00
45 changed files with 15356 additions and 3625 deletions
+591
View File
@@ -0,0 +1,591 @@
# Pernod Ricard Data MetaModel — T-Box Processbook
**Version 0.1** — Draft for review — generated 2026-08-03
> Generated from `processes.yaml`. The BPMN files in `bpmn/` are the authoritative form; the views below are derived from the same source. Do not edit this document (EV-007, EV-008).
Procedures for changing the vocabulary of the model. Each procedure names the rules it enforces, the steps a person must perform, and the points at which the work stops rather than continues.
## Procedures
| Id | Family | Procedure | Scope |
|---|---|---|---|
| **X1** | Release | Propagate a change and publish a version | Any validated modification of the vocabulary. |
| **C1** | Create | Create a term | class | relation | attribute | annotation |
| **C2** | Create | Create a controlled value | typed individual | literal |
| **C3** | Create | Create a shape | Any SHACL constraint added to the validation set. |
| **D1** | Delete | Deprecate a term | class | relation | attribute | annotation | individual |
| **D2** | Delete | Withdraw a term permanently | class | relation | attribute | annotation | individual |
| **R1** | Verify | Verify the vocabulary against the rulebook | The whole vocabulary, or the subset touched by a change. |
| **R2** | Verify | Validate instances against the shapes | Any instance graph in the validation perimeter. |
| **R3** | Verify | Audit what the shapes cannot see | The whole vocabulary and its source file. |
## Reading a diagram
- A **rounded** node is a start or end state.
- A **diamond** is a decision; every outgoing path is labelled.
- A **shaded** box is a human task: it cannot be automated, and the work stops there until someone acts.
- A **double-bordered** box calls another procedure by its identifier.
---
# X1 — Propagate a change and publish a version
| | |
|---|---|
| Family | Release |
| Scope | Any validated modification of the vocabulary. |
| Trigger | A change to the vocabulary has been agreed and is ready to apply. |
| Inputs | the agreed change, the target version, the list of affected artifacts |
| Outputs | a merged branch, a tagged version, an execution log |
| Rules enforced | EV-004, EV-005, EV-006, EV-007, EV-008, EV-009, EV-010, EV-014, EV-015, TN-023 |
| Calls | R1, R2 |
**Note.** The most frequently invoked procedure of the processbook: every other procedure ends by calling it. Two of its steps are gateways rather than checks, because an idempotence failure and a validation failure must stop the work rather than be noted in passing.
```mermaid
flowchart TD
start(["Change agreed"])
branch["Create the migration branch"]
apply["Apply the change through an RDF parser"]
labels["Regenerate labels by derivation"]
abox["Propagate to the instances"]
shapes["Update the shapes and align conformsTo"]
artifacts["Regenerate every derived artifact"]
replay["Replay the chain a second time"]
idem{"Second run reports zero change?"}
verify_voc[["R1: Verify the vocabulary"]]
verify_abox[["R2: Validate the instances"]]
log["Write the execution log for this attempt"]
clean{"Any violation?"}
decide["Correct or abandon"]
drop["Destroy the branch and restart from the published version"]
review["Review the merge"]
merge["Merge as a block"]
bump["Increment the ontology version"]
end(["Version published"])
aborted(["Change abandoned"])
start --> branch
branch --> apply
apply --> labels
labels --> abox
abox --> shapes
shapes --> artifacts
artifacts --> replay
replay --> idem
idem -->|zero change| verify_voc
idem -->|the chain is not replayable| decide
verify_voc --> verify_abox
verify_abox --> log
log --> clean
clean -->|none| review
clean -->|at least one| decide
decide -->|correct| apply
decide -->|abandon| drop
drop --> aborted
review --> merge
merge --> bump
bump --> end
class idem decision;
class clean decision;
class decide human;
class review human;
classDef human fill:#FFF4CE,stroke:#B08900;
classDef decision fill:#E8F0F7,stroke:#3E6E96;
```
| Step | Type | Rules |
|---|---|---|
| Create the migration branch | script | EV-009 |
| Apply the change through an RDF parser | script | EV-005, EV-006 |
| Regenerate labels by derivation | script | TN-023 |
| Propagate to the instances | script | EV-009 |
| Update the shapes and align conformsTo | script | EV-010 |
| Regenerate every derived artifact | script | EV-008 |
| Replay the chain a second time | script | EV-006, EV-007 |
| Second run reports zero change? | decision | - |
| Verify the vocabulary (R1) | calls | - |
| Validate the instances (R2) | calls | - |
| Write the execution log for this attempt | script | EV-015 |
| Any violation? | decision | - |
| Correct or abandon | human | EV-004, EV-007 |
| Destroy the branch and restart from the published version | script | EV-007 |
| Review the merge | human | EV-004, EV-009 |
| Merge as a block | script | EV-009 |
| Increment the ontology version | script | EV-014 |
---
# C1 — Create a term
| | |
|---|---|
| Family | Create |
| Scope | class | relation | attribute | annotation |
| Trigger | A missing concept, edge or field has been identified. |
| Inputs | the intended meaning, the nature, the target layer, the provenance, the parent or the domain and range |
| Outputs | a declared term, a published version |
| Rules enforced | EV-004, TN-001, TN-002, TN-003, TN-004, TN-005, TN-006, TN-007, TN-009, TN-010, TN-011, TN-012, TN-013, TN-014, TN-015, TN-018, TN-019, TN-020, TN-021, TN-022, TN-023, TN-024, TN-025, TN-026, TN-027, TN-028 |
| Calls | R1, X1 |
**Parameter.** The nature of the term selects the naming rules applied at the naming step: a class takes TN-001, TN-002, TN-004, TN-005, TN-006, TN-007 and TN-027; a relation takes TN-001, TN-002, TN-009, TN-010, TN-011 and TN-028; an attribute takes TN-001, TN-002, TN-004, TN-012, TN-013, TN-014 and TN-015; an annotation takes TN-001, TN-002 and TN-003.
**Note.** The first step has no tool today. Checking that no existing term already covers the need is the semantic uniqueness control that SHACL cannot perform, and it stays a human task until R3 exists.
```mermaid
flowchart TD
start(["Need identified"])
unique["Check that no existing term covers the need"]
exists{"A term already covers it?"}
reuse(["Reuse the existing term"])
name["Name the term according to its nature"]
label["Derive the label"]
short["Decide the short label and the acronym"]
comment["Write the comment"]
declare["Declare typing"]
validate["Validate before applying"]
check[["R1: Verify against the rulebook"]]
release[["X1: Propagate and publish"]]
end(["Term available"])
start --> unique
unique --> exists
exists -->|yes| reuse
exists -->|no| name
name --> label
label --> short
short --> comment
comment --> declare
declare --> validate
validate --> check
check --> release
release --> end
class unique human;
class exists decision;
class short human;
class comment human;
class validate human;
classDef human fill:#FFF4CE,stroke:#B08900;
classDef decision fill:#E8F0F7,stroke:#3E6E96;
```
| Step | Type | Rules |
|---|---|---|
| Check that no existing term covers the need | human | - |
| A term already covers it? | decision | - |
| Name the term according to its nature | script | TN-001, TN-002, TN-003, TN-004, TN-005, TN-006, TN-009, TN-010, TN-011, TN-012, TN-013, TN-014, TN-015, TN-028 |
| Derive the label | script | TN-018, TN-019, TN-020, TN-021, TN-023 |
| Decide the short label and the acronym | human | TN-022 |
| Write the comment | human | TN-024 |
| Declare typing | script | TN-007, TN-025, TN-026, TN-027 |
| Validate before applying | human | EV-004 |
| Verify against the rulebook (R1) | calls | - |
| Propagate and publish (X1) | calls | - |
---
# C2 — Create a controlled value
| | |
|---|---|
| Family | Create |
| Scope | typed individual | literal |
| Trigger | A new value is needed in a controlled set. |
| Inputs | the value, the set it belongs to, whether a domain may add others |
| Outputs | a declared value, a published version |
| Rules enforced | EV-004, TN-001, TN-016, TN-017, TN-018, TN-022 |
| Calls | C3, R1, X1 |
**Parameter.** The form is not a style choice but the outcome of the first decision. The two branches have different consequences: the literal branch edits the shapes and therefore the vocabulary, so it falls under the version freeze; the individual branch touches nothing else.
```mermaid
flowchart TD
start(["New value needed"])
decide["Must it be defined, owned or extended by a domain?"]
form{"Which form?"}
indiv["Create the typed individual and derive its label"]
literal["Write the literal in upper snake case"]
shape[["C3: Extend the closed list in the shapes"]]
validate["Validate before applying"]
check[["R1: Verify against the rulebook"]]
release[["X1: Propagate and publish"]]
end(["Value available"])
start --> decide
decide --> form
form -->|extensible by a domain| indiv
form -->|closed governance state| literal
indiv --> validate
literal --> shape
shape --> validate
validate --> check
check --> release
release --> end
class decide human;
class form decision;
class validate human;
classDef human fill:#FFF4CE,stroke:#B08900;
classDef decision fill:#E8F0F7,stroke:#3E6E96;
```
| Step | Type | Rules |
|---|---|---|
| Must it be defined, owned or extended by a domain? | human | TN-016 |
| Which form? | decision | - |
| Create the typed individual and derive its label | script | TN-001, TN-018, TN-022 |
| Write the literal in upper snake case | script | TN-017 |
| Extend the closed list in the shapes (C3) | calls | - |
| Validate before applying | human | EV-004 |
| Verify against the rulebook (R1) | calls | - |
| Propagate and publish (X1) | calls | - |
---
# C3 — Create a shape
| | |
|---|---|
| Family | Create |
| Scope | Any SHACL constraint added to the validation set. |
| Trigger | A rule needs an executor, or a new class enters the validation perimeter. |
| Inputs | the rule to enforce, the target class or property |
| Outputs | a shape, a rulebook entry naming it as executor |
| Rules enforced | EV-010, EV-011, EV-012, EV-013 |
| Calls | R2, X1 |
**Note.** The step that is forgotten is the last one before release: recording the shape as the executor of its rule. Without it a rule stays declared blocking with nothing enforcing it, which EV-011 forbids.
```mermaid
flowchart TD
start(["Constraint needed"])
write["Write the constraint"]
pattern["Check that every pattern is expressed positively"]
portable{"Expressible within the specification?"}
demote["Move the rule to the script tier"]
conforms["Align the declared ontology version"]
test[["R2: Test against real instances"]]
record["Record the shape as the executor of its rule"]
release[["X1: Propagate and publish"]]
end(["Shape in force"])
start --> write
write --> pattern
pattern --> portable
portable -->|yes| conforms
portable -->|no| demote
demote --> record
conforms --> test
test --> record
record --> release
release --> end
class write human;
class portable decision;
class demote human;
class record human;
classDef human fill:#FFF4CE,stroke:#B08900;
classDef decision fill:#E8F0F7,stroke:#3E6E96;
```
| Step | Type | Rules |
|---|---|---|
| Write the constraint | human | - |
| Check that every pattern is expressed positively | script | EV-013 |
| Expressible within the specification? | decision | - |
| Move the rule to the script tier | human | EV-012 |
| Align the declared ontology version | script | EV-010 |
| Test against real instances (R2) | calls | - |
| Record the shape as the executor of its rule | human | EV-011, EV-012 |
| Propagate and publish (X1) | calls | - |
---
# D1 — Deprecate a term
| | |
|---|---|
| Family | Delete |
| Scope | class | relation | attribute | annotation | individual |
| Trigger | A term is superseded or no longer needed. |
| Inputs | the term, its replacement if any |
| Outputs | a deprecated stub, a published version |
| Rules enforced | EV-001, EV-015, TN-020 |
| Calls | X1 |
**Note.** The count at step two is informative, not a condition: a term may be deprecated whether or not it is instantiated. It becomes a condition only in D2.
```mermaid
flowchart TD
start(["Term superseded"])
replacement["Identify the replacement"]
count["Count the instances for information"]
mark["Mark deprecated and declare the replacement"]
strip["Strip the stub of every edge"]
label["Remove any mention of state from the label"]
release[["X1: Propagate and publish"]]
end(["Term deprecated"])
start --> replacement
replacement --> count
count --> mark
mark --> strip
strip --> label
label --> release
release --> end
class replacement human;
classDef human fill:#FFF4CE,stroke:#B08900;
classDef decision fill:#E8F0F7,stroke:#3E6E96;
```
| Step | Type | Rules |
|---|---|---|
| Identify the replacement | human | - |
| Count the instances for information | script | EV-015 |
| Mark deprecated and declare the replacement | script | EV-001 |
| Strip the stub of every edge | script | EV-001 |
| Remove any mention of state from the label | script | TN-020 |
| Propagate and publish (X1) | calls | - |
---
# D2 — Withdraw a term permanently
| | |
|---|---|
| Family | Delete |
| Scope | class | relation | attribute | annotation | individual |
| Trigger | A deprecated term is to be removed from the vocabulary. |
| Inputs | the deprecated term |
| Outputs | a vocabulary without the term, a proof of non-instantiation, a published version |
| Rules enforced | EV-002, EV-003, EV-015 |
| Calls | R1, R2, X1 |
**Note.** The reference cleaning step is what prevents phantom nodes: a subject removed while its identifier is still cited elsewhere is reconstructed by inference, present in traversals and absent from every control. It is a step of the procedure, not a check at the end of a script.
```mermaid
flowchart TD
start(["Withdrawal requested"])
count["Run the counting query"]
instantiated{"Any instance found?"}
keep(["Kept deprecated"])
proof["Attach the proof to the commit"]
remove["Remove the subject block"]
refs["Remove every reference naming the subject"]
check_voc[["R1: Verify the vocabulary"]]
check_abox[["R2: Validate the instances"]]
release[["X1: Propagate and publish"]]
end(["Term withdrawn"])
start --> count
count --> instantiated
instantiated -->|yes| keep
instantiated -->|no| proof
proof --> remove
remove --> refs
refs --> check_voc
check_voc --> check_abox
check_abox --> release
release --> end
class instantiated decision;
classDef human fill:#FFF4CE,stroke:#B08900;
classDef decision fill:#E8F0F7,stroke:#3E6E96;
```
| Step | Type | Rules |
|---|---|---|
| Run the counting query | script | EV-002, EV-015 |
| Any instance found? | decision | - |
| Attach the proof to the commit | script | EV-002, EV-015 |
| Remove the subject block | script | - |
| Remove every reference naming the subject | script | EV-003 |
| Verify the vocabulary (R1) | calls | - |
| Validate the instances (R2) | calls | - |
| Propagate and publish (X1) | calls | - |
---
# R1 — Verify the vocabulary against the rulebook
| | |
|---|---|
| Family | Verify |
| Scope | The whole vocabulary, or the subset touched by a change. |
| Trigger | A term has been created, changed or withdrawn, or a release is prepared. |
| Inputs | the vocabulary, the rulebook source |
| Outputs | a conformance report |
| Rules enforced | EV-014, TN-001, TN-002, TN-003, TN-004, TN-005, TN-006, TN-007, TN-008, TN-009, TN-010, TN-011, TN-012, TN-013, TN-014, TN-015, TN-016, TN-017, TN-018, TN-019, TN-020, TN-021, TN-022, TN-023, TN-024, TN-025, TN-026, TN-027, TN-028 |
| Calls | - |
**Note.** Two tiers run in sequence, not in parallel: the script settles everything mechanisable, and a person answers only for the rules no script can judge. Reversing the order wastes review time on findings the script would have caught.
```mermaid
flowchart TD
start(["Verification requested"])
script["Run the naming and declaration checks"]
mechanised{"Any mechanised violation?"}
report_fail(["Report returned with violations"])
human["Review the rules no script can judge"]
judged{"Reviewer raises an issue?"}
report_ok(["Vocabulary conforms"])
start --> script
script --> mechanised
mechanised -->|at least one| report_fail
mechanised -->|none| human
human --> judged
judged -->|yes| report_fail
judged -->|no| report_ok
class mechanised decision;
class human human;
class judged decision;
classDef human fill:#FFF4CE,stroke:#B08900;
classDef decision fill:#E8F0F7,stroke:#3E6E96;
```
| Step | Type | Rules |
|---|---|---|
| Run the naming and declaration checks | script | TN-001, TN-002, TN-003, TN-004, TN-005, TN-006, TN-007, TN-009, TN-010, TN-011, TN-012, TN-013, TN-014, TN-015, TN-017, TN-018, TN-019, TN-020, TN-021, TN-023, TN-025, TN-026, TN-027, TN-028, EV-014 |
| Any mechanised violation? | decision | - |
| Review the rules no script can judge | human | TN-008, TN-016, TN-022, TN-024 |
| Reviewer raises an issue? | decision | - |
---
# R2 — Validate instances against the shapes
| | |
|---|---|
| Family | Verify |
| Scope | Any instance graph in the validation perimeter. |
| Trigger | Instances have changed, shapes have changed, or a release is prepared. |
| Inputs | the instance graph, the shapes, the ontology |
| Outputs | a validation report |
| Rules enforced | EV-010 |
| Calls | - |
**Note.** The version check comes first and aborts rather than warns. Validating against shapes that target another version of the ontology does not fail loudly: it returns a long list of violations that reads exactly like a regression of the model.
```mermaid
flowchart TD
start(["Validation requested"])
conforms["Compare the declared target version with the ontology version"]
match{"Versions match?"}
abort(["Aborted on version mismatch"])
run["Run the shape validation"]
violations{"Any violation?"}
fail(["Report returned with violations"])
ok(["Instances conform"])
start --> conforms
conforms --> match
match -->|no| abort
match -->|yes| run
run --> violations
violations -->|at least one| fail
violations -->|none| ok
class match decision;
class violations decision;
classDef human fill:#FFF4CE,stroke:#B08900;
classDef decision fill:#E8F0F7,stroke:#3E6E96;
```
| Step | Type | Rules |
|---|---|---|
| Compare the declared target version with the ontology version | script | EV-010 |
| Versions match? | decision | - |
| Run the shape validation | script | - |
| Any violation? | decision | - |
---
# R3 — Audit what the shapes cannot see
| | |
|---|---|
| Family | Verify |
| Scope | The whole vocabulary and its source file. |
| Trigger | Periodic audit, or before a version is published. |
| Inputs | the vocabulary, its source file |
| Outputs | a shortlist of suspected duplicates, a list of duplicated blocks |
| Rules enforced | EV-012 |
| Calls | - |
**Note.** Shape validation reads a graph, not a file and not meaning. Two identifiers standing for the same notion produce two individually valid graphs; a duplicated block of text produces identical triples and no complaint. This procedure is the tier those rules fall to, and its output is a shortlist for a person rather than a verdict.
```mermaid
flowchart TD
start(["Audit requested"])
index["Build the normalised label index"]
hash["Hash every normalised subject block"]
shortlist{"Any candidate found?"}
clean(["Nothing to arbitrate"])
review["Arbitrate each candidate"]
act{"Duplication confirmed?"}
merge(["Referred to the merge procedure"])
dismissed(["Candidates dismissed"])
start --> index
index --> hash
hash --> shortlist
shortlist -->|none| clean
shortlist -->|at least one| review
review --> act
act -->|yes| merge
act -->|no| dismissed
class shortlist decision;
class review human;
class act decision;
classDef human fill:#FFF4CE,stroke:#B08900;
classDef decision fill:#E8F0F7,stroke:#3E6E96;
```
| Step | Type | Rules |
|---|---|---|
| Build the normalised label index | script | EV-012 |
| Hash every normalised subject block | script | EV-012 |
| Any candidate found? | decision | - |
| Arbitrate each candidate | human | - |
| Duplication confirmed? | decision | - |
---
# Rule coverage
Which procedures enforce each rule. Derived, not maintained: a rule cited in a diagram appears here automatically.
| Rule | Procedures |
|---|---|
| EV-001 | D1 |
| EV-002 | D2 |
| EV-003 | D2 |
| EV-004 | C1, C2, X1 |
| EV-005 | X1 |
| EV-006 | X1 |
| EV-007 | X1 |
| EV-008 | X1 |
| EV-009 | X1 |
| EV-010 | C3, R2, X1 |
| EV-011 | C3 |
| EV-012 | C3, R3 |
| EV-013 | C3 |
| EV-014 | R1, X1 |
| EV-015 | D1, D2, X1 |
| TN-001 | C1, C2, R1 |
| TN-002 | C1, R1 |
| TN-003 | C1, R1 |
| TN-004 | C1, R1 |
| TN-005 | C1, R1 |
| TN-006 | C1, R1 |
| TN-007 | C1, R1 |
| TN-008 | R1 |
| TN-009 | C1, R1 |
| TN-010 | C1, R1 |
| TN-011 | C1, R1 |
| TN-012 | C1, R1 |
| TN-013 | C1, R1 |
| TN-014 | C1, R1 |
| TN-015 | C1, R1 |
| TN-016 | C2, R1 |
| TN-017 | C2, R1 |
| TN-018 | C1, C2, R1 |
| TN-019 | C1, R1 |
| TN-020 | C1, D1, R1 |
| TN-021 | C1, R1 |
| TN-022 | C1, C2, R1 |
| TN-023 | C1, R1, X1 |
| TN-024 | C1, R1 |
| TN-025 | C1, R1 |
| TN-026 | C1, R1 |
| TN-027 | C1, R1 |
| TN-028 | C1, R1 |
+970
View File
@@ -0,0 +1,970 @@
# Pernod Ricard Data MetaModel — T-Box Rulebook
**Version 1.2** — Draft for review — generated 2026-08-03
> Generated from `rules.yaml`. Do not edit this document: edit the source and regenerate (EV-007, EV-008).
## Scope
Governs the vocabulary of the model itself: the IRIs, labels and declarative axioms of every class, property and enumeration individual. Does not govern instances, which remain under the A-Box rulebook.
Written to be read and applied directly, by a person or by a language model, without further context. Every rule states what must hold, why, and what enforces it.
## Categories
| Category | Subject | Rules |
|---|---|---|
| **Identifier** | Form of the IRI | 17 |
| **Label** | Derivation and display | 6 |
| **Declaration** | Declarative completeness | 5 |
| **Evolution** | Change and propagation | 15 |
## Severity
- **BLOCKING** — The commit does not pass. Enforced pre-commit and in continuous integration by the named executor.
- **MAJOR** — Reviewed and answered for, but not machine-enforceable. Reserved for rules that require human judgement, per EV-011.
- **GUIDELINE** — Recommended practice with no gate.
---
# Identifier — Form of the IRI
The IRI is identity. It is immutable in the RDF sense — changing one is a migration, never an edit — so it must be unambiguous, searchable and free of local convention.
## TN-001 — CamelCase IRI with no separator
**Statement.** The local name of an IRI is strict CamelCase with no separator of any kind: no hyphen, no underscore, no dot, no space. Initial capital for a class or an enumeration individual; initial lower case for a property, whether object, datatype or annotation. Consecutive capitals are admitted only where they carry an acronym reproduced from a target class short label under TN-011, and nowhere else.
| | |
|---|---|
| Severity | **BLOCKING** |
| Applies to | class, object_property, datatype_property, annotation_property, individual |
| Enforced at | script |
| Executor | `check_tbox_naming.py` |
| Procedure | _pending_ |
**Why.** A hyphen forces escaping in certain Turtle and SPARQL contexts and makes decamelisation ambiguous: Sub-Domain has no single correct expansion. Typographic convention is a display concern and belongs to the short label, never to identity.
| From | To | Note |
|---|---|---|
| pr:data_domain | pr:DataDomain | no underscore |
| pr:Sub-Domain | pr:DataSubDomain | no hyphen |
| pr:BelongsTo | pr:belongsTo | a property starts lower case |
## TN-002 — No acronym in the IRI
**Statement.** The local name of an IRI contains no acronym, initialism or abbreviation. Every short form is expanded in full. One exception, and one only: a relation reproducing the short label of the class it targets carries that short label as it stands, acronym included, under TN-011. No other term may carry an acronym, and no exception list is maintained.
| | |
|---|---|
| Severity | **BLOCKING** |
| Applies to | class, object_property, datatype_property, annotation_property, individual |
| Enforced at | script |
| Executor | `check_tbox_naming.py` |
| Procedure | _pending_ |
| Related | A-Box NR-004 |
**Why.** An acronym in an identifier is a local convention pretending to be a name. It is unsearchable by anyone who does not already know it, and it collides across domains: PO is a Product Owner in one team and a Purchase Order in another. Length is not a counter-argument — an IRI is written by tooling and read in context, and the short form remains available through the short label and the acronym annotation. The single exception exists because a relation name is read on every edge of the graph and in every query, where the spoken form is what makes it legible; reproducing the target's short label verbatim also keeps the relation name derivable from the class rather than invented, which a truncated form would not.
| From | To | Note |
|---|---|---|
| pr:KPI | pr:KeyPerformanceIndicator | short form moves to pr:acronym |
| pr:BIDataSource | pr:BusinessIntelligenceDataSource | |
| pr:definitionUri | pr:definitionAddress | URI is itself an acronym |
| a relation targeting a class whose short label is BI Field | pr:containsBIField | the one exception — see TN-011 |
## TN-003 — Display properties are annotations
**Statement.** pr:acronym and pr:shortLabel are declared owl:AnnotationProperty, never owl:DatatypeProperty.
| | |
|---|---|
| Severity | **BLOCKING** |
| Applies to | annotation_property |
| Enforced at | script |
| Executor | `check_tbox_naming.py` |
| Procedure | _pending_ |
**Why.** A datatype property applies to an individual. Annotating a term of the vocabulary with one takes the ontology out of OWL DL into OWL Full, which reasoners reject and strict validators flag. An annotation property applies to anything — class, property or individual. Nothing is lost on the control side: SHACL validates triples and does not read OWL declarations, so every shape targeting these properties keeps working unchanged.
| From | To | Note |
|---|---|---|
| pr:shortLabel a owl:DatatypeProperty | pr:shortLabel a owl:AnnotationProperty | |
| pr:acronym a owl:DatatypeProperty | pr:acronym a owl:AnnotationProperty | |
## TN-004 — No digit in the IRI
**Statement.** The local name of a vocabulary term contains no digit. A numeric parameter — a window, a threshold, a version — is a value, never an identity.
| | |
|---|---|
| Severity | **BLOCKING** |
| Applies to | class, object_property, datatype_property, annotation_property |
| Enforced at | script |
| Executor | `check_tbox_naming.py` |
| Procedure | _pending_ |
| Related | A-Box NR-014 (deliberate opposite) |
**Why.** An instance identifier does encode numbers, and that is its purpose: it carries a position in the domain tree. A vocabulary term encodes nothing but its own meaning. The opposition is deliberate and is stated in the ontology header so that no reader takes one for a violation of the other. A term named after a thirty-day window starts lying the day the window becomes ninety, and nothing detects it.
| From | To | Note |
|---|---|---|
| pr:queryCount30d | pr:queryCount | the window belongs to the harvesting specification |
## TN-005 — A class is a singular common noun
**Statement.** The local name of a class is a singular noun phrase. No plural, no verb. No type word such as Object, Entity or Item unless it names a genuine abstraction of the model. No suffix rule is imposed on abstract classes.
| | |
|---|---|
| Severity | **BLOCKING** |
| Applies to | class |
| Enforced at | script, human |
| Executor | `check_tbox_naming.py` |
| Procedure | _pending_ |
| Related | A-Box NR-008, NR-013 |
**Why.** Abstractness is not inferable from a name and must not be encoded in one. The correspondence fails in both directions: classes can be abstract without any suffix, and classes ending in Object can be among the most heavily instantiated in the model. Renaming a central business term to satisfy a naming rule would trade meaning for symmetry. Abstractness is declared instead, by TN-007.
| From | To | Note |
|---|---|---|
| pr:DataDomains | pr:DataDomain | singular |
| pr:ActorObject | pr:Actor | no type word added for its own sake |
## TN-006 — Every layer root is abstract
**Statement.** A class that serves as the root of a layer carries pr:isAbstract true.
| | |
|---|---|
| Severity | **BLOCKING** |
| Applies to | class |
| Enforced at | script |
| Executor | `check_tbox_naming.py` |
| Procedure | _pending_ |
**Why.** A non-abstract layer root allows an instance to be typed as an object of that layer without saying which kind of object it is. That statement carries no information and no downstream control can repair it.
| From | To | Note |
|---|---|---|
| pr:PhysicalLayerObject with no declaration | pr:isAbstract true | |
## TN-007 — Every class declares whether it is abstract
**Statement.** Every owl:Class carries pr:isAbstract with an explicit boolean value. Absence of the property is a violation, not a default.
| | |
|---|---|
| Severity | **BLOCKING** |
| Applies to | class |
| Enforced at | script |
| Executor | `check_tbox_naming.py` |
| Procedure | _pending_ |
**Why.** Where declaration is optional, nothing distinguishes a class deliberately left concrete from one where the question was never asked. Two situations make the compulsion necessary. A class with subclasses may be perfectly instantiable, and has been wrongly marked abstract by inference. And a class used as a classification axis may have no declared subclass at all, its members being typed by multiple typing on instances — its abstractness is then structurally unverifiable and only the explicit declaration carries it.
| From | To | Note |
|---|---|---|
| a class with no declaration | pr:isAbstract true or false | silence is a violation |
| pr:Metric inferred abstract because it has a subclass | pr:isAbstract false | concrete despite having a subclass |
## TN-008 — Abstractness is declared, never inferred
**Statement.** Abstractness is read from pr:isAbstract and from nothing else. No consumer of the model — viewer, export, script or documentation generator — may derive it from the presence of subclasses, from the absence of instances, or from any other structural signal.
| | |
|---|---|
| Severity | **BLOCKING** |
| Applies to | class, consumer |
| Enforced at | human |
| Executor | `consumer contract review` |
| Procedure | _pending_ |
**Why.** A declaration rule holds in the source only if it also binds every consumer. Otherwise each one re-invents its own inference and the model acquires as many answers as it has readers. This is the same failure mode that justified a governed short label rather than a display dictionary per viewer.
| From | To | Note |
|---|---|---|
| isAbstract = term.subclasses.length > 0 | isAbstract = term.isAbstract === true | consumer contract |
## TN-009 — A relation begins with a verb
**Statement.** The local name of an object property begins with a verb, in lower case. Both the active form and the passive or participial form are admitted: a past participle is a verb in first position. No relation begins with a preposition or with a noun.
| | |
|---|---|
| Severity | **BLOCKING** |
| Applies to | object_property |
| Enforced at | script |
| Executor | `check_tbox_naming.py` |
| Procedure | _pending_ |
**Why.** A relation reads as a verb and an attribute reads as a noun. The distinction is what lets a reader tell an edge from a field without opening the declaration. The explicit clause on participial forms is required: read literally, a verb-first rule would condemn a whole family of sound relations such as computedBy, storedIn and derivedFrom.
| From | To | Note |
|---|---|---|
| pr:inDatabase | pr:isInDatabase | a preposition is not a verb |
| pr:primaryLocation | pr:primarilyStoredIn | a noun is not a verb |
| pr:computedBy | pr:computedBy | participial form is conforming |
## TN-010 — The is prefix is either a copula or a predicate
**Statement.** On an owl:ObjectProperty, is acts as a COPULA — a state verb before a preposition or adjective, whose target is a node of the graph. On a boolean datatype or annotation property, is acts as a PREDICATE — it introduces an adjective or participle and the value is true or false. No other use is admitted.
| | |
|---|---|
| Severity | **BLOCKING** |
| Applies to | object_property, datatype_property, annotation_property |
| Enforced at | script |
| Executor | `check_tbox_naming.py` |
| Procedure | _pending_ |
**Why.** Conformance of a name beginning with is is never checkable on the name alone. A control asserting that every such term is a boolean produces false positives on every copula; the reverse control lets real problems through. The correct check reads the declared type of the property first, then applies the matching clause.
| From | To | Note |
|---|---|---|
| pr:isInSchema | copula | object property |
| pr:isNullable | predicate | datatype property of range xsd:boolean |
## TN-011 — A relation names its target by its short label
**Statement.** A relation need not mention its target class at all. When it does, it uses the target class's pr:shortLabel AS IT STANDS — not the full label, not a truncation of the short label, and not a form invented for the occasion. Where the short label carries an acronym, the acronym is reproduced with it: this is the single exception admitted by TN-002, and TN-001 admits the consecutive capitals it produces.
| | |
|---|---|
| Severity | **BLOCKING** |
| Applies to | object_property |
| Enforced at | script |
| Executor | `check_tbox_naming.py` |
| Procedure | _pending_ |
**Why.** Relation names are what is read on the edges of the graph, constantly, in every viewer and every query. Full labels make them unreadable on an arrow. The short label is the form the organisation actually speaks, and reusing it makes a relation name predictable from the class it points at instead of arbitrary. Reproducing it whole is what preserves that property: a truncation would be a form the model invented, unpredictable from the class and unverifiable against it. A corollary: relations do not normally carry a short label of their own, since this rule already keeps their IRIs short.
| From | To | Note |
|---|---|---|
| pr:hasDomainOwner | pr:hasDomainOwner | target DataDomainOwner, short label Domain Owner — conforming |
| pr:hasDGL | pr:hasGovernanceLead | target short label is Governance Lead |
| pr:inBIDataSource | pr:isInBIDataSource | short label BI Data Source reproduced whole, acronym included |
| pr:containsField | pr:containsBIField | a truncated short label is not admitted |
## TN-012 — An attribute is a noun
**Statement.** The local name of a datatype property is a noun phrase. It begins with no verb, and in particular with no has.
| | |
|---|---|
| Severity | **BLOCKING** |
| Applies to | datatype_property |
| Enforced at | script |
| Executor | `check_tbox_naming.py` |
| Procedure | _pending_ |
**Why.** A has prefix carries no information — rdfs:domain already says who has the attribute — and it costs the reader the distinction TN-009 exists to make visible. Under TN-018 the derived label would read as a verb phrase on a field, so a relation and an attribute would become indistinguishable in every display.
| From | To | Note |
|---|---|---|
| pr:hasFormula | pr:formula | |
| pr:hasName | pr:canonicalName | name alone is too generic to stand as an identity |
## TN-013 — A date attribute is a noun, not a participle
**Statement.** An attribute holding a date or timestamp is named as a noun.
| | |
|---|---|
| Severity | **BLOCKING** |
| Applies to | datatype_property |
| Enforced at | script |
| Executor | `check_tbox_naming.py` |
| Procedure | _pending_ |
**Why.** The participial form is idiomatic English but creates a standing family of exceptions to TN-012 that must be defended at every review. The nominal form is consistent with the other attributes of the model and needs no defence.
| From | To | Note |
|---|---|---|
| pr:createdOn | pr:creationDate | |
| pr:lastQueriedOn | pr:lastQueryDate | |
## TN-014 — An attribute never takes a relational suffix
**Statement.** A datatype property never takes a form ending in By, In, From or On. Those forms are reserved for object properties. The only exception is the predicative is of booleans, covered by TN-015.
| | |
|---|---|
| Severity | **BLOCKING** |
| Applies to | datatype_property |
| Enforced at | script |
| Executor | `check_tbox_naming.py` |
| Procedure | _pending_ |
**Why.** An attribute named like a relation will be read as one. A reader encountering a term in the shape of computedBy or ownedBy expects a node at the other end, not a string. The tell is usually visible in the label: a parenthetical gloss added to explain what the value actually holds is evidence that the name itself is wrong.
| From | To | Note |
|---|---|---|
| pr:definedBy holding a string | pr:definitionAddress | address, not location — location is taken by the storage side |
## TN-015 — A boolean attribute begins with is
**Statement.** A property of range xsd:boolean begins with is, followed by an adjective or participle. This is the single explicit exception to TN-012.
| | |
|---|---|
| Severity | **BLOCKING** |
| Applies to | datatype_property, annotation_property |
| Enforced at | script |
| Executor | `check_tbox_naming.py` |
| Procedure | _pending_ |
**Why.** A bare adjective does not read as a statement, and some of them collide with reserved words in the languages that consume the model.
| From | To | Note |
|---|---|---|
| pr:nullable | pr:isNullable | |
| pr:abstract | pr:isAbstract | abstract is a reserved word in several target languages |
## TN-016 — A controlled value is an individual or a literal
**Statement.** A value becomes a TYPED INDIVIDUAL if it needs to be defined, owned, or extended by a domain without modifying the model. It stays a LITERAL if it is a closed technical state whose list is the property of governance and must precisely not be extended. The model chooses one form per kind of value and states the choice.
| | |
|---|---|
| Severity | **BLOCKING** |
| Applies to | class, individual, datatype_property |
| Enforced at | shacl, human |
| Executor | `pr_metamodel_shapes.ttl` |
| Procedure | _pending_ |
| Related | A-Box NR-016 |
**Why.** A literal cannot carry a definition, an owner or a link, and extending its list means editing the shapes and therefore the frozen vocabulary. A typed individual can be added by a domain without touching the model, appears as a node in every viewer, and can be reached by following an edge. The test is not how the value looks but who is allowed to add one.
| From | To | Note |
|---|---|---|
| an activation level defined in a playbook, not in the graph | literal constrained by sh:in | not domain-extensible |
| a kind of system a domain may need to add | typed individual | extensible and worth defining |
## TN-017 — Controlled literals are UPPER_SNAKE_CASE
**Statement.** Controlled literal values are written UPPER_SNAKE_CASE. This is the only place in the model where that casing is used.
| | |
|---|---|
| Severity | **BLOCKING** |
| Applies to | datatype_property |
| Enforced at | shacl |
| Executor | `pr_metamodel_shapes.ttl` |
| Procedure | _pending_ |
**Why.** The casing exists so that a value drawn from a closed list is distinguishable from free text at a glance, in the source and in any export.
| From | To | Note |
|---|---|---|
| "Full activation" | "FULL_ACTIVATION" | |
| "Published" | "PUBLISHED" | |
---
# Label — Derivation and display
The label is derived, not authored. Anything a reader needs that the derivation cannot produce lives in a dedicated channel: the short label for the spoken form, the comment for meaning.
## TN-018 — The label is derived from the IRI
**Statement.** rdfs:label is obtained from the local name of the IRI by inserting a space at every case boundary. No word added, removed, reordered or substituted. The words of a class label keep their initial capital; the words of a property label are entirely lower case.
| | |
|---|---|
| Severity | **BLOCKING** |
| Applies to | class, object_property, datatype_property, annotation_property, individual |
| Enforced at | script |
| Executor | `check_tbox_naming.py` |
| Procedure | _pending_ |
| Related | A-Box NR-005 (deliberate opposite) |
**Why.** A label carrying meaning the IRI does not is the symptom of a badly named IRI or of an incomplete comment — never of a legitimate exception to the label. Where a label has been quietly enriched to explain a term, the explanation belongs in rdfs:comment and the short form, if the organisation speaks one, in pr:shortLabel.
| From | To | Note |
|---|---|---|
| pr:BusinessIntelligenceDataSource | Business Intelligence Data Source | a class label keeps its capitals |
| pr:primarilyStoredIn | primarily stored in | a property label is lower case |
| "Ownership and Categorization Layer Object" | "Ownership Layer Object" | the added meaning moves to rdfs:comment |
## TN-019 — No acronym in the label
**Statement.** The label introduces no acronym or abbreviation that the IRI does not already carry, including in a recapitalised form that the derivation would not produce. Where the IRI legitimately carries an acronym under TN-011, the derived label carries it too and nothing further is added.
| | |
|---|---|
| Severity | **BLOCKING** |
| Applies to | class, object_property, datatype_property, annotation_property, individual |
| Enforced at | script |
| Executor | `check_tbox_naming.py` |
| Procedure | _pending_ |
**Why.** A direct corollary of TN-018, kept as a separate rule because it catches labels whose IRI is innocent: a term correctly named in full can still carry a hand-written label that reintroduces the short form. Stated as an introduction rather than a prohibition, it stays a pure corollary — whatever the IRI legitimately holds, the derivation reproduces, and nothing else.
| From | To | Note |
|---|---|---|
| "BI expression" | "expression" | the IRI was already conforming |
## TN-020 — No state in the label
**Statement.** The label mentions no status, version or deprecation. State lives in an axiom — owl:deprecated, pr:status — never in text. Deprecated and active terms are never mixed in a single view: a consumer filters on the axiom at query time and presents deprecated terms separately.
| | |
|---|---|
| Severity | **BLOCKING** |
| Applies to | class, object_property, datatype_property, annotation_property, individual, consumer |
| Enforced at | script |
| Executor | `check_tbox_naming.py` |
| Procedure | _pending_ |
**Why.** A term whose label repeats its state carries that state twice. The day one is changed without the other, the two contradict each other and nothing detects it. With the rule in force, the question of whether a term is deprecated has exactly one possible answer.
| From | To | Note |
|---|---|---|
| "belongs to domain (deprecated)" | "belongs to domain" | owl:deprecated carries the state |
## TN-021 — No gloss or parenthesis in the label
**Statement.** No parenthesis, qualification or example appears in the label. They belong in rdfs:comment.
| | |
|---|---|
| Severity | **BLOCKING** |
| Applies to | class, object_property, datatype_property, annotation_property |
| Enforced at | script |
| Executor | `check_tbox_naming.py` |
| Procedure | _pending_ |
**Why.** A parenthetical gloss is almost always the symptom of an IRI that does not say what it holds. The right response is usually to rename the term, not to annotate the label.
| From | To | Note |
|---|---|---|
| "defined by (code URI)" | "definition address" | rename the term rather than gloss the label |
| "query count (30 days)" | "query count" | |
## TN-022 — The short label carries the spoken form
**Statement.** A term carries pr:shortLabel ONLY where the form derived under TN-018 is not what the organisation writes or says. Where the two coincide, no short label is declared: a duplicate of the label carries no information and is one more thing to keep in step. The short label follows the casing of the label it replaces — title case for a class — and admits what the label cannot: acronyms, abbreviations, dropped words.
| | |
|---|---|
| Severity | **MAJOR** |
| Applies to | class, individual |
| Enforced at | human |
| Executor | `review checklist` |
| Procedure | _pending_ |
**Why.** The short label is what makes the derivation rule bearable. Without it, a mechanical label impoverishes every screen; with it, display becomes explicit and centrally governed instead of being improvised by each consumer. Restricting it to the cases where it actually differs is what keeps it meaningful: a short label present on every term degenerates into a second copy of the label, drifting from it one entry at a time. It is MAJOR rather than BLOCKING because judging whether a derived form matches what people actually say is human work, and EV-011 forbids declaring a rule blocking with no executor.
| From | To | Note |
|---|---|---|
| "Data Sub Domain Owner" | short label Sub Domain Owner | the word Data is dropped in the spoken form |
| "Key Performance Indicator" | short label KPI, acronym KPI | the acronym channel is what makes TN-002 bearable |
| a short label reading Metric on a class labelled Metric | no short label | a duplicate of the label carries nothing |
## TN-023 — The label is generated, never typed
**Statement.** rdfs:label is never written by hand. It is produced by the derivation function from the IRI and stored in the source. A hand-typed label is a violation even when its value happens to be correct.
| | |
|---|---|
| Severity | **BLOCKING** |
| Applies to | class, object_property, datatype_property, annotation_property, individual |
| Enforced at | script |
| Executor | `check_tbox_naming.py` |
| Procedure | _pending_ |
**Why.** Generate and store, rather than store only or compute at read time. The label stays in the source so SPARQL and third-party tooling behave normally, while a script regenerates it at every commit and the gate fails if a stored label differs from the derived one. Redundancy becomes harmless because it is checked: rather than hoping two facts stay consistent, the model makes inconsistency detectable.
| From | To | Note |
|---|---|---|
| label absent | violation | |
| label differing from the derivation | violation — drift | |
---
# Declaration — Declarative completeness
Nothing is left to be guessed. What a term is, what it applies to, where it sits and where it came from are all stated, never inferred from structure or from silence.
## TN-024 — Every active term carries a comment
**Statement.** rdfs:comment is mandatory on every active term. On a property the comment is structured: the definition, plus the scope constraint or the confusion to be avoided. On a class the form is free.
| | |
|---|---|
| Severity | **BLOCKING** |
| Applies to | class, object_property, datatype_property, annotation_property |
| Enforced at | script, human |
| Executor | `check_tbox_naming.py` |
| Procedure | _pending_ |
**Why.** A comment that paraphrases the name prevents nothing. A good one prevents a specific error: it says where the property may and may not be declared, or which neighbouring term it must not be confused with. The structure is imposed on properties because they are where ambiguity costs most — they are asserted thousands of times by people who will not read the model. The script checks presence; only a reviewer checks value, which is why the rule names two tiers.
| From | To | Note |
|---|---|---|
| "the owner" | definition plus where it may be declared and what it excludes | |
## TN-025 — Every property declares its domain and range
**Statement.** rdfs:domain and rdfs:range are mandatory on every property, EXCEPT where the property is deliberately polymorphic. A polymorphic property states so in its comment and has its scope declared in SHACL. Silent absence is a violation; documented absence is not.
| | |
|---|---|
| Severity | **BLOCKING** |
| Applies to | object_property, datatype_property |
| Enforced at | script, human |
| Executor | `check_tbox_naming.py` |
| Procedure | _pending_ |
**Why.** A property with no domain cannot be targeted by any control. But some properties are polymorphic by design, their scope controlled class by class in SHACL rather than by twin properties; giving those an rdfs:domain would trigger the RDFS retyping described in TN-028. The rule therefore separates the two cases rather than demanding a domain everywhere.
| From | To | Note |
|---|---|---|
| a polymorphic property with no domain and no comment | violation | silence is indistinguishable from omission |
| a polymorphic property with no domain | conforming | scope declared in SHACL |
## TN-026 — Every term declares how it was authored
**Statement.** pr:authoringMode is mandatory on every term. pr:harvestSource is mandatory if and only if the mode is HARVESTED.
| | |
|---|---|
| Severity | **BLOCKING** |
| Applies to | class, object_property, datatype_property, annotation_property |
| Enforced at | script |
| Executor | `check_tbox_naming.py` |
| Procedure | _pending_ |
**Why.** Provenance decides who may edit a term and what a divergence means. A term stating where its data comes from without stating that it is harvested, or declaring itself harvested without naming a source, is half-declared in a way no control can catch. Declared symmetrically, provenance also makes a harvester specifiable from the model itself rather than from a side document.
| From | To | Note |
|---|---|---|
| harvestSource present | authoringMode HARVESTED | |
| authoringMode HARVESTED, harvestSource absent | harvestSource declared | |
## TN-027 — A concrete class has one layer and one provenance
**Statement.** Every concrete class descends from exactly one layer root and from exactly one provenance axis. Controlled-vocabulary classes are outside the layers by nature — a named and closed exception.
| | |
|---|---|
| Severity | **BLOCKING** |
| Applies to | class |
| Enforced at | script |
| Executor | `check_tbox_naming.py` |
| Procedure | _pending_ |
**Why.** A class with no layer is invisible to every layer-scoped control and to every view organised by layer; a class with two is ambiguous in both. Where a family of classes genuinely sits outside the data layers — actors, governance objects — the answer is an explicit root of its own, not silence. Adding such a root by INSERTION above an existing abstraction, rather than by renaming it, keeps the ranges that point at that abstraction readable: a range must state the nature of its target, not its position in the model.
| From | To | Note |
|---|---|---|
| a class with no parent | attached to its layer root | |
| renaming an abstraction into a layer root | inserting the layer root above it | preserves the meaning of every range that points at it |
## TN-028 — A sub-property inherits the domain of its parent
**Statement.** Declaring rdfs:subPropertyOf does not shield a property from the rdfs:domain of its parent. Every assertion of a sub-property is also an assertion of the parent, so the parent's domain applies and its RDFS retyping takes effect.
| | |
|---|---|
| Severity | **BLOCKING** |
| Applies to | object_property, datatype_property |
| Enforced at | script, human |
| Executor | `check_tbox_naming.py` |
| Procedure | _pending_ |
**Why.** In RDFS a domain is not a rejecting constraint but a retyping inference. Attaching a sub-property to a parent whose domain does not fit will silently retype its subjects — an object of one layer quietly becomes an object of another, entering counts, controls and traversals it was never part of. Where two relations share a verb but not a domain, they must not be merged and must not be linked by subPropertyOf; the unification, if one is needed, belongs on a shared upper property with no domain of its own.
| From | To | Note |
|---|---|---|
| a dashboard field asserted through a property whose domain is a warehouse column | the dashboard field is inferred to BE a warehouse column | silent retyping |
| linking the two by rdfs:subPropertyOf | no protection — the parent's domain still applies | |
| a shared upper property with no domain | end-to-end traversal without merging IRIs | the correct unification |
---
# Evolution — Change and propagation
Every rule here is anchored on a real failure. They govern how the model changes without breaking what depends on it.
## EV-001 — A rename is never an edit in place
**Statement.** A rename creates a new term and keeps the old one with owl:deprecated true and dcterms:isReplacedBy. No IRI is modified in place. The deprecated stub carries no edge: no subClassOf, no label, no comment — only its type and the two axioms.
| | |
|---|---|
| Severity | **BLOCKING** |
| Applies to | procedure |
| Enforced at | human |
| Executor | `commit review` |
| Procedure | _pending_ |
| Related | A-Box LC-002 |
**Why.** Nothing propagates in RDF. An IRI is a string copied into every triple, so renaming means rewriting every triple where it appears as subject, predicate or object — declaration, subclass axioms, domain, range, instance typing, shape targets and paths, queries, viewers, documentation. Edges are moved, not duplicated. The stub exists so that a consumer holding an older export can still be told what replaced the term.
| From | To | Note |
|---|---|---|
| editing an IRI in place | new term plus deprecated stub | |
## EV-002 — A withdrawal requires proof of non-instantiation
**Statement.** A term is removed only after a count proving it was never instantiated, the count being attached to the commit. Without proof, deprecation only.
| | |
|---|---|
| Severity | **BLOCKING** |
| Applies to | procedure |
| Enforced at | script |
| Executor | `count query attached to the commit` |
| Procedure | _pending_ |
**Why.** Removal is the one irreversible operation on a vocabulary. The proof is cheap — a single counting query — and it is what separates a safe withdrawal from a silent data loss.
| From | To | Note |
|---|---|---|
| deleting a term | SELECT (COUNT(?s) AS ?n) WHERE { ?s a pr:Term } | must return zero |
## EV-003 — A deletion removes every reference to the subject
**Statement.** A subject removed while its IRI is still cited elsewhere is recreated by RDFS inference. Every reference is removed in the same operation.
| | |
|---|---|
| Severity | **BLOCKING** |
| Applies to | procedure |
| Enforced at | script |
| Executor | `dangling-reference check in the migration script` |
| Procedure | _pending_ |
**Why.** A graph has no foreign keys. Deleting the block that declares a subject leaves every triple that names it intact, and a reasoner will reconstruct a hollow node from them — present in traversals, absent from every control.
| From | To | Note |
|---|---|---|
| deleting a subject block | deleting the block and every triple naming it | |
## EV-004 — A change is validated before it is applied
**Statement.** Every modification of the model is agreed before any artifact is written. Dry run first, apply second.
| | |
|---|---|
| Severity | **BLOCKING** |
| Applies to | procedure |
| Enforced at | human |
| Executor | `commit review` |
| Procedure | _pending_ |
**Why.** The damage from a bad change is rarely in the change itself but in everything that silently depended on the old state. A dry run that reports what it would touch is what makes that dependency visible while it can still be discussed.
| From | To | Note |
|---|---|---|
| migrate.py --apply as the first run | migrate.py, then --apply | dry run is the default |
## EV-005 — Graph edits go through an RDF parser
**Statement.** Any transformation of the graph is performed with an RDF parser. No regular expression applied line by line to Turtle.
| | |
|---|---|
| Severity | **BLOCKING** |
| Applies to | procedure, script |
| Enforced at | human |
| Executor | `script review` |
| Procedure | _pending_ |
**Why.** Turtle is a structured syntax and a line is not a unit of meaning. A literal may contain the very characters a regex splits on, so a text-level edit that looks correct on every example can truncate a value on the one that matters. A parser knows the difference between a delimiter and a character inside a string; a regex does not, and every workaround for that is a partial reimplementation of the parser.
| From | To | Note |
|---|---|---|
| re.sub on the file text | remove and add on parsed triples | |
| hand-written clause splitting | parser | a workaround is evidence the wrong tool is in use |
## EV-006 — Every script is idempotent
**Statement.** A script may be replayed without changing the result. Its guard tests the TARGET state, not the source state, and it counts its own result. Acceptance test: two consecutive runs, the second reporting zero modifications.
| | |
|---|---|
| Severity | **BLOCKING** |
| Applies to | script |
| Enforced at | script |
| Executor | `double run in continuous integration` |
| Procedure | _pending_ |
**Why.** A chain of transformations is only safe if any step can be replayed. A guard that tests whether work remains to be done breaks as soon as another script has already changed the source; a guard that tests whether the target state exists survives it. The self-count is what turns the property into a verifiable one.
| From | To | Note |
|---|---|---|
| appending unconditionally | appending only if the target is absent | guard on the target state |
## EV-007 — The replayable source is the start of the chain
**Statement.** The replayable source is the starting file plus the ordered sequence of scripts, never an intermediate committed state. Hand-editing a file produced by a script is forbidden, including for a typo: the script is corrected and the chain replayed.
| | |
|---|---|
| Severity | **BLOCKING** |
| Applies to | procedure |
| Enforced at | human |
| Executor | `commit review` |
| Procedure | _pending_ |
**Why.** A committed intermediate file is a convenience, not a source. As soon as one hand edit exists in it that no script performs, replaying the chain no longer reproduces the committed state and nobody can say which of the two is right. Keeping the chain authoritative is also what makes a failed migration recoverable: restart from the last true source with a corrected script, rather than repair a half-migrated file by hand.
| From | To | Note |
|---|---|---|
| fixing a typo in a generated file | fixing the script and replaying | |
## EV-008 — Every artifact has a producer script
**Statement.** No artifact contains hard-coded data. Every deliverable — viewer, documentation, workbook, diagram — is regenerated from a versioned source by a script.
| | |
|---|---|
| Severity | **BLOCKING** |
| Applies to | script, artifact |
| Enforced at | human |
| Executor | `artifact review` |
| Procedure | _pending_ |
**Why.** An artifact that cannot be regenerated becomes stale the moment the model moves, and there is no way to tell whether it is stale or merely different. This rulebook applies the rule to itself: the source is one file, the document and the workbook are generated from it.
| From | To | Note |
|---|---|---|
| a hand-maintained workbook | a source file plus a generator | |
## EV-009 — A change propagates in a single merge
**Statement.** A change is propagated to the vocabulary, the instances, the shapes, the consumers and the documentation in one branch, merged as a block. The main line never sees a state where one has moved and another has not.
| | |
|---|---|
| Severity | **BLOCKING** |
| Applies to | procedure |
| Enforced at | human |
| Executor | `merge review` |
| Procedure | _pending_ |
**Why.** Partial propagation is not a smaller version of a change, it is a different and invalid state. Its worst form is silent: a gate that validates the stale half and reports success. A single merge makes the intermediate state unreachable rather than merely discouraged.
| From | To | Note |
|---|---|---|
| committing the vocabulary and updating the shapes later | one branch | |
## EV-010 — Shapes declare the ontology version they target
**Statement.** The shapes graph declares dcterms:conformsTo equal to the owl:versionIRI of the ontology. Any divergence aborts validation immediately.
| | |
|---|---|
| Severity | **BLOCKING** |
| Applies to | shape, script |
| Enforced at | script |
| Executor | `run_shacl_validation.py` |
| Procedure | _pending_ |
**Why.** Validating against the wrong shapes does not fail loudly. It produces a long list of violations that reads exactly like a regression of the model, and the time goes into interpreting them rather than noticing the mismatch. One declared triple and one check turn that into a single line naming the real problem.
| From | To | Note |
|---|---|---|
| hundreds of violations to interpret | ABORT — shapes target a different ontology version | |
## EV-011 — No blocking rule without an executor
**Statement.** A rule may be declared BLOCKING only if a named script or shape enforces it. A blocking rule with no control is a statement of intent.
| | |
|---|---|
| Severity | **BLOCKING** |
| Applies to | rule |
| Enforced at | script |
| Executor | `rulebook source self-check` |
| Procedure | _pending_ |
**Why.** An unenforced blocking rule is worse than an absent one: it is cited as though it held, and the gap only surfaces when something it should have caught reaches production. Making the executor a mandatory field turns the omission into something a script can detect on the rulebook itself.
| From | To | Note |
|---|---|---|
| severity BLOCKING with no executor | violation | detected by the generators before they write anything |
## EV-012 — Every rule names the tier that enforces it
**Statement.** Controls exist at three tiers — SHACL for the shape of the graph, scripts for the file, the lexicon and naming, human review for semantics. Every rule names its tier. A rule with no assigned tier is a violation.
| | |
|---|---|
| Severity | **BLOCKING** |
| Applies to | rule |
| Enforced at | script |
| Executor | `rulebook source self-check` |
| Procedure | _pending_ |
**Why.** SHACL validates a graph, not a file, and not meaning. Two IRIs standing for the same notion produce two individually valid graphs; a duplicated block of text produces identical triples and no complaint. Naming the tier forces the question of what actually catches each rule, and pushes the ones SHACL cannot see to a control that can: a normalised-label index reviewed by a person, a file-level hash, a naming script, a review checklist.
| From | To | Note |
|---|---|---|
| two IRIs for one notion | normalised label index plus human review | SHACL cannot see it |
| a duplicated text block | file-level block hashing | identical triples |
## EV-013 — No sh:pattern outside the SHACL specification
**Statement.** sh:pattern uses XPath 2.0 regular expressions, which support neither lookbehind nor lookahead. A constraint is expressed positively — what the string must be — rather than negatively. Where that is impossible the rule leaves SHACL and moves to the script tier under EV-012.
| | |
|---|---|
| Severity | **BLOCKING** |
| Applies to | shape |
| Enforced at | script |
| Executor | `run_shacl_validation.py` |
| Procedure | _pending_ |
| Related | A-Box NR-001 |
**Why.** A pattern written in a richer regex dialect does not degrade gracefully: the validator fails to compile it, and the rule that depended on it silently stops being enforced while still being declared blocking.
| From | To | Note |
|---|---|---|
| a pattern using (?<! ... ) | a pattern describing the whole accepted string | express positively |
## EV-014 — The term namespace is never versioned
**Statement.** The namespace of terms is not versioned. The version lives in owl:versionIRI alone.
| | |
|---|---|
| Severity | **BLOCKING** |
| Applies to | ontology |
| Enforced at | script |
| Executor | `check_tbox_naming.py` |
| Procedure | _pending_ |
**Why.** A versioned term namespace changes every IRI at every release, breaking every consumer for no benefit. It also fails silently in the other direction: a file bound to an outdated namespace targets IRIs that no longer exist, so every shape matches nothing and validation passes while testing nothing at all.
| From | To | Note |
|---|---|---|
| a prefix bound to a versioned namespace | a prefix bound to the stable namespace | the version lives in owl:versionIRI |
## EV-015 — Every procedure that writes produces an execution log
**Statement.** Any procedure that modifies the graph writes a versioned execution log, committed alongside the change. The log records the attempt number and timestamp, a checksum of every input consumed, the ontology version and the version the shapes declare they target, a count per step, the full validation report, and a difference against the previous attempt.
| | |
|---|---|
| Severity | **BLOCKING** |
| Applies to | procedure, script |
| Enforced at | script |
| Executor | `the procedure's own runner` |
| Procedure | _pending_ |
**Why.** A counter printed to the terminal disappears with the terminal. When a validation run fails, the question is never only what failed but whether it is the same failure as the previous attempt: a violation that persists unchanged after a correction says the cause lies elsewhere, while one that moves every time says the change is being patched rather than fixed. Those are opposite diagnoses and neither is available without a record of the previous run. The input checksums serve the same purpose one level down, separating a wrong model from a stale file — the ambiguity that made a past incident expensive to explain. The log is also the natural carrier of the proof of non-instantiation required by EV-002, which otherwise has no defined format.
| From | To | Note |
|---|---|---|
| a counter printed to standard output | a versioned log committed with the change | |
| a failed run with no record of the previous one | a difference against the previous attempt | what makes correct-or-abandon decidable |
---
# Annex A — Abstractness
Applies TN-006 and TN-007 to the model. 44 classes, of which **13 abstract** and **31 concrete**. Maintained with the model, not after it.
| Class | isAbstract | Note |
|---|---|---|
| `MetaModelObject` | **true** | root of the model |
| `DefinedObject` | **true** | provenance axis |
| `CapturedObject` | **true** | provenance axis |
| `OwnershipLayerObject` | **true** | layer root |
| `BusinessLayerObject` | **true** | layer root |
| `LogicalLayerObject` | **true** | layer root |
| `PhysicalLayerObject` | **true** | NEW — omission in v1.6 |
| `DeliveryLayerObject` | **true** | layer root |
| `ConsumptionLayerObject` | **true** | layer root |
| `GovernanceLayerObject` | **true** | NEW — seventh layer root |
| `DataStructure` | **true** | harvesting types the concrete sort |
| `KeyConstraint` | **true** | only PrimaryKey and ForeignKey instantiate |
| `Actor` | **true** | renaming would degrade the range of ownedBy and hasPublisher |
| `DataDomain` | false | |
| `DataSubDomain` | false | |
| `BusinessObject` | false | ends in Object and is heavily instantiated - refutes any suffix rule |
| `BusinessConcept` | false | |
| `Metric` | false | concrete despite having KeyPerformanceIndicator beneath it |
| `KeyPerformanceIndicator` | false | |
| `DataObject` | false | |
| `DataElement` | false | |
| `Database` | false | |
| `Schema` | false | |
| `Field` | false | |
| `System` | false | |
| `Transformation` | false | |
| `BaseTable` | false | |
| `ExternalTable` | false | |
| `View` | false | |
| `PrimaryKey` | false | |
| `ForeignKey` | false | |
| `DataProduct` | false | |
| `DataContract` | false | |
| `DataInterface` | false | |
| `BusinessIntelligenceWorkspace` | false | |
| `BusinessIntelligenceDataSource` | false | |
| `BusinessIntelligenceField` | false | |
| `BusinessIntelligenceReport` | false | |
| `DataDomainOwner` | false | |
| `DataSubDomainOwner` | false | |
| `DataProductOwner` | false | |
| `DataSteward` | false | |
| `DataGovernanceLead` | false | |
| `SystemType` | false | must stay concrete — it types five individuals |
---
# Annex B — Display
Applies TN-011 and TN-022. The short label is what screens display and what relation names reuse; the acronym is a search key, never an identity.
| IRI | rdfs:label | shortLabel | acronym | Note |
|---|---|---|---|---|
| `MetaModelObject` | Meta Model Object | - | - | |
| `DefinedObject` | Defined Object | - | - | |
| `CapturedObject` | Captured Object | - | - | |
| `OwnershipLayerObject` | Ownership Layer Object | Ownership Layer | - | the categorisation role moves into rdfs:comment |
| `BusinessLayerObject` | Business Layer Object | Business Layer | - | |
| `LogicalLayerObject` | Logical Layer Object | Logical Layer | - | |
| `PhysicalLayerObject` | Physical Layer Object | Physical Layer | - | |
| `DeliveryLayerObject` | Delivery Layer Object | Delivery Layer | - | |
| `ConsumptionLayerObject` | Consumption Layer Object | Consumption Layer | - | |
| `GovernanceLayerObject` | Governance Layer Object | Governance Layer | - | |
| `DataStructure` | Data Structure | - | - | the former short label was a gloss and belongs in rdfs:comment |
| `KeyConstraint` | Key Constraint | - | - | |
| `Actor` | Actor | - | - | |
| `DataDomain` | Data Domain | Domain | DD | DD is already the short form used inside instance identifiers |
| `DataSubDomain` | Data Sub Domain | Sub Domain | SD | no hyphen, for consistency |
| `BusinessObject` | Business Object | - | BO | |
| `BusinessConcept` | Business Concept | Concept | - | drives the name of usesConcept |
| `Metric` | Metric | - | - | |
| `KeyPerformanceIndicator` | Key Performance Indicator | KPI | KPI | the acronym channel is what makes TN-002 bearable |
| `DataObject` | Data Object | - | - | |
| `DataElement` | Data Element | Element | - | drives the name of hasElement and hasGrainElement |
| `Database` | Database | - | - | |
| `Schema` | Schema | - | - | |
| `Field` | Field | - | - | |
| `System` | System | - | - | |
| `SystemType` | System Type | - | - | |
| `Transformation` | Transformation | - | - | |
| `BaseTable` | Base Table | - | - | |
| `ExternalTable` | External Table | - | - | |
| `View` | View | - | - | |
| `PrimaryKey` | Primary Key | - | - | |
| `ForeignKey` | Foreign Key | - | - | |
| `DataProduct` | Data Product | - | - | |
| `DataContract` | Data Contract | - | - | |
| `DataInterface` | Data Interface | - | - | |
| `BusinessIntelligenceWorkspace` | Business Intelligence Workspace | BI Workspace | - | |
| `BusinessIntelligenceDataSource` | Business Intelligence Data Source | BI Data Source | - | |
| `BusinessIntelligenceField` | Business Intelligence Field | BI Field | - | reproduced whole in containsBIField under TN-011 |
| `BusinessIntelligenceReport` | Business Intelligence Report | BI Report | - | |
| `DataDomainOwner` | Data Domain Owner | Domain Owner | DDO | |
| `DataSubDomainOwner` | Data Sub Domain Owner | Sub Domain Owner | SDO | |
| `DataProductOwner` | Data Product Owner | Product Owner | PO | not DPO - the initialism is taken by Data Protection Officer, a real GDPR role |
| `DataSteward` | Data Steward | Steward | - | |
| `DataGovernanceLead` | Data Governance Lead | Governance Lead | DGL | |
Binary file not shown.
+209
View File
@@ -0,0 +1,209 @@
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:pr="https://ontology.pernod-ricard.com/process/" id="defs_C1" targetNamespace="https://ontology.pernod-ricard.com/process/" exporter="generate_bpmn.py">
<bpmn:process id="C1" name="Create a term" isExecutable="false">
<bpmn:documentation>Create a term | Scope: class | relation | attribute | annotation | Trigger: A missing concept, edge or field has been identified.</bpmn:documentation>
<bpmn:startEvent id="start" name="Need identified">
<bpmn:outgoing>flow_1</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:userTask id="unique" name="Check that no existing term covers the need">
<bpmn:incoming>flow_1</bpmn:incoming>
<bpmn:outgoing>flow_2</bpmn:outgoing>
</bpmn:userTask>
<bpmn:exclusiveGateway id="exists" name="A term already covers it?">
<bpmn:incoming>flow_2</bpmn:incoming>
<bpmn:outgoing>flow_3</bpmn:outgoing>
<bpmn:outgoing>flow_4</bpmn:outgoing>
</bpmn:exclusiveGateway>
<bpmn:endEvent id="reuse" name="Reuse the existing term">
<bpmn:incoming>flow_3</bpmn:incoming>
</bpmn:endEvent>
<bpmn:scriptTask id="name" name="Name the term according to its nature">
<bpmn:extensionElements>
<pr:rules>
<pr:rule>TN-001</pr:rule>
<pr:rule>TN-002</pr:rule>
<pr:rule>TN-003</pr:rule>
<pr:rule>TN-004</pr:rule>
<pr:rule>TN-005</pr:rule>
<pr:rule>TN-006</pr:rule>
<pr:rule>TN-009</pr:rule>
<pr:rule>TN-010</pr:rule>
<pr:rule>TN-011</pr:rule>
<pr:rule>TN-012</pr:rule>
<pr:rule>TN-013</pr:rule>
<pr:rule>TN-014</pr:rule>
<pr:rule>TN-015</pr:rule>
<pr:rule>TN-028</pr:rule>
</pr:rules>
</bpmn:extensionElements>
<bpmn:incoming>flow_4</bpmn:incoming>
<bpmn:outgoing>flow_5</bpmn:outgoing>
</bpmn:scriptTask>
<bpmn:scriptTask id="label" name="Derive the label">
<bpmn:extensionElements>
<pr:rules>
<pr:rule>TN-018</pr:rule>
<pr:rule>TN-019</pr:rule>
<pr:rule>TN-020</pr:rule>
<pr:rule>TN-021</pr:rule>
<pr:rule>TN-023</pr:rule>
</pr:rules>
</bpmn:extensionElements>
<bpmn:incoming>flow_5</bpmn:incoming>
<bpmn:outgoing>flow_6</bpmn:outgoing>
</bpmn:scriptTask>
<bpmn:userTask id="short" name="Decide the short label and the acronym">
<bpmn:extensionElements>
<pr:rules>
<pr:rule>TN-022</pr:rule>
</pr:rules>
</bpmn:extensionElements>
<bpmn:incoming>flow_6</bpmn:incoming>
<bpmn:outgoing>flow_7</bpmn:outgoing>
</bpmn:userTask>
<bpmn:userTask id="comment" name="Write the comment">
<bpmn:extensionElements>
<pr:rules>
<pr:rule>TN-024</pr:rule>
</pr:rules>
</bpmn:extensionElements>
<bpmn:incoming>flow_7</bpmn:incoming>
<bpmn:outgoing>flow_8</bpmn:outgoing>
</bpmn:userTask>
<bpmn:scriptTask id="declare" name="Declare typing">
<bpmn:extensionElements>
<pr:rules>
<pr:rule>TN-007</pr:rule>
<pr:rule>TN-025</pr:rule>
<pr:rule>TN-026</pr:rule>
<pr:rule>TN-027</pr:rule>
</pr:rules>
</bpmn:extensionElements>
<bpmn:incoming>flow_8</bpmn:incoming>
<bpmn:outgoing>flow_9</bpmn:outgoing>
</bpmn:scriptTask>
<bpmn:userTask id="validate" name="Validate before applying">
<bpmn:extensionElements>
<pr:rules>
<pr:rule>EV-004</pr:rule>
</pr:rules>
</bpmn:extensionElements>
<bpmn:incoming>flow_9</bpmn:incoming>
<bpmn:outgoing>flow_10</bpmn:outgoing>
</bpmn:userTask>
<bpmn:callActivity id="check" name="Verify against the rulebook" calledElement="R1">
<bpmn:incoming>flow_10</bpmn:incoming>
<bpmn:outgoing>flow_11</bpmn:outgoing>
</bpmn:callActivity>
<bpmn:callActivity id="release" name="Propagate and publish" calledElement="X1">
<bpmn:incoming>flow_11</bpmn:incoming>
<bpmn:outgoing>flow_12</bpmn:outgoing>
</bpmn:callActivity>
<bpmn:endEvent id="end" name="Term available">
<bpmn:incoming>flow_12</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="flow_1" sourceRef="start" targetRef="unique" />
<bpmn:sequenceFlow id="flow_2" sourceRef="unique" targetRef="exists" />
<bpmn:sequenceFlow id="flow_3" name="yes" sourceRef="exists" targetRef="reuse" />
<bpmn:sequenceFlow id="flow_4" name="no" sourceRef="exists" targetRef="name" />
<bpmn:sequenceFlow id="flow_5" sourceRef="name" targetRef="label" />
<bpmn:sequenceFlow id="flow_6" sourceRef="label" targetRef="short" />
<bpmn:sequenceFlow id="flow_7" sourceRef="short" targetRef="comment" />
<bpmn:sequenceFlow id="flow_8" sourceRef="comment" targetRef="declare" />
<bpmn:sequenceFlow id="flow_9" sourceRef="declare" targetRef="validate" />
<bpmn:sequenceFlow id="flow_10" sourceRef="validate" targetRef="check" />
<bpmn:sequenceFlow id="flow_11" sourceRef="check" targetRef="release" />
<bpmn:sequenceFlow id="flow_12" sourceRef="release" targetRef="end" />
</bpmn:process>
<bpmndi:BPMNDiagram id="diagram_C1">
<bpmndi:BPMNPlane id="plane_C1" bpmnElement="C1">
<bpmndi:BPMNShape id="shape_start" bpmnElement="start">
<dc:Bounds x="60" y="80" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="shape_unique" bpmnElement="unique">
<dc:Bounds x="260" y="80" width="150" height="70" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="shape_exists" bpmnElement="exists">
<dc:Bounds x="460" y="80" width="50" height="50" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="shape_reuse" bpmnElement="reuse">
<dc:Bounds x="660" y="80" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="shape_name" bpmnElement="name">
<dc:Bounds x="660" y="210" width="150" height="70" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="shape_label" bpmnElement="label">
<dc:Bounds x="860" y="80" width="150" height="70" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="shape_short" bpmnElement="short">
<dc:Bounds x="1060" y="80" width="150" height="70" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="shape_comment" bpmnElement="comment">
<dc:Bounds x="1260" y="80" width="150" height="70" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="shape_declare" bpmnElement="declare">
<dc:Bounds x="1460" y="80" width="150" height="70" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="shape_validate" bpmnElement="validate">
<dc:Bounds x="1660" y="80" width="150" height="70" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="shape_check" bpmnElement="check">
<dc:Bounds x="1860" y="80" width="150" height="70" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="shape_release" bpmnElement="release">
<dc:Bounds x="2060" y="80" width="150" height="70" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="shape_end" bpmnElement="end">
<dc:Bounds x="2260" y="80" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="edge_flow_1" bpmnElement="flow_1">
<di:waypoint x="96" y="98" />
<di:waypoint x="260" y="115" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="edge_flow_2" bpmnElement="flow_2">
<di:waypoint x="410" y="115" />
<di:waypoint x="460" y="105" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="edge_flow_3" bpmnElement="flow_3">
<di:waypoint x="510" y="105" />
<di:waypoint x="660" y="98" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="edge_flow_4" bpmnElement="flow_4">
<di:waypoint x="510" y="105" />
<di:waypoint x="660" y="245" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="edge_flow_5" bpmnElement="flow_5">
<di:waypoint x="810" y="245" />
<di:waypoint x="860" y="115" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="edge_flow_6" bpmnElement="flow_6">
<di:waypoint x="1010" y="115" />
<di:waypoint x="1060" y="115" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="edge_flow_7" bpmnElement="flow_7">
<di:waypoint x="1210" y="115" />
<di:waypoint x="1260" y="115" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="edge_flow_8" bpmnElement="flow_8">
<di:waypoint x="1410" y="115" />
<di:waypoint x="1460" y="115" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="edge_flow_9" bpmnElement="flow_9">
<di:waypoint x="1610" y="115" />
<di:waypoint x="1660" y="115" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="edge_flow_10" bpmnElement="flow_10">
<di:waypoint x="1810" y="115" />
<di:waypoint x="1860" y="115" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="edge_flow_11" bpmnElement="flow_11">
<di:waypoint x="2010" y="115" />
<di:waypoint x="2060" y="115" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="edge_flow_12" bpmnElement="flow_12">
<di:waypoint x="2210" y="115" />
<di:waypoint x="2260" y="98" />
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>
+152
View File
@@ -0,0 +1,152 @@
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:pr="https://ontology.pernod-ricard.com/process/" id="defs_C2" targetNamespace="https://ontology.pernod-ricard.com/process/" exporter="generate_bpmn.py">
<bpmn:process id="C2" name="Create a controlled value" isExecutable="false">
<bpmn:documentation>Create a controlled value | Scope: typed individual | literal | Trigger: A new value is needed in a controlled set.</bpmn:documentation>
<bpmn:startEvent id="start" name="New value needed">
<bpmn:outgoing>flow_1</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:userTask id="decide" name="Must it be defined, owned or extended by a domain?">
<bpmn:extensionElements>
<pr:rules>
<pr:rule>TN-016</pr:rule>
</pr:rules>
</bpmn:extensionElements>
<bpmn:incoming>flow_1</bpmn:incoming>
<bpmn:outgoing>flow_2</bpmn:outgoing>
</bpmn:userTask>
<bpmn:exclusiveGateway id="form" name="Which form?">
<bpmn:incoming>flow_2</bpmn:incoming>
<bpmn:outgoing>flow_3</bpmn:outgoing>
<bpmn:outgoing>flow_4</bpmn:outgoing>
</bpmn:exclusiveGateway>
<bpmn:scriptTask id="indiv" name="Create the typed individual and derive its label">
<bpmn:extensionElements>
<pr:rules>
<pr:rule>TN-001</pr:rule>
<pr:rule>TN-018</pr:rule>
<pr:rule>TN-022</pr:rule>
</pr:rules>
</bpmn:extensionElements>
<bpmn:incoming>flow_3</bpmn:incoming>
<bpmn:outgoing>flow_5</bpmn:outgoing>
</bpmn:scriptTask>
<bpmn:scriptTask id="literal" name="Write the literal in upper snake case">
<bpmn:extensionElements>
<pr:rules>
<pr:rule>TN-017</pr:rule>
</pr:rules>
</bpmn:extensionElements>
<bpmn:incoming>flow_4</bpmn:incoming>
<bpmn:outgoing>flow_6</bpmn:outgoing>
</bpmn:scriptTask>
<bpmn:callActivity id="shape" name="Extend the closed list in the shapes" calledElement="C3">
<bpmn:incoming>flow_6</bpmn:incoming>
<bpmn:outgoing>flow_7</bpmn:outgoing>
</bpmn:callActivity>
<bpmn:userTask id="validate" name="Validate before applying">
<bpmn:extensionElements>
<pr:rules>
<pr:rule>EV-004</pr:rule>
</pr:rules>
</bpmn:extensionElements>
<bpmn:incoming>flow_5</bpmn:incoming>
<bpmn:incoming>flow_7</bpmn:incoming>
<bpmn:outgoing>flow_8</bpmn:outgoing>
</bpmn:userTask>
<bpmn:callActivity id="check" name="Verify against the rulebook" calledElement="R1">
<bpmn:incoming>flow_8</bpmn:incoming>
<bpmn:outgoing>flow_9</bpmn:outgoing>
</bpmn:callActivity>
<bpmn:callActivity id="release" name="Propagate and publish" calledElement="X1">
<bpmn:incoming>flow_9</bpmn:incoming>
<bpmn:outgoing>flow_10</bpmn:outgoing>
</bpmn:callActivity>
<bpmn:endEvent id="end" name="Value available">
<bpmn:incoming>flow_10</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="flow_1" sourceRef="start" targetRef="decide" />
<bpmn:sequenceFlow id="flow_2" sourceRef="decide" targetRef="form" />
<bpmn:sequenceFlow id="flow_3" name="extensible by a domain" sourceRef="form" targetRef="indiv" />
<bpmn:sequenceFlow id="flow_4" name="closed governance state" sourceRef="form" targetRef="literal" />
<bpmn:sequenceFlow id="flow_5" sourceRef="indiv" targetRef="validate" />
<bpmn:sequenceFlow id="flow_6" sourceRef="literal" targetRef="shape" />
<bpmn:sequenceFlow id="flow_7" sourceRef="shape" targetRef="validate" />
<bpmn:sequenceFlow id="flow_8" sourceRef="validate" targetRef="check" />
<bpmn:sequenceFlow id="flow_9" sourceRef="check" targetRef="release" />
<bpmn:sequenceFlow id="flow_10" sourceRef="release" targetRef="end" />
</bpmn:process>
<bpmndi:BPMNDiagram id="diagram_C2">
<bpmndi:BPMNPlane id="plane_C2" bpmnElement="C2">
<bpmndi:BPMNShape id="shape_start" bpmnElement="start">
<dc:Bounds x="60" y="80" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="shape_decide" bpmnElement="decide">
<dc:Bounds x="260" y="80" width="150" height="70" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="shape_form" bpmnElement="form">
<dc:Bounds x="460" y="80" width="50" height="50" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="shape_indiv" bpmnElement="indiv">
<dc:Bounds x="660" y="80" width="150" height="70" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="shape_literal" bpmnElement="literal">
<dc:Bounds x="660" y="210" width="150" height="70" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="shape_shape" bpmnElement="shape">
<dc:Bounds x="860" y="80" width="150" height="70" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="shape_validate" bpmnElement="validate">
<dc:Bounds x="1060" y="80" width="150" height="70" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="shape_check" bpmnElement="check">
<dc:Bounds x="1260" y="80" width="150" height="70" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="shape_release" bpmnElement="release">
<dc:Bounds x="1460" y="80" width="150" height="70" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="shape_end" bpmnElement="end">
<dc:Bounds x="1660" y="80" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="edge_flow_1" bpmnElement="flow_1">
<di:waypoint x="96" y="98" />
<di:waypoint x="260" y="115" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="edge_flow_2" bpmnElement="flow_2">
<di:waypoint x="410" y="115" />
<di:waypoint x="460" y="105" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="edge_flow_3" bpmnElement="flow_3">
<di:waypoint x="510" y="105" />
<di:waypoint x="660" y="115" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="edge_flow_4" bpmnElement="flow_4">
<di:waypoint x="510" y="105" />
<di:waypoint x="660" y="245" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="edge_flow_5" bpmnElement="flow_5">
<di:waypoint x="810" y="115" />
<di:waypoint x="1060" y="115" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="edge_flow_6" bpmnElement="flow_6">
<di:waypoint x="810" y="245" />
<di:waypoint x="860" y="115" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="edge_flow_7" bpmnElement="flow_7">
<di:waypoint x="1010" y="115" />
<di:waypoint x="1060" y="115" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="edge_flow_8" bpmnElement="flow_8">
<di:waypoint x="1210" y="115" />
<di:waypoint x="1260" y="115" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="edge_flow_9" bpmnElement="flow_9">
<di:waypoint x="1410" y="115" />
<di:waypoint x="1460" y="115" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="edge_flow_10" bpmnElement="flow_10">
<di:waypoint x="1610" y="115" />
<di:waypoint x="1660" y="98" />
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>
+151
View File
@@ -0,0 +1,151 @@
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:pr="https://ontology.pernod-ricard.com/process/" id="defs_C3" targetNamespace="https://ontology.pernod-ricard.com/process/" exporter="generate_bpmn.py">
<bpmn:process id="C3" name="Create a shape" isExecutable="false">
<bpmn:documentation>Create a shape | Scope: Any SHACL constraint added to the validation set. | Trigger: A rule needs an executor, or a new class enters the validation perimeter.</bpmn:documentation>
<bpmn:startEvent id="start" name="Constraint needed">
<bpmn:outgoing>flow_1</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:userTask id="write" name="Write the constraint">
<bpmn:incoming>flow_1</bpmn:incoming>
<bpmn:outgoing>flow_2</bpmn:outgoing>
</bpmn:userTask>
<bpmn:scriptTask id="pattern" name="Check that every pattern is expressed positively">
<bpmn:extensionElements>
<pr:rules>
<pr:rule>EV-013</pr:rule>
</pr:rules>
</bpmn:extensionElements>
<bpmn:incoming>flow_2</bpmn:incoming>
<bpmn:outgoing>flow_3</bpmn:outgoing>
</bpmn:scriptTask>
<bpmn:exclusiveGateway id="portable" name="Expressible within the specification?">
<bpmn:incoming>flow_3</bpmn:incoming>
<bpmn:outgoing>flow_4</bpmn:outgoing>
<bpmn:outgoing>flow_5</bpmn:outgoing>
</bpmn:exclusiveGateway>
<bpmn:userTask id="demote" name="Move the rule to the script tier">
<bpmn:extensionElements>
<pr:rules>
<pr:rule>EV-012</pr:rule>
</pr:rules>
</bpmn:extensionElements>
<bpmn:incoming>flow_5</bpmn:incoming>
<bpmn:outgoing>flow_6</bpmn:outgoing>
</bpmn:userTask>
<bpmn:scriptTask id="conforms" name="Align the declared ontology version">
<bpmn:extensionElements>
<pr:rules>
<pr:rule>EV-010</pr:rule>
</pr:rules>
</bpmn:extensionElements>
<bpmn:incoming>flow_4</bpmn:incoming>
<bpmn:outgoing>flow_7</bpmn:outgoing>
</bpmn:scriptTask>
<bpmn:callActivity id="test" name="Test against real instances" calledElement="R2">
<bpmn:incoming>flow_7</bpmn:incoming>
<bpmn:outgoing>flow_8</bpmn:outgoing>
</bpmn:callActivity>
<bpmn:userTask id="record" name="Record the shape as the executor of its rule">
<bpmn:extensionElements>
<pr:rules>
<pr:rule>EV-011</pr:rule>
<pr:rule>EV-012</pr:rule>
</pr:rules>
</bpmn:extensionElements>
<bpmn:incoming>flow_6</bpmn:incoming>
<bpmn:incoming>flow_8</bpmn:incoming>
<bpmn:outgoing>flow_9</bpmn:outgoing>
</bpmn:userTask>
<bpmn:callActivity id="release" name="Propagate and publish" calledElement="X1">
<bpmn:incoming>flow_9</bpmn:incoming>
<bpmn:outgoing>flow_10</bpmn:outgoing>
</bpmn:callActivity>
<bpmn:endEvent id="end" name="Shape in force">
<bpmn:incoming>flow_10</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="flow_1" sourceRef="start" targetRef="write" />
<bpmn:sequenceFlow id="flow_2" sourceRef="write" targetRef="pattern" />
<bpmn:sequenceFlow id="flow_3" sourceRef="pattern" targetRef="portable" />
<bpmn:sequenceFlow id="flow_4" name="yes" sourceRef="portable" targetRef="conforms" />
<bpmn:sequenceFlow id="flow_5" name="no" sourceRef="portable" targetRef="demote" />
<bpmn:sequenceFlow id="flow_6" sourceRef="demote" targetRef="record" />
<bpmn:sequenceFlow id="flow_7" sourceRef="conforms" targetRef="test" />
<bpmn:sequenceFlow id="flow_8" sourceRef="test" targetRef="record" />
<bpmn:sequenceFlow id="flow_9" sourceRef="record" targetRef="release" />
<bpmn:sequenceFlow id="flow_10" sourceRef="release" targetRef="end" />
</bpmn:process>
<bpmndi:BPMNDiagram id="diagram_C3">
<bpmndi:BPMNPlane id="plane_C3" bpmnElement="C3">
<bpmndi:BPMNShape id="shape_start" bpmnElement="start">
<dc:Bounds x="60" y="80" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="shape_write" bpmnElement="write">
<dc:Bounds x="260" y="80" width="150" height="70" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="shape_pattern" bpmnElement="pattern">
<dc:Bounds x="460" y="80" width="150" height="70" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="shape_portable" bpmnElement="portable">
<dc:Bounds x="660" y="80" width="50" height="50" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="shape_demote" bpmnElement="demote">
<dc:Bounds x="860" y="80" width="150" height="70" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="shape_conforms" bpmnElement="conforms">
<dc:Bounds x="860" y="210" width="150" height="70" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="shape_test" bpmnElement="test">
<dc:Bounds x="1060" y="80" width="150" height="70" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="shape_record" bpmnElement="record">
<dc:Bounds x="1260" y="80" width="150" height="70" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="shape_release" bpmnElement="release">
<dc:Bounds x="1460" y="80" width="150" height="70" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="shape_end" bpmnElement="end">
<dc:Bounds x="1660" y="80" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="edge_flow_1" bpmnElement="flow_1">
<di:waypoint x="96" y="98" />
<di:waypoint x="260" y="115" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="edge_flow_2" bpmnElement="flow_2">
<di:waypoint x="410" y="115" />
<di:waypoint x="460" y="115" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="edge_flow_3" bpmnElement="flow_3">
<di:waypoint x="610" y="115" />
<di:waypoint x="660" y="105" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="edge_flow_4" bpmnElement="flow_4">
<di:waypoint x="710" y="105" />
<di:waypoint x="860" y="245" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="edge_flow_5" bpmnElement="flow_5">
<di:waypoint x="710" y="105" />
<di:waypoint x="860" y="115" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="edge_flow_6" bpmnElement="flow_6">
<di:waypoint x="1010" y="115" />
<di:waypoint x="1260" y="115" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="edge_flow_7" bpmnElement="flow_7">
<di:waypoint x="1010" y="245" />
<di:waypoint x="1060" y="115" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="edge_flow_8" bpmnElement="flow_8">
<di:waypoint x="1210" y="115" />
<di:waypoint x="1260" y="115" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="edge_flow_9" bpmnElement="flow_9">
<di:waypoint x="1410" y="115" />
<di:waypoint x="1460" y="115" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="edge_flow_10" bpmnElement="flow_10">
<di:waypoint x="1610" y="115" />
<di:waypoint x="1660" y="98" />
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>
+119
View File
@@ -0,0 +1,119 @@
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:pr="https://ontology.pernod-ricard.com/process/" id="defs_D1" targetNamespace="https://ontology.pernod-ricard.com/process/" exporter="generate_bpmn.py">
<bpmn:process id="D1" name="Deprecate a term" isExecutable="false">
<bpmn:documentation>Deprecate a term | Scope: class | relation | attribute | annotation | individual | Trigger: A term is superseded or no longer needed.</bpmn:documentation>
<bpmn:startEvent id="start" name="Term superseded">
<bpmn:outgoing>flow_1</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:userTask id="replacement" name="Identify the replacement">
<bpmn:incoming>flow_1</bpmn:incoming>
<bpmn:outgoing>flow_2</bpmn:outgoing>
</bpmn:userTask>
<bpmn:scriptTask id="count" name="Count the instances for information">
<bpmn:extensionElements>
<pr:rules>
<pr:rule>EV-015</pr:rule>
</pr:rules>
</bpmn:extensionElements>
<bpmn:incoming>flow_2</bpmn:incoming>
<bpmn:outgoing>flow_3</bpmn:outgoing>
</bpmn:scriptTask>
<bpmn:scriptTask id="mark" name="Mark deprecated and declare the replacement">
<bpmn:extensionElements>
<pr:rules>
<pr:rule>EV-001</pr:rule>
</pr:rules>
</bpmn:extensionElements>
<bpmn:incoming>flow_3</bpmn:incoming>
<bpmn:outgoing>flow_4</bpmn:outgoing>
</bpmn:scriptTask>
<bpmn:scriptTask id="strip" name="Strip the stub of every edge">
<bpmn:extensionElements>
<pr:rules>
<pr:rule>EV-001</pr:rule>
</pr:rules>
</bpmn:extensionElements>
<bpmn:incoming>flow_4</bpmn:incoming>
<bpmn:outgoing>flow_5</bpmn:outgoing>
</bpmn:scriptTask>
<bpmn:scriptTask id="label" name="Remove any mention of state from the label">
<bpmn:extensionElements>
<pr:rules>
<pr:rule>TN-020</pr:rule>
</pr:rules>
</bpmn:extensionElements>
<bpmn:incoming>flow_5</bpmn:incoming>
<bpmn:outgoing>flow_6</bpmn:outgoing>
</bpmn:scriptTask>
<bpmn:callActivity id="release" name="Propagate and publish" calledElement="X1">
<bpmn:incoming>flow_6</bpmn:incoming>
<bpmn:outgoing>flow_7</bpmn:outgoing>
</bpmn:callActivity>
<bpmn:endEvent id="end" name="Term deprecated">
<bpmn:incoming>flow_7</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="flow_1" sourceRef="start" targetRef="replacement" />
<bpmn:sequenceFlow id="flow_2" sourceRef="replacement" targetRef="count" />
<bpmn:sequenceFlow id="flow_3" sourceRef="count" targetRef="mark" />
<bpmn:sequenceFlow id="flow_4" sourceRef="mark" targetRef="strip" />
<bpmn:sequenceFlow id="flow_5" sourceRef="strip" targetRef="label" />
<bpmn:sequenceFlow id="flow_6" sourceRef="label" targetRef="release" />
<bpmn:sequenceFlow id="flow_7" sourceRef="release" targetRef="end" />
</bpmn:process>
<bpmndi:BPMNDiagram id="diagram_D1">
<bpmndi:BPMNPlane id="plane_D1" bpmnElement="D1">
<bpmndi:BPMNShape id="shape_start" bpmnElement="start">
<dc:Bounds x="60" y="80" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="shape_replacement" bpmnElement="replacement">
<dc:Bounds x="260" y="80" width="150" height="70" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="shape_count" bpmnElement="count">
<dc:Bounds x="460" y="80" width="150" height="70" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="shape_mark" bpmnElement="mark">
<dc:Bounds x="660" y="80" width="150" height="70" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="shape_strip" bpmnElement="strip">
<dc:Bounds x="860" y="80" width="150" height="70" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="shape_label" bpmnElement="label">
<dc:Bounds x="1060" y="80" width="150" height="70" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="shape_release" bpmnElement="release">
<dc:Bounds x="1260" y="80" width="150" height="70" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="shape_end" bpmnElement="end">
<dc:Bounds x="1460" y="80" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="edge_flow_1" bpmnElement="flow_1">
<di:waypoint x="96" y="98" />
<di:waypoint x="260" y="115" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="edge_flow_2" bpmnElement="flow_2">
<di:waypoint x="410" y="115" />
<di:waypoint x="460" y="115" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="edge_flow_3" bpmnElement="flow_3">
<di:waypoint x="610" y="115" />
<di:waypoint x="660" y="115" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="edge_flow_4" bpmnElement="flow_4">
<di:waypoint x="810" y="115" />
<di:waypoint x="860" y="115" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="edge_flow_5" bpmnElement="flow_5">
<di:waypoint x="1010" y="115" />
<di:waypoint x="1060" y="115" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="edge_flow_6" bpmnElement="flow_6">
<di:waypoint x="1210" y="115" />
<di:waypoint x="1260" y="115" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="edge_flow_7" bpmnElement="flow_7">
<di:waypoint x="1410" y="115" />
<di:waypoint x="1460" y="98" />
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>
+152
View File
@@ -0,0 +1,152 @@
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:pr="https://ontology.pernod-ricard.com/process/" id="defs_D2" targetNamespace="https://ontology.pernod-ricard.com/process/" exporter="generate_bpmn.py">
<bpmn:process id="D2" name="Withdraw a term permanently" isExecutable="false">
<bpmn:documentation>Withdraw a term permanently | Scope: class | relation | attribute | annotation | individual | Trigger: A deprecated term is to be removed from the vocabulary.</bpmn:documentation>
<bpmn:startEvent id="start" name="Withdrawal requested">
<bpmn:outgoing>flow_1</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:scriptTask id="count" name="Run the counting query">
<bpmn:extensionElements>
<pr:rules>
<pr:rule>EV-002</pr:rule>
<pr:rule>EV-015</pr:rule>
</pr:rules>
</bpmn:extensionElements>
<bpmn:incoming>flow_1</bpmn:incoming>
<bpmn:outgoing>flow_2</bpmn:outgoing>
</bpmn:scriptTask>
<bpmn:exclusiveGateway id="instantiated" name="Any instance found?">
<bpmn:incoming>flow_2</bpmn:incoming>
<bpmn:outgoing>flow_3</bpmn:outgoing>
<bpmn:outgoing>flow_4</bpmn:outgoing>
</bpmn:exclusiveGateway>
<bpmn:endEvent id="keep" name="Kept deprecated">
<bpmn:incoming>flow_3</bpmn:incoming>
</bpmn:endEvent>
<bpmn:scriptTask id="proof" name="Attach the proof to the commit">
<bpmn:extensionElements>
<pr:rules>
<pr:rule>EV-002</pr:rule>
<pr:rule>EV-015</pr:rule>
</pr:rules>
</bpmn:extensionElements>
<bpmn:incoming>flow_4</bpmn:incoming>
<bpmn:outgoing>flow_5</bpmn:outgoing>
</bpmn:scriptTask>
<bpmn:scriptTask id="remove" name="Remove the subject block">
<bpmn:incoming>flow_5</bpmn:incoming>
<bpmn:outgoing>flow_6</bpmn:outgoing>
</bpmn:scriptTask>
<bpmn:scriptTask id="refs" name="Remove every reference naming the subject">
<bpmn:extensionElements>
<pr:rules>
<pr:rule>EV-003</pr:rule>
</pr:rules>
</bpmn:extensionElements>
<bpmn:incoming>flow_6</bpmn:incoming>
<bpmn:outgoing>flow_7</bpmn:outgoing>
</bpmn:scriptTask>
<bpmn:callActivity id="check_voc" name="Verify the vocabulary" calledElement="R1">
<bpmn:incoming>flow_7</bpmn:incoming>
<bpmn:outgoing>flow_8</bpmn:outgoing>
</bpmn:callActivity>
<bpmn:callActivity id="check_abox" name="Validate the instances" calledElement="R2">
<bpmn:incoming>flow_8</bpmn:incoming>
<bpmn:outgoing>flow_9</bpmn:outgoing>
</bpmn:callActivity>
<bpmn:callActivity id="release" name="Propagate and publish" calledElement="X1">
<bpmn:incoming>flow_9</bpmn:incoming>
<bpmn:outgoing>flow_10</bpmn:outgoing>
</bpmn:callActivity>
<bpmn:endEvent id="end" name="Term withdrawn">
<bpmn:incoming>flow_10</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="flow_1" sourceRef="start" targetRef="count" />
<bpmn:sequenceFlow id="flow_2" sourceRef="count" targetRef="instantiated" />
<bpmn:sequenceFlow id="flow_3" name="yes" sourceRef="instantiated" targetRef="keep" />
<bpmn:sequenceFlow id="flow_4" name="no" sourceRef="instantiated" targetRef="proof" />
<bpmn:sequenceFlow id="flow_5" sourceRef="proof" targetRef="remove" />
<bpmn:sequenceFlow id="flow_6" sourceRef="remove" targetRef="refs" />
<bpmn:sequenceFlow id="flow_7" sourceRef="refs" targetRef="check_voc" />
<bpmn:sequenceFlow id="flow_8" sourceRef="check_voc" targetRef="check_abox" />
<bpmn:sequenceFlow id="flow_9" sourceRef="check_abox" targetRef="release" />
<bpmn:sequenceFlow id="flow_10" sourceRef="release" targetRef="end" />
</bpmn:process>
<bpmndi:BPMNDiagram id="diagram_D2">
<bpmndi:BPMNPlane id="plane_D2" bpmnElement="D2">
<bpmndi:BPMNShape id="shape_start" bpmnElement="start">
<dc:Bounds x="60" y="80" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="shape_count" bpmnElement="count">
<dc:Bounds x="260" y="80" width="150" height="70" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="shape_instantiated" bpmnElement="instantiated">
<dc:Bounds x="460" y="80" width="50" height="50" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="shape_keep" bpmnElement="keep">
<dc:Bounds x="660" y="80" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="shape_proof" bpmnElement="proof">
<dc:Bounds x="660" y="210" width="150" height="70" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="shape_remove" bpmnElement="remove">
<dc:Bounds x="860" y="80" width="150" height="70" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="shape_refs" bpmnElement="refs">
<dc:Bounds x="1060" y="80" width="150" height="70" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="shape_check_voc" bpmnElement="check_voc">
<dc:Bounds x="1260" y="80" width="150" height="70" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="shape_check_abox" bpmnElement="check_abox">
<dc:Bounds x="1460" y="80" width="150" height="70" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="shape_release" bpmnElement="release">
<dc:Bounds x="1660" y="80" width="150" height="70" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="shape_end" bpmnElement="end">
<dc:Bounds x="1860" y="80" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="edge_flow_1" bpmnElement="flow_1">
<di:waypoint x="96" y="98" />
<di:waypoint x="260" y="115" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="edge_flow_2" bpmnElement="flow_2">
<di:waypoint x="410" y="115" />
<di:waypoint x="460" y="105" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="edge_flow_3" bpmnElement="flow_3">
<di:waypoint x="510" y="105" />
<di:waypoint x="660" y="98" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="edge_flow_4" bpmnElement="flow_4">
<di:waypoint x="510" y="105" />
<di:waypoint x="660" y="245" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="edge_flow_5" bpmnElement="flow_5">
<di:waypoint x="810" y="245" />
<di:waypoint x="860" y="115" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="edge_flow_6" bpmnElement="flow_6">
<di:waypoint x="1010" y="115" />
<di:waypoint x="1060" y="115" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="edge_flow_7" bpmnElement="flow_7">
<di:waypoint x="1210" y="115" />
<di:waypoint x="1260" y="115" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="edge_flow_8" bpmnElement="flow_8">
<di:waypoint x="1410" y="115" />
<di:waypoint x="1460" y="115" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="edge_flow_9" bpmnElement="flow_9">
<di:waypoint x="1610" y="115" />
<di:waypoint x="1660" y="115" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="edge_flow_10" bpmnElement="flow_10">
<di:waypoint x="1810" y="115" />
<di:waypoint x="1860" y="98" />
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>
+131
View File
@@ -0,0 +1,131 @@
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:pr="https://ontology.pernod-ricard.com/process/" id="defs_R1" targetNamespace="https://ontology.pernod-ricard.com/process/" exporter="generate_bpmn.py">
<bpmn:process id="R1" name="Verify the vocabulary against the rulebook" isExecutable="false">
<bpmn:documentation>Verify the vocabulary against the rulebook | Scope: The whole vocabulary, or the subset touched by a change. | Trigger: A term has been created, changed or withdrawn, or a release is prepared.</bpmn:documentation>
<bpmn:startEvent id="start" name="Verification requested">
<bpmn:outgoing>flow_1</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:scriptTask id="script" name="Run the naming and declaration checks">
<bpmn:extensionElements>
<pr:rules>
<pr:rule>TN-001</pr:rule>
<pr:rule>TN-002</pr:rule>
<pr:rule>TN-003</pr:rule>
<pr:rule>TN-004</pr:rule>
<pr:rule>TN-005</pr:rule>
<pr:rule>TN-006</pr:rule>
<pr:rule>TN-007</pr:rule>
<pr:rule>TN-009</pr:rule>
<pr:rule>TN-010</pr:rule>
<pr:rule>TN-011</pr:rule>
<pr:rule>TN-012</pr:rule>
<pr:rule>TN-013</pr:rule>
<pr:rule>TN-014</pr:rule>
<pr:rule>TN-015</pr:rule>
<pr:rule>TN-017</pr:rule>
<pr:rule>TN-018</pr:rule>
<pr:rule>TN-019</pr:rule>
<pr:rule>TN-020</pr:rule>
<pr:rule>TN-021</pr:rule>
<pr:rule>TN-023</pr:rule>
<pr:rule>TN-025</pr:rule>
<pr:rule>TN-026</pr:rule>
<pr:rule>TN-027</pr:rule>
<pr:rule>TN-028</pr:rule>
<pr:rule>EV-014</pr:rule>
</pr:rules>
</bpmn:extensionElements>
<bpmn:incoming>flow_1</bpmn:incoming>
<bpmn:outgoing>flow_2</bpmn:outgoing>
</bpmn:scriptTask>
<bpmn:exclusiveGateway id="mechanised" name="Any mechanised violation?">
<bpmn:incoming>flow_2</bpmn:incoming>
<bpmn:outgoing>flow_3</bpmn:outgoing>
<bpmn:outgoing>flow_4</bpmn:outgoing>
</bpmn:exclusiveGateway>
<bpmn:endEvent id="report_fail" name="Report returned with violations">
<bpmn:incoming>flow_3</bpmn:incoming>
<bpmn:incoming>flow_6</bpmn:incoming>
</bpmn:endEvent>
<bpmn:userTask id="human" name="Review the rules no script can judge">
<bpmn:extensionElements>
<pr:rules>
<pr:rule>TN-008</pr:rule>
<pr:rule>TN-016</pr:rule>
<pr:rule>TN-022</pr:rule>
<pr:rule>TN-024</pr:rule>
</pr:rules>
</bpmn:extensionElements>
<bpmn:incoming>flow_4</bpmn:incoming>
<bpmn:outgoing>flow_5</bpmn:outgoing>
</bpmn:userTask>
<bpmn:exclusiveGateway id="judged" name="Reviewer raises an issue?">
<bpmn:incoming>flow_5</bpmn:incoming>
<bpmn:outgoing>flow_6</bpmn:outgoing>
<bpmn:outgoing>flow_7</bpmn:outgoing>
</bpmn:exclusiveGateway>
<bpmn:endEvent id="report_ok" name="Vocabulary conforms">
<bpmn:incoming>flow_7</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="flow_1" sourceRef="start" targetRef="script" />
<bpmn:sequenceFlow id="flow_2" sourceRef="script" targetRef="mechanised" />
<bpmn:sequenceFlow id="flow_3" name="at least one" sourceRef="mechanised" targetRef="report_fail" />
<bpmn:sequenceFlow id="flow_4" name="none" sourceRef="mechanised" targetRef="human" />
<bpmn:sequenceFlow id="flow_5" sourceRef="human" targetRef="judged" />
<bpmn:sequenceFlow id="flow_6" name="yes" sourceRef="judged" targetRef="report_fail" />
<bpmn:sequenceFlow id="flow_7" name="no" sourceRef="judged" targetRef="report_ok" />
</bpmn:process>
<bpmndi:BPMNDiagram id="diagram_R1">
<bpmndi:BPMNPlane id="plane_R1" bpmnElement="R1">
<bpmndi:BPMNShape id="shape_start" bpmnElement="start">
<dc:Bounds x="60" y="80" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="shape_script" bpmnElement="script">
<dc:Bounds x="260" y="80" width="150" height="70" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="shape_mechanised" bpmnElement="mechanised">
<dc:Bounds x="460" y="80" width="50" height="50" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="shape_report_fail" bpmnElement="report_fail">
<dc:Bounds x="1060" y="80" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="shape_human" bpmnElement="human">
<dc:Bounds x="660" y="80" width="150" height="70" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="shape_judged" bpmnElement="judged">
<dc:Bounds x="860" y="80" width="50" height="50" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="shape_report_ok" bpmnElement="report_ok">
<dc:Bounds x="1060" y="210" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="edge_flow_1" bpmnElement="flow_1">
<di:waypoint x="96" y="98" />
<di:waypoint x="260" y="115" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="edge_flow_2" bpmnElement="flow_2">
<di:waypoint x="410" y="115" />
<di:waypoint x="460" y="105" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="edge_flow_3" bpmnElement="flow_3">
<di:waypoint x="510" y="105" />
<di:waypoint x="1060" y="98" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="edge_flow_4" bpmnElement="flow_4">
<di:waypoint x="510" y="105" />
<di:waypoint x="660" y="115" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="edge_flow_5" bpmnElement="flow_5">
<di:waypoint x="810" y="115" />
<di:waypoint x="860" y="105" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="edge_flow_6" bpmnElement="flow_6">
<di:waypoint x="910" y="105" />
<di:waypoint x="1060" y="98" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="edge_flow_7" bpmnElement="flow_7">
<di:waypoint x="910" y="105" />
<di:waypoint x="1060" y="228" />
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>
+104
View File
@@ -0,0 +1,104 @@
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:pr="https://ontology.pernod-ricard.com/process/" id="defs_R2" targetNamespace="https://ontology.pernod-ricard.com/process/" exporter="generate_bpmn.py">
<bpmn:process id="R2" name="Validate instances against the shapes" isExecutable="false">
<bpmn:documentation>Validate instances against the shapes | Scope: Any instance graph in the validation perimeter. | Trigger: Instances have changed, shapes have changed, or a release is prepared.</bpmn:documentation>
<bpmn:startEvent id="start" name="Validation requested">
<bpmn:outgoing>flow_1</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:scriptTask id="conforms" name="Compare the declared target version with the ontology version">
<bpmn:extensionElements>
<pr:rules>
<pr:rule>EV-010</pr:rule>
</pr:rules>
</bpmn:extensionElements>
<bpmn:incoming>flow_1</bpmn:incoming>
<bpmn:outgoing>flow_2</bpmn:outgoing>
</bpmn:scriptTask>
<bpmn:exclusiveGateway id="match" name="Versions match?">
<bpmn:incoming>flow_2</bpmn:incoming>
<bpmn:outgoing>flow_3</bpmn:outgoing>
<bpmn:outgoing>flow_4</bpmn:outgoing>
</bpmn:exclusiveGateway>
<bpmn:endEvent id="abort" name="Aborted on version mismatch">
<bpmn:incoming>flow_3</bpmn:incoming>
</bpmn:endEvent>
<bpmn:scriptTask id="run" name="Run the shape validation">
<bpmn:incoming>flow_4</bpmn:incoming>
<bpmn:outgoing>flow_5</bpmn:outgoing>
</bpmn:scriptTask>
<bpmn:exclusiveGateway id="violations" name="Any violation?">
<bpmn:incoming>flow_5</bpmn:incoming>
<bpmn:outgoing>flow_6</bpmn:outgoing>
<bpmn:outgoing>flow_7</bpmn:outgoing>
</bpmn:exclusiveGateway>
<bpmn:endEvent id="fail" name="Report returned with violations">
<bpmn:incoming>flow_6</bpmn:incoming>
</bpmn:endEvent>
<bpmn:endEvent id="ok" name="Instances conform">
<bpmn:incoming>flow_7</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="flow_1" sourceRef="start" targetRef="conforms" />
<bpmn:sequenceFlow id="flow_2" sourceRef="conforms" targetRef="match" />
<bpmn:sequenceFlow id="flow_3" name="no" sourceRef="match" targetRef="abort" />
<bpmn:sequenceFlow id="flow_4" name="yes" sourceRef="match" targetRef="run" />
<bpmn:sequenceFlow id="flow_5" sourceRef="run" targetRef="violations" />
<bpmn:sequenceFlow id="flow_6" name="at least one" sourceRef="violations" targetRef="fail" />
<bpmn:sequenceFlow id="flow_7" name="none" sourceRef="violations" targetRef="ok" />
</bpmn:process>
<bpmndi:BPMNDiagram id="diagram_R2">
<bpmndi:BPMNPlane id="plane_R2" bpmnElement="R2">
<bpmndi:BPMNShape id="shape_start" bpmnElement="start">
<dc:Bounds x="60" y="80" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="shape_conforms" bpmnElement="conforms">
<dc:Bounds x="260" y="80" width="150" height="70" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="shape_match" bpmnElement="match">
<dc:Bounds x="460" y="80" width="50" height="50" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="shape_abort" bpmnElement="abort">
<dc:Bounds x="660" y="80" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="shape_run" bpmnElement="run">
<dc:Bounds x="660" y="210" width="150" height="70" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="shape_violations" bpmnElement="violations">
<dc:Bounds x="860" y="80" width="50" height="50" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="shape_fail" bpmnElement="fail">
<dc:Bounds x="1060" y="80" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="shape_ok" bpmnElement="ok">
<dc:Bounds x="1060" y="210" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="edge_flow_1" bpmnElement="flow_1">
<di:waypoint x="96" y="98" />
<di:waypoint x="260" y="115" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="edge_flow_2" bpmnElement="flow_2">
<di:waypoint x="410" y="115" />
<di:waypoint x="460" y="105" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="edge_flow_3" bpmnElement="flow_3">
<di:waypoint x="510" y="105" />
<di:waypoint x="660" y="98" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="edge_flow_4" bpmnElement="flow_4">
<di:waypoint x="510" y="105" />
<di:waypoint x="660" y="245" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="edge_flow_5" bpmnElement="flow_5">
<di:waypoint x="810" y="245" />
<di:waypoint x="860" y="105" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="edge_flow_6" bpmnElement="flow_6">
<di:waypoint x="910" y="105" />
<di:waypoint x="1060" y="98" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="edge_flow_7" bpmnElement="flow_7">
<di:waypoint x="910" y="105" />
<di:waypoint x="1060" y="228" />
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>
+121
View File
@@ -0,0 +1,121 @@
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:pr="https://ontology.pernod-ricard.com/process/" id="defs_R3" targetNamespace="https://ontology.pernod-ricard.com/process/" exporter="generate_bpmn.py">
<bpmn:process id="R3" name="Audit what the shapes cannot see" isExecutable="false">
<bpmn:documentation>Audit what the shapes cannot see | Scope: The whole vocabulary and its source file. | Trigger: Periodic audit, or before a version is published.</bpmn:documentation>
<bpmn:startEvent id="start" name="Audit requested">
<bpmn:outgoing>flow_1</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:scriptTask id="index" name="Build the normalised label index">
<bpmn:extensionElements>
<pr:rules>
<pr:rule>EV-012</pr:rule>
</pr:rules>
</bpmn:extensionElements>
<bpmn:incoming>flow_1</bpmn:incoming>
<bpmn:outgoing>flow_2</bpmn:outgoing>
</bpmn:scriptTask>
<bpmn:scriptTask id="hash" name="Hash every normalised subject block">
<bpmn:extensionElements>
<pr:rules>
<pr:rule>EV-012</pr:rule>
</pr:rules>
</bpmn:extensionElements>
<bpmn:incoming>flow_2</bpmn:incoming>
<bpmn:outgoing>flow_3</bpmn:outgoing>
</bpmn:scriptTask>
<bpmn:exclusiveGateway id="shortlist" name="Any candidate found?">
<bpmn:incoming>flow_3</bpmn:incoming>
<bpmn:outgoing>flow_4</bpmn:outgoing>
<bpmn:outgoing>flow_5</bpmn:outgoing>
</bpmn:exclusiveGateway>
<bpmn:endEvent id="clean" name="Nothing to arbitrate">
<bpmn:incoming>flow_4</bpmn:incoming>
</bpmn:endEvent>
<bpmn:userTask id="review" name="Arbitrate each candidate">
<bpmn:incoming>flow_5</bpmn:incoming>
<bpmn:outgoing>flow_6</bpmn:outgoing>
</bpmn:userTask>
<bpmn:exclusiveGateway id="act" name="Duplication confirmed?">
<bpmn:incoming>flow_6</bpmn:incoming>
<bpmn:outgoing>flow_7</bpmn:outgoing>
<bpmn:outgoing>flow_8</bpmn:outgoing>
</bpmn:exclusiveGateway>
<bpmn:endEvent id="merge" name="Referred to the merge procedure">
<bpmn:incoming>flow_7</bpmn:incoming>
</bpmn:endEvent>
<bpmn:endEvent id="dismissed" name="Candidates dismissed">
<bpmn:incoming>flow_8</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="flow_1" sourceRef="start" targetRef="index" />
<bpmn:sequenceFlow id="flow_2" sourceRef="index" targetRef="hash" />
<bpmn:sequenceFlow id="flow_3" sourceRef="hash" targetRef="shortlist" />
<bpmn:sequenceFlow id="flow_4" name="none" sourceRef="shortlist" targetRef="clean" />
<bpmn:sequenceFlow id="flow_5" name="at least one" sourceRef="shortlist" targetRef="review" />
<bpmn:sequenceFlow id="flow_6" sourceRef="review" targetRef="act" />
<bpmn:sequenceFlow id="flow_7" name="yes" sourceRef="act" targetRef="merge" />
<bpmn:sequenceFlow id="flow_8" name="no" sourceRef="act" targetRef="dismissed" />
</bpmn:process>
<bpmndi:BPMNDiagram id="diagram_R3">
<bpmndi:BPMNPlane id="plane_R3" bpmnElement="R3">
<bpmndi:BPMNShape id="shape_start" bpmnElement="start">
<dc:Bounds x="60" y="80" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="shape_index" bpmnElement="index">
<dc:Bounds x="260" y="80" width="150" height="70" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="shape_hash" bpmnElement="hash">
<dc:Bounds x="460" y="80" width="150" height="70" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="shape_shortlist" bpmnElement="shortlist">
<dc:Bounds x="660" y="80" width="50" height="50" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="shape_clean" bpmnElement="clean">
<dc:Bounds x="860" y="80" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="shape_review" bpmnElement="review">
<dc:Bounds x="860" y="210" width="150" height="70" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="shape_act" bpmnElement="act">
<dc:Bounds x="1060" y="80" width="50" height="50" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="shape_merge" bpmnElement="merge">
<dc:Bounds x="1260" y="80" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="shape_dismissed" bpmnElement="dismissed">
<dc:Bounds x="1260" y="210" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="edge_flow_1" bpmnElement="flow_1">
<di:waypoint x="96" y="98" />
<di:waypoint x="260" y="115" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="edge_flow_2" bpmnElement="flow_2">
<di:waypoint x="410" y="115" />
<di:waypoint x="460" y="115" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="edge_flow_3" bpmnElement="flow_3">
<di:waypoint x="610" y="115" />
<di:waypoint x="660" y="105" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="edge_flow_4" bpmnElement="flow_4">
<di:waypoint x="710" y="105" />
<di:waypoint x="860" y="98" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="edge_flow_5" bpmnElement="flow_5">
<di:waypoint x="710" y="105" />
<di:waypoint x="860" y="245" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="edge_flow_6" bpmnElement="flow_6">
<di:waypoint x="1010" y="245" />
<di:waypoint x="1060" y="105" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="edge_flow_7" bpmnElement="flow_7">
<di:waypoint x="1110" y="105" />
<di:waypoint x="1260" y="98" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="edge_flow_8" bpmnElement="flow_8">
<di:waypoint x="1110" y="105" />
<di:waypoint x="1260" y="228" />
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>
+326
View File
@@ -0,0 +1,326 @@
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:pr="https://ontology.pernod-ricard.com/process/" id="defs_X1" targetNamespace="https://ontology.pernod-ricard.com/process/" exporter="generate_bpmn.py">
<bpmn:process id="X1" name="Propagate a change and publish a version" isExecutable="false">
<bpmn:documentation>Propagate a change and publish a version | Scope: Any validated modification of the vocabulary. | Trigger: A change to the vocabulary has been agreed and is ready to apply.</bpmn:documentation>
<bpmn:startEvent id="start" name="Change agreed">
<bpmn:outgoing>flow_1</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:scriptTask id="branch" name="Create the migration branch">
<bpmn:extensionElements>
<pr:rules>
<pr:rule>EV-009</pr:rule>
</pr:rules>
</bpmn:extensionElements>
<bpmn:incoming>flow_1</bpmn:incoming>
<bpmn:outgoing>flow_2</bpmn:outgoing>
</bpmn:scriptTask>
<bpmn:scriptTask id="apply" name="Apply the change through an RDF parser">
<bpmn:extensionElements>
<pr:rules>
<pr:rule>EV-005</pr:rule>
<pr:rule>EV-006</pr:rule>
</pr:rules>
</bpmn:extensionElements>
<bpmn:incoming>flow_2</bpmn:incoming>
<bpmn:incoming>flow_16</bpmn:incoming>
<bpmn:outgoing>flow_3</bpmn:outgoing>
</bpmn:scriptTask>
<bpmn:scriptTask id="labels" name="Regenerate labels by derivation">
<bpmn:extensionElements>
<pr:rules>
<pr:rule>TN-023</pr:rule>
</pr:rules>
</bpmn:extensionElements>
<bpmn:incoming>flow_3</bpmn:incoming>
<bpmn:outgoing>flow_4</bpmn:outgoing>
</bpmn:scriptTask>
<bpmn:scriptTask id="abox" name="Propagate to the instances">
<bpmn:extensionElements>
<pr:rules>
<pr:rule>EV-009</pr:rule>
</pr:rules>
</bpmn:extensionElements>
<bpmn:incoming>flow_4</bpmn:incoming>
<bpmn:outgoing>flow_5</bpmn:outgoing>
</bpmn:scriptTask>
<bpmn:scriptTask id="shapes" name="Update the shapes and align conformsTo">
<bpmn:extensionElements>
<pr:rules>
<pr:rule>EV-010</pr:rule>
</pr:rules>
</bpmn:extensionElements>
<bpmn:incoming>flow_5</bpmn:incoming>
<bpmn:outgoing>flow_6</bpmn:outgoing>
</bpmn:scriptTask>
<bpmn:scriptTask id="artifacts" name="Regenerate every derived artifact">
<bpmn:extensionElements>
<pr:rules>
<pr:rule>EV-008</pr:rule>
</pr:rules>
</bpmn:extensionElements>
<bpmn:incoming>flow_6</bpmn:incoming>
<bpmn:outgoing>flow_7</bpmn:outgoing>
</bpmn:scriptTask>
<bpmn:scriptTask id="replay" name="Replay the chain a second time">
<bpmn:extensionElements>
<pr:rules>
<pr:rule>EV-006</pr:rule>
<pr:rule>EV-007</pr:rule>
</pr:rules>
</bpmn:extensionElements>
<bpmn:incoming>flow_7</bpmn:incoming>
<bpmn:outgoing>flow_8</bpmn:outgoing>
</bpmn:scriptTask>
<bpmn:exclusiveGateway id="idem" name="Second run reports zero change?">
<bpmn:incoming>flow_8</bpmn:incoming>
<bpmn:outgoing>flow_9</bpmn:outgoing>
<bpmn:outgoing>flow_10</bpmn:outgoing>
</bpmn:exclusiveGateway>
<bpmn:callActivity id="verify_voc" name="Verify the vocabulary" calledElement="R1">
<bpmn:incoming>flow_9</bpmn:incoming>
<bpmn:outgoing>flow_11</bpmn:outgoing>
</bpmn:callActivity>
<bpmn:callActivity id="verify_abox" name="Validate the instances" calledElement="R2">
<bpmn:incoming>flow_11</bpmn:incoming>
<bpmn:outgoing>flow_12</bpmn:outgoing>
</bpmn:callActivity>
<bpmn:scriptTask id="log" name="Write the execution log for this attempt">
<bpmn:extensionElements>
<pr:rules>
<pr:rule>EV-015</pr:rule>
</pr:rules>
</bpmn:extensionElements>
<bpmn:incoming>flow_12</bpmn:incoming>
<bpmn:outgoing>flow_13</bpmn:outgoing>
</bpmn:scriptTask>
<bpmn:exclusiveGateway id="clean" name="Any violation?">
<bpmn:incoming>flow_13</bpmn:incoming>
<bpmn:outgoing>flow_14</bpmn:outgoing>
<bpmn:outgoing>flow_15</bpmn:outgoing>
</bpmn:exclusiveGateway>
<bpmn:userTask id="decide" name="Correct or abandon">
<bpmn:extensionElements>
<pr:rules>
<pr:rule>EV-004</pr:rule>
<pr:rule>EV-007</pr:rule>
</pr:rules>
</bpmn:extensionElements>
<bpmn:incoming>flow_10</bpmn:incoming>
<bpmn:incoming>flow_15</bpmn:incoming>
<bpmn:outgoing>flow_16</bpmn:outgoing>
<bpmn:outgoing>flow_17</bpmn:outgoing>
</bpmn:userTask>
<bpmn:scriptTask id="drop" name="Destroy the branch and restart from the published version">
<bpmn:extensionElements>
<pr:rules>
<pr:rule>EV-007</pr:rule>
</pr:rules>
</bpmn:extensionElements>
<bpmn:incoming>flow_17</bpmn:incoming>
<bpmn:outgoing>flow_18</bpmn:outgoing>
</bpmn:scriptTask>
<bpmn:userTask id="review" name="Review the merge">
<bpmn:extensionElements>
<pr:rules>
<pr:rule>EV-004</pr:rule>
<pr:rule>EV-009</pr:rule>
</pr:rules>
</bpmn:extensionElements>
<bpmn:incoming>flow_14</bpmn:incoming>
<bpmn:outgoing>flow_19</bpmn:outgoing>
</bpmn:userTask>
<bpmn:scriptTask id="merge" name="Merge as a block">
<bpmn:extensionElements>
<pr:rules>
<pr:rule>EV-009</pr:rule>
</pr:rules>
</bpmn:extensionElements>
<bpmn:incoming>flow_19</bpmn:incoming>
<bpmn:outgoing>flow_20</bpmn:outgoing>
</bpmn:scriptTask>
<bpmn:scriptTask id="bump" name="Increment the ontology version">
<bpmn:extensionElements>
<pr:rules>
<pr:rule>EV-014</pr:rule>
</pr:rules>
</bpmn:extensionElements>
<bpmn:incoming>flow_20</bpmn:incoming>
<bpmn:outgoing>flow_21</bpmn:outgoing>
</bpmn:scriptTask>
<bpmn:endEvent id="end" name="Version published">
<bpmn:incoming>flow_21</bpmn:incoming>
</bpmn:endEvent>
<bpmn:endEvent id="aborted" name="Change abandoned">
<bpmn:incoming>flow_18</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="flow_1" sourceRef="start" targetRef="branch" />
<bpmn:sequenceFlow id="flow_2" sourceRef="branch" targetRef="apply" />
<bpmn:sequenceFlow id="flow_3" sourceRef="apply" targetRef="labels" />
<bpmn:sequenceFlow id="flow_4" sourceRef="labels" targetRef="abox" />
<bpmn:sequenceFlow id="flow_5" sourceRef="abox" targetRef="shapes" />
<bpmn:sequenceFlow id="flow_6" sourceRef="shapes" targetRef="artifacts" />
<bpmn:sequenceFlow id="flow_7" sourceRef="artifacts" targetRef="replay" />
<bpmn:sequenceFlow id="flow_8" sourceRef="replay" targetRef="idem" />
<bpmn:sequenceFlow id="flow_9" name="zero change" sourceRef="idem" targetRef="verify_voc" />
<bpmn:sequenceFlow id="flow_10" name="the chain is not replayable" sourceRef="idem" targetRef="decide" />
<bpmn:sequenceFlow id="flow_11" sourceRef="verify_voc" targetRef="verify_abox" />
<bpmn:sequenceFlow id="flow_12" sourceRef="verify_abox" targetRef="log" />
<bpmn:sequenceFlow id="flow_13" sourceRef="log" targetRef="clean" />
<bpmn:sequenceFlow id="flow_14" name="none" sourceRef="clean" targetRef="review" />
<bpmn:sequenceFlow id="flow_15" name="at least one" sourceRef="clean" targetRef="decide" />
<bpmn:sequenceFlow id="flow_16" name="correct" sourceRef="decide" targetRef="apply" />
<bpmn:sequenceFlow id="flow_17" name="abandon" sourceRef="decide" targetRef="drop" />
<bpmn:sequenceFlow id="flow_18" sourceRef="drop" targetRef="aborted" />
<bpmn:sequenceFlow id="flow_19" sourceRef="review" targetRef="merge" />
<bpmn:sequenceFlow id="flow_20" sourceRef="merge" targetRef="bump" />
<bpmn:sequenceFlow id="flow_21" sourceRef="bump" targetRef="end" />
</bpmn:process>
<bpmndi:BPMNDiagram id="diagram_X1">
<bpmndi:BPMNPlane id="plane_X1" bpmnElement="X1">
<bpmndi:BPMNShape id="shape_start" bpmnElement="start">
<dc:Bounds x="60" y="80" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="shape_branch" bpmnElement="branch">
<dc:Bounds x="260" y="80" width="150" height="70" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="shape_apply" bpmnElement="apply">
<dc:Bounds x="11660" y="80" width="150" height="70" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="shape_labels" bpmnElement="labels">
<dc:Bounds x="11860" y="80" width="150" height="70" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="shape_abox" bpmnElement="abox">
<dc:Bounds x="12060" y="80" width="150" height="70" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="shape_shapes" bpmnElement="shapes">
<dc:Bounds x="11460" y="80" width="150" height="70" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="shape_artifacts" bpmnElement="artifacts">
<dc:Bounds x="11660" y="210" width="150" height="70" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="shape_replay" bpmnElement="replay">
<dc:Bounds x="11860" y="210" width="150" height="70" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="shape_idem" bpmnElement="idem">
<dc:Bounds x="12060" y="210" width="50" height="50" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="shape_verify_voc" bpmnElement="verify_voc">
<dc:Bounds x="11460" y="210" width="150" height="70" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="shape_verify_abox" bpmnElement="verify_abox">
<dc:Bounds x="11660" y="340" width="150" height="70" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="shape_log" bpmnElement="log">
<dc:Bounds x="11860" y="340" width="150" height="70" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="shape_clean" bpmnElement="clean">
<dc:Bounds x="12060" y="340" width="50" height="50" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="shape_decide" bpmnElement="decide">
<dc:Bounds x="11460" y="340" width="150" height="70" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="shape_drop" bpmnElement="drop">
<dc:Bounds x="11660" y="470" width="150" height="70" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="shape_review" bpmnElement="review">
<dc:Bounds x="11460" y="470" width="150" height="70" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="shape_merge" bpmnElement="merge">
<dc:Bounds x="11660" y="600" width="150" height="70" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="shape_bump" bpmnElement="bump">
<dc:Bounds x="11860" y="470" width="150" height="70" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="shape_end" bpmnElement="end">
<dc:Bounds x="12060" y="470" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="shape_aborted" bpmnElement="aborted">
<dc:Bounds x="11860" y="600" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="edge_flow_1" bpmnElement="flow_1">
<di:waypoint x="96" y="98" />
<di:waypoint x="260" y="115" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="edge_flow_2" bpmnElement="flow_2">
<di:waypoint x="410" y="115" />
<di:waypoint x="11660" y="115" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="edge_flow_3" bpmnElement="flow_3">
<di:waypoint x="11810" y="115" />
<di:waypoint x="11860" y="115" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="edge_flow_4" bpmnElement="flow_4">
<di:waypoint x="12010" y="115" />
<di:waypoint x="12060" y="115" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="edge_flow_5" bpmnElement="flow_5">
<di:waypoint x="12210" y="115" />
<di:waypoint x="11460" y="115" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="edge_flow_6" bpmnElement="flow_6">
<di:waypoint x="11610" y="115" />
<di:waypoint x="11660" y="245" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="edge_flow_7" bpmnElement="flow_7">
<di:waypoint x="11810" y="245" />
<di:waypoint x="11860" y="245" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="edge_flow_8" bpmnElement="flow_8">
<di:waypoint x="12010" y="245" />
<di:waypoint x="12060" y="235" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="edge_flow_9" bpmnElement="flow_9">
<di:waypoint x="12110" y="235" />
<di:waypoint x="11460" y="245" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="edge_flow_10" bpmnElement="flow_10">
<di:waypoint x="12110" y="235" />
<di:waypoint x="11460" y="375" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="edge_flow_11" bpmnElement="flow_11">
<di:waypoint x="11610" y="245" />
<di:waypoint x="11660" y="375" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="edge_flow_12" bpmnElement="flow_12">
<di:waypoint x="11810" y="375" />
<di:waypoint x="11860" y="375" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="edge_flow_13" bpmnElement="flow_13">
<di:waypoint x="12010" y="375" />
<di:waypoint x="12060" y="365" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="edge_flow_14" bpmnElement="flow_14">
<di:waypoint x="12110" y="365" />
<di:waypoint x="11460" y="505" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="edge_flow_15" bpmnElement="flow_15">
<di:waypoint x="12110" y="365" />
<di:waypoint x="11460" y="375" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="edge_flow_16" bpmnElement="flow_16">
<di:waypoint x="11610" y="375" />
<di:waypoint x="11660" y="115" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="edge_flow_17" bpmnElement="flow_17">
<di:waypoint x="11610" y="375" />
<di:waypoint x="11660" y="505" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="edge_flow_18" bpmnElement="flow_18">
<di:waypoint x="11810" y="505" />
<di:waypoint x="11860" y="618" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="edge_flow_19" bpmnElement="flow_19">
<di:waypoint x="11610" y="505" />
<di:waypoint x="11660" y="635" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="edge_flow_20" bpmnElement="flow_20">
<di:waypoint x="11810" y="635" />
<di:waypoint x="11860" y="505" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="edge_flow_21" bpmnElement="flow_21">
<di:waypoint x="12010" y="505" />
<di:waypoint x="12060" y="488" />
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+206
View File
@@ -0,0 +1,206 @@
#!/usr/bin/env python3
"""
Generate strict, queryable BPMN 2.0 files from processes.yaml (EV-008).
USAGE
python3 generate_bpmn.py [output_dir]
One file per procedure, in bpmn/. Rule identifiers are carried in
extensionElements under the project namespace, so a rule can be traced to every
procedure that enforces it with a single XPath expression:
//bpmn:process[.//pr:rule='TN-002']/@id
Diagram interchange is emitted with a simple left-to-right layout so the files
open directly in any BPMN editor. Layout is cosmetic and never authoritative.
"""
import os
import sys
from xml.sax.saxutils import escape
import yaml
HERE = os.path.dirname(os.path.abspath(__file__))
SRC = os.path.join(HERE, "processes.yaml")
OUTDIR = sys.argv[1] if len(sys.argv) > 1 else os.path.join(HERE, "bpmn")
BPMN = "http://www.omg.org/spec/BPMN/20100524/MODEL"
BPMNDI = "http://www.omg.org/spec/BPMN/20100524/DI"
DI = "http://www.omg.org/spec/DD/20100524/DI"
DC = "http://www.omg.org/spec/DD/20100524/DC"
TAG = {
"start": "startEvent",
"end": "endEvent",
"userTask": "userTask",
"scriptTask": "scriptTask",
"callActivity": "callActivity",
"gateway": "exclusiveGateway",
}
W = {"start": 36, "end": 36, "gateway": 50}
H = {"start": 36, "end": 36, "gateway": 50}
DEFAULT_W, DEFAULT_H = 150, 70
COL, ROW = 200, 130
def selfcheck(doc, rule_ids):
"""A procedure must be a connected graph, and every rule it cites must exist."""
problems = []
ids = {p["id"] for p in doc["processes"]}
for p in doc["processes"]:
nodes = {n["id"] for n in p["flow"]}
for f in p["flows"]:
for side in ("from", "to"):
if f[side] not in nodes:
problems.append("%s: flow references unknown node %s" % (p["id"], f[side]))
targets = {f["to"] for f in p["flows"]}
sources = {f["from"] for f in p["flows"]}
for n in p["flow"]:
if n["type"] == "start" and n["id"] in targets:
problems.append("%s: start event %s has an incoming flow" % (p["id"], n["id"]))
if n["type"] == "end" and n["id"] in sources:
problems.append("%s: end event %s has an outgoing flow" % (p["id"], n["id"]))
if n["type"] not in ("start", "end") and n["id"] not in targets:
problems.append("%s: node %s is unreachable" % (p["id"], n["id"]))
if n["type"] != "end" and n["id"] not in sources:
problems.append("%s: node %s is a dead end" % (p["id"], n["id"]))
if n["type"] == "callActivity" and n.get("calls") not in ids:
problems.append("%s: %s calls unknown procedure %s"
% (p["id"], n["id"], n.get("calls")))
for r in n.get("rules") or []:
if rule_ids and r not in rule_ids:
problems.append("%s/%s: unknown rule %s" % (p["id"], n["id"], r))
return problems
def layout(proc):
"""Assign a column by longest path from start, a row to disambiguate siblings."""
succ = {}
for f in proc["flows"]:
succ.setdefault(f["from"], []).append(f["to"])
starts = [n["id"] for n in proc["flow"] if n["type"] == "start"]
depth = {}
for s in starts:
stack = [(s, 0)]
seen = set()
while stack:
node, d = stack.pop()
if (node, d) in seen:
continue
seen.add((node, d))
if depth.get(node, -1) < d:
depth[node] = d
for nxt in succ.get(node, []):
if d < 60:
stack.append((nxt, d + 1))
used = {}
pos = {}
for n in proc["flow"]:
c = depth.get(n["id"], 0)
r = used.get(c, 0)
used[c] = r + 1
pos[n["id"]] = (60 + c * COL, 80 + r * ROW)
return pos
def build(proc, ns):
pid = proc["id"]
o = []
a = o.append
a('<?xml version="1.0" encoding="UTF-8"?>')
a('<bpmn:definitions xmlns:bpmn="%s" xmlns:bpmndi="%s" xmlns:di="%s" '
'xmlns:dc="%s" xmlns:pr="%s" id="defs_%s" targetNamespace="%s" '
'exporter="generate_bpmn.py">' % (BPMN, BPMNDI, DI, DC, ns, pid, ns))
a(' <bpmn:process id="%s" name="%s" isExecutable="false">'
% (pid, escape(proc["name"])))
doc = "%s | Scope: %s | Trigger: %s" % (
proc["name"], proc.get("scope", ""), proc.get("trigger", ""))
a(' <bpmn:documentation>%s</bpmn:documentation>' % escape(doc))
incoming, outgoing = {}, {}
for i, f in enumerate(proc["flows"], start=1):
fid = "flow_%d" % i
f["_id"] = fid
outgoing.setdefault(f["from"], []).append(fid)
incoming.setdefault(f["to"], []).append(fid)
for n in proc["flow"]:
tag = TAG[n["type"]]
attrs = 'id="%s" name="%s"' % (n["id"], escape(n["name"]))
if n["type"] == "callActivity":
attrs += ' calledElement="%s"' % n["calls"]
a(' <bpmn:%s %s>' % (tag, attrs))
rules = n.get("rules") or []
if rules:
a(' <bpmn:extensionElements>')
a(' <pr:rules>')
for r in rules:
a(' <pr:rule>%s</pr:rule>' % r)
a(' </pr:rules>')
a(' </bpmn:extensionElements>')
for fid in incoming.get(n["id"], []):
a(' <bpmn:incoming>%s</bpmn:incoming>' % fid)
for fid in outgoing.get(n["id"], []):
a(' <bpmn:outgoing>%s</bpmn:outgoing>' % fid)
a(' </bpmn:%s>' % tag)
for f in proc["flows"]:
nm = ' name="%s"' % escape(f["condition"]) if f.get("condition") else ""
a(' <bpmn:sequenceFlow id="%s"%s sourceRef="%s" targetRef="%s" />'
% (f["_id"], nm, f["from"], f["to"]))
a(' </bpmn:process>')
pos = layout(proc)
a(' <bpmndi:BPMNDiagram id="diagram_%s">' % pid)
a(' <bpmndi:BPMNPlane id="plane_%s" bpmnElement="%s">' % (pid, pid))
for n in proc["flow"]:
x, y = pos[n["id"]]
w, h = W.get(n["type"], DEFAULT_W), H.get(n["type"], DEFAULT_H)
a(' <bpmndi:BPMNShape id="shape_%s" bpmnElement="%s">' % (n["id"], n["id"]))
a(' <dc:Bounds x="%d" y="%d" width="%d" height="%d" />' % (x, y, w, h))
a(' </bpmndi:BPMNShape>')
for f in proc["flows"]:
sx, sy = pos[f["from"]]
tx, ty = pos[f["to"]]
sw = W.get(next(n["type"] for n in proc["flow"] if n["id"] == f["from"]), DEFAULT_W)
sh = H.get(next(n["type"] for n in proc["flow"] if n["id"] == f["from"]), DEFAULT_H)
th = H.get(next(n["type"] for n in proc["flow"] if n["id"] == f["to"]), DEFAULT_H)
a(' <bpmndi:BPMNEdge id="edge_%s" bpmnElement="%s">' % (f["_id"], f["_id"]))
a(' <di:waypoint x="%d" y="%d" />' % (sx + sw, sy + sh // 2))
a(' <di:waypoint x="%d" y="%d" />' % (tx, ty + th // 2))
a(' </bpmndi:BPMNEdge>')
a(' </bpmndi:BPMNPlane>')
a(' </bpmndi:BPMNDiagram>')
a('</bpmn:definitions>')
return "\n".join(o) + "\n"
def main():
doc = yaml.safe_load(open(SRC, encoding="utf-8"))
rule_ids = set()
rb = os.path.join(HERE, "rules.yaml")
if os.path.exists(rb):
rule_ids = {r["id"] for r in yaml.safe_load(open(rb, encoding="utf-8"))["rules"]}
problems = selfcheck(doc, rule_ids)
if problems:
print("SELF-CHECK FAILED")
for p in problems:
print(" " + p)
sys.exit(1)
os.makedirs(OUTDIR, exist_ok=True)
ns = doc["meta"]["namespace"]
for proc in doc["processes"]:
path = os.path.join(OUTDIR, "%s.bpmn" % proc["id"])
open(path, "w", encoding="utf-8").write(build(proc, ns))
n_rules = len({r for n in proc["flow"] for r in (n.get("rules") or [])})
print(" %-4s %-46s %2d nodes, %2d flows, %2d rules"
% (proc["id"], proc["name"], len(proc["flow"]),
len(proc["flows"]), n_rules))
print("self-check passed: %d procedures, graphs connected, every rule known"
% len(doc["processes"]))
if __name__ == "__main__":
main()
+157
View File
@@ -0,0 +1,157 @@
#!/usr/bin/env python3
"""
Generate the Markdown processbook, with Mermaid views, from processes.yaml
(EV-008). The BPMN files remain the authoritative form; the views here are
derived from the same source and cannot drift from it.
USAGE
python3 generate_processbook_md.py [output.md]
"""
import os
import sys
import yaml
HERE = os.path.dirname(os.path.abspath(__file__))
SRC = os.path.join(HERE, "processes.yaml")
OUT = sys.argv[1] if len(sys.argv) > 1 else os.path.join(HERE, "PR_TBox_Processbook.md")
SHAPE = {
"start": ("([", "])"),
"end": ("([", "])"),
"userTask": ("[", "]"),
"scriptTask": ("[", "]"),
"callActivity": ("[[", "]]"),
"gateway": ("{", "}"),
}
KIND = {
"userTask": "human",
"scriptTask": "script",
"callActivity": "calls",
"gateway": "decision",
"start": "start",
"end": "end",
}
def flow(t):
return " ".join((t or "").split())
def mermaid(proc):
out = ["```mermaid", "flowchart TD"]
for n in proc["flow"]:
o, c = SHAPE[n["type"]]
label = n["name"].replace('"', "'")
if n["type"] == "callActivity":
label = "%s: %s" % (n["calls"], label)
out.append(' %s%s"%s"%s' % (n["id"], o, label, c))
for f in proc["flows"]:
if f.get("condition"):
out.append(' %s -->|%s| %s' % (f["from"], f["condition"], f["to"]))
else:
out.append(" %s --> %s" % (f["from"], f["to"]))
for n in proc["flow"]:
if n["type"] == "userTask":
out.append(" class %s human;" % n["id"])
elif n["type"] == "gateway":
out.append(" class %s decision;" % n["id"])
out.append(" classDef human fill:#FFF4CE,stroke:#B08900;")
out.append(" classDef decision fill:#E8F0F7,stroke:#3E6E96;")
out.append("```")
return out
def main():
doc = yaml.safe_load(open(SRC, encoding="utf-8"))
m, out = doc["meta"], []
w = out.append
w("# %s" % m["title"])
w("")
w("**Version %s** — %s — generated %s" % (m["version"], m["status"], m["date"]))
w("")
w("> Generated from `processes.yaml`. The BPMN files in `bpmn/` are the "
"authoritative form; the views below are derived from the same source. "
"Do not edit this document (EV-007, EV-008).")
w("")
w(flow(m["scope"]))
w("")
w("## Procedures")
w("")
w("| Id | Family | Procedure | Scope |")
w("|---|---|---|---|")
for p in doc["processes"]:
w("| **%s** | %s | %s | %s |" % (p["id"], p["family"], p["name"], p.get("scope", "")))
w("")
w("## Reading a diagram")
w("")
w("- A **rounded** node is a start or end state.")
w("- A **diamond** is a decision; every outgoing path is labelled.")
w("- A **shaded** box is a human task: it cannot be automated, and the work "
"stops there until someone acts.")
w("- A **double-bordered** box calls another procedure by its identifier.")
w("")
for p in doc["processes"]:
w("---")
w("")
w("# %s%s" % (p["id"], p["name"]))
w("")
w("| | |")
w("|---|---|")
w("| Family | %s |" % p["family"])
w("| Scope | %s |" % p.get("scope", ""))
w("| Trigger | %s |" % p.get("trigger", ""))
w("| Inputs | %s |" % ", ".join(p.get("inputs") or []))
w("| Outputs | %s |" % ", ".join(p.get("outputs") or []))
rules = sorted({r for n in p["flow"] for r in (n.get("rules") or [])})
w("| Rules enforced | %s |" % (", ".join(rules) or "-"))
calls = sorted({n["calls"] for n in p["flow"] if n["type"] == "callActivity"})
w("| Calls | %s |" % (", ".join(calls) or "-"))
w("")
if p.get("parameter"):
w("**Parameter.** %s" % flow(p["parameter"]))
w("")
if p.get("note"):
w("**Note.** %s" % flow(p["note"]))
w("")
out.extend(mermaid(p))
w("")
w("| Step | Type | Rules |")
w("|---|---|---|")
for n in p["flow"]:
if n["type"] in ("start", "end"):
continue
name = n["name"]
if n["type"] == "callActivity":
name = "%s (%s)" % (name, n["calls"])
w("| %s | %s | %s |"
% (name, KIND[n["type"]], ", ".join(n.get("rules") or []) or "-"))
w("")
w("---")
w("")
w("# Rule coverage")
w("")
w("Which procedures enforce each rule. Derived, not maintained: a rule cited "
"in a diagram appears here automatically.")
w("")
cover = {}
for p in doc["processes"]:
for n in p["flow"]:
for r in n.get("rules") or []:
cover.setdefault(r, set()).add(p["id"])
w("| Rule | Procedures |")
w("|---|---|")
for r in sorted(cover):
w("| %s | %s |" % (r, ", ".join(sorted(cover[r]))))
w("")
open(OUT, "w", encoding="utf-8").write("\n".join(out))
print("written: %s (%d lines, %d procedures, %d rules covered)"
% (OUT, len(out), len(doc["processes"]), len(cover)))
if __name__ == "__main__":
main()
+154
View File
@@ -0,0 +1,154 @@
#!/usr/bin/env python3
"""
Generate the Markdown rulebook from rules.yaml (EV-008).
USAGE
python3 generate_rulebook_md.py [output.md]
rules.yaml is the single source of truth. This script never edits it.
The output must never be edited by hand (EV-007).
"""
import os
import sys
import yaml
HERE = os.path.dirname(os.path.abspath(__file__))
SRC = os.path.join(HERE, "rules.yaml")
OUT = sys.argv[1] if len(sys.argv) > 1 else os.path.join(HERE, "PR_TBox_Rulebook.md")
def selfcheck(doc):
"""EV-011 and EV-012 applied to the rulebook itself."""
problems = []
for r in doc["rules"]:
ctl = r.get("control") or {}
if not ctl.get("tier"):
problems.append("%s: no control tier (EV-012)" % r["id"])
if r.get("severity") == "BLOCKING" and not ctl.get("executor"):
problems.append("%s: BLOCKING without an executor (EV-011)" % r["id"])
if r.get("category") not in doc["categories"]:
problems.append("%s: unknown category" % r["id"])
return problems
def flow(text):
"""Collapse a YAML folded scalar into a single paragraph."""
return " ".join((text or "").split())
def joined(v):
return ", ".join(v) if isinstance(v, list) else (v or "-")
def main():
doc = yaml.safe_load(open(SRC, encoding="utf-8"))
problems = selfcheck(doc)
if problems:
print("SELF-CHECK FAILED")
for p in problems:
print(" " + p)
sys.exit(1)
m, out = doc["meta"], []
w = out.append
w("# %s" % m["title"])
w("")
w("**Version %s** — %s — generated %s" % (m["version"], m["status"], m["date"]))
w("")
w("> Generated from `rules.yaml`. Do not edit this document: edit the source "
"and regenerate (EV-007, EV-008).")
w("")
w("## Scope")
w("")
w(flow(m["scope"]))
w("")
w(flow(m["audience"]))
w("")
w("## Categories")
w("")
w("| Category | Subject | Rules |")
w("|---|---|---|")
for k, v in doc["categories"].items():
n = sum(1 for r in doc["rules"] if r["category"] == k)
w("| **%s** | %s | %d |" % (k, v["title"], n))
w("")
w("## Severity")
w("")
for k, v in doc["severity_model"].items():
w("- **%s** — %s" % (k, flow(v)))
w("")
for cat, info in doc["categories"].items():
w("---")
w("")
w("# %s%s" % (cat, info["title"]))
w("")
w(flow(info["intent"]))
w("")
for r in [x for x in doc["rules"] if x["category"] == cat]:
ctl = r.get("control") or {}
w("## %s%s" % (r["id"], r["title"]))
w("")
w("**Statement.** %s" % flow(r["statement"]))
w("")
w("| | |")
w("|---|---|")
w("| Severity | **%s** |" % r["severity"])
w("| Applies to | %s |" % joined(r.get("scope")))
w("| Enforced at | %s |" % joined(ctl.get("tier")))
w("| Executor | `%s` |" % (ctl.get("executor") or "-"))
w("| Procedure | %s |" % (ctl.get("procedure") or "_pending_"))
if r.get("filiation"):
w("| Related | %s |" % r["filiation"])
w("")
if r.get("rationale"):
w("**Why.** %s" % flow(r["rationale"]))
w("")
if r.get("examples"):
w("| From | To | Note |")
w("|---|---|---|")
for e in r["examples"]:
w("| %s | %s | %s |" % (e["from"], e["to"], e.get("note") or ""))
w("")
ab = doc["abstractness"]
w("---")
w("")
w("# Annex A — Abstractness")
w("")
w("Applies TN-006 and TN-007 to the model. %d classes, of which **%d abstract** "
"and **%d concrete**. Maintained with the model, not after it."
% (len(ab), sum(1 for a in ab if a["is_abstract"]),
sum(1 for a in ab if not a["is_abstract"])))
w("")
w("| Class | isAbstract | Note |")
w("|---|---|---|")
for a in ab:
w("| `%s` | %s | %s |"
% (a["term"], "**true**" if a["is_abstract"] else "false", a.get("note") or ""))
w("")
w("---")
w("")
w("# Annex B — Display")
w("")
w("Applies TN-011 and TN-022. The short label is what screens display and what "
"relation names reuse; the acronym is a search key, never an identity.")
w("")
w("| IRI | rdfs:label | shortLabel | acronym | Note |")
w("|---|---|---|---|---|")
for d in doc["display"]:
w("| `%s` | %s | %s | %s | %s |"
% (d["iri"], d["label"], d.get("short_label") or "-",
d.get("acronym") or "-", d.get("note") or ""))
w("")
open(OUT, "w", encoding="utf-8").write("\n".join(out))
print("self-check passed: %d rules, every tier assigned, no blocking rule "
"without an executor" % len(doc["rules"]))
print("written: %s (%d lines)" % (OUT, len(out)))
if __name__ == "__main__":
main()
+218
View File
@@ -0,0 +1,218 @@
#!/usr/bin/env python3
"""
Generate the Excel rulebook from rules.yaml (EV-008).
USAGE
python3 generate_rulebook_xlsx.py [output.xlsx]
rules.yaml is the single source of truth. This script never edits it.
The output must never be edited by hand (EV-007).
"""
import os
import sys
import yaml
from openpyxl import Workbook
from openpyxl.styles import Alignment, Border, Font, PatternFill, Side
from openpyxl.utils import get_column_letter
HERE = os.path.dirname(os.path.abspath(__file__))
SRC = os.path.join(HERE, "rules.yaml")
OUT = sys.argv[1] if len(sys.argv) > 1 else os.path.join(HERE, "PR_TBox_Rulebook.xlsx")
FONT = "Arial"
INK = "1F2933"
HEAD_FILL = PatternFill("solid", fgColor="1F2933")
BAND = {
"Identifier": "E8F0F7",
"Label": "EAF3EC",
"Declaration": "FBF0E4",
"Evolution": "F2EAF5",
}
MAJOR_FILL = PatternFill("solid", fgColor="FFF4CE")
THIN = Side(style="thin", color="C9CFD6")
BORDER = Border(left=THIN, right=THIN, top=THIN, bottom=THIN)
def selfcheck(doc):
"""EV-011 and EV-012 applied to the rulebook itself."""
problems = []
for r in doc["rules"]:
ctl = r.get("control") or {}
if not ctl.get("tier"):
problems.append("%s: no control tier (EV-012)" % r["id"])
if r.get("severity") == "BLOCKING" and not ctl.get("executor"):
problems.append("%s: BLOCKING without an executor (EV-011)" % r["id"])
if r.get("category") not in doc["categories"]:
problems.append("%s: unknown category" % r["id"])
return problems
def flow(t):
return " ".join((t or "").split())
def joined(v):
return ", ".join(v) if isinstance(v, list) else (v if v else "")
def header(ws, cols):
for i, (title, width) in enumerate(cols, start=1):
c = ws.cell(row=1, column=i, value=title)
c.font = Font(name=FONT, size=10, bold=True, color="FFFFFF")
c.fill = HEAD_FILL
c.alignment = Alignment(vertical="center", horizontal="left", wrap_text=True)
c.border = BORDER
ws.column_dimensions[get_column_letter(i)].width = width
ws.row_dimensions[1].height = 28
ws.freeze_panes = ws.cell(row=2, column=1)
ws.auto_filter.ref = "A1:%s1" % get_column_letter(len(cols))
def put(ws, r, c, value, wrap=False, bold=False, fill=None, size=10):
cell = ws.cell(row=r, column=c, value=value)
cell.font = Font(name=FONT, size=size, bold=bold, color=INK)
cell.alignment = Alignment(vertical="top", wrap_text=wrap)
cell.border = BORDER
if fill:
cell.fill = fill
return cell
def sheet_cover(wb, doc):
m = doc["meta"]
ws = wb.create_sheet("Cover")
ws.column_dimensions["A"].width = 24
ws.column_dimensions["B"].width = 100
put(ws, 1, 1, m["title"], bold=True, size=14)
rows = [
("Version", "%s%s" % (m["version"], m["status"])),
("Generated", "%s from rules.yaml" % m["date"]),
("Scope", flow(m["scope"])),
("Audience", flow(m["audience"])),
("Generation", "Generated from rules.yaml by generate_rulebook_xlsx.py. "
"Do not edit this workbook: edit the source and regenerate "
"(EV-007, EV-008)."),
]
r = 3
for k, v in rows:
put(ws, r, 1, k, bold=True)
put(ws, r, 2, v, wrap=True)
ws.row_dimensions[r].height = 15 * (len(v) // 95 + 1)
r += 1
r += 1
put(ws, r, 1, "Categories", bold=True, size=12)
r += 1
for k, v in doc["categories"].items():
put(ws, r, 1, k, bold=True, fill=PatternFill("solid", fgColor=BAND[k]))
put(ws, r, 2, "%s%s" % (v["title"], flow(v["intent"])), wrap=True)
ws.row_dimensions[r].height = 30
r += 1
r += 1
put(ws, r, 1, "Counts", bold=True, size=12)
r += 1
for label, formula in [
("Rules", "=COUNTA(Rules!A2:A200)"),
("of which blocking", '=COUNTIF(Rules!D2:D200,"BLOCKING")'),
("Examples", "=COUNTA(Examples!A2:A400)"),
("Classes", "=COUNTA(Abstractness!A2:A200)"),
("of which abstract", "=COUNTIF(Abstractness!B2:B200,TRUE)"),
("Display entries", "=COUNTA(Display!A2:A200)"),
]:
put(ws, r, 1, label)
put(ws, r, 2, formula)
r += 1
def sheet_rules(wb, doc):
ws = wb.create_sheet("Rules")
header(ws, [("Rule", 10), ("Category", 14), ("Title", 40), ("Severity", 11),
("Statement", 70), ("Applies to", 24), ("Enforced at", 14),
("Executor", 26), ("Procedure", 16), ("Related", 16),
("Why", 90)])
r = 2
for x in doc["rules"]:
ctl = x.get("control") or {}
band = PatternFill("solid", fgColor=BAND[x["category"]])
put(ws, r, 1, x["id"], bold=True, fill=band)
put(ws, r, 2, x["category"], fill=band)
put(ws, r, 3, x["title"], wrap=True, bold=True)
put(ws, r, 4, x["severity"],
fill=MAJOR_FILL if x["severity"] != "BLOCKING" else None)
put(ws, r, 5, flow(x["statement"]), wrap=True)
put(ws, r, 6, joined(x.get("scope")), wrap=True)
put(ws, r, 7, joined(ctl.get("tier")))
put(ws, r, 8, ctl.get("executor") or "")
put(ws, r, 9, ctl.get("procedure") or "pending")
put(ws, r, 10, x.get("filiation") or "")
put(ws, r, 11, flow(x.get("rationale")), wrap=True)
ws.row_dimensions[r].height = 78
r += 1
def sheet_examples(wb, doc):
ws = wb.create_sheet("Examples")
header(ws, [("Rule", 10), ("Category", 14), ("From", 48), ("To", 48), ("Note", 62)])
r = 2
for x in doc["rules"]:
for e in x.get("examples") or []:
band = PatternFill("solid", fgColor=BAND[x["category"]])
put(ws, r, 1, x["id"], bold=True, fill=band)
put(ws, r, 2, x["category"], fill=band)
put(ws, r, 3, str(e["from"]), wrap=True)
put(ws, r, 4, str(e["to"]), wrap=True)
put(ws, r, 5, e.get("note") or "", wrap=True)
r += 1
def sheet_abstractness(wb, doc):
ws = wb.create_sheet("Abstractness")
header(ws, [("Class", 36), ("isAbstract", 12), ("Note", 96)])
r = 2
for a in doc["abstractness"]:
put(ws, r, 1, a["term"], bold=a["is_abstract"])
put(ws, r, 2, a["is_abstract"])
put(ws, r, 3, a.get("note") or "", wrap=True)
r += 1
def sheet_display(wb, doc):
ws = wb.create_sheet("Display")
header(ws, [("IRI", 36), ("rdfs:label", 36), ("shortLabel", 22),
("acronym", 10), ("Note", 80)])
r = 2
for d in doc["display"]:
put(ws, r, 1, d["iri"], bold=True)
put(ws, r, 2, d["label"])
put(ws, r, 3, d.get("short_label") or "")
put(ws, r, 4, d.get("acronym") or "")
put(ws, r, 5, d.get("note") or "", wrap=True)
r += 1
def main():
doc = yaml.safe_load(open(SRC, encoding="utf-8"))
problems = selfcheck(doc)
if problems:
print("SELF-CHECK FAILED")
for p in problems:
print(" " + p)
sys.exit(1)
wb = Workbook()
wb.remove(wb.active)
sheet_cover(wb, doc)
sheet_rules(wb, doc)
sheet_examples(wb, doc)
sheet_abstractness(wb, doc)
sheet_display(wb, doc)
wb.save(OUT)
print("self-check passed: %d rules" % len(doc["rules"]))
print("written: %s (%d sheets)" % (OUT, len(wb.sheetnames)))
if __name__ == "__main__":
main()
@@ -0,0 +1,52 @@
{
"attempt_timestamp": "2026-08-05T09:31:13",
"mode": "dry-run",
"from_version": "1.6",
"to_version": "2.0",
"input_checksums": {
"ontology/pr_metamodel.ttl": "552bbe0e9c0883d869ac5d6de40503bb",
"instances/sodh.ttl": "9eb1dbb96e325dbd61c6e880b43eaa44",
"shapes/pr_metamodel_shapes.ttl": "f6a1b4681e5817f8852c81424312e3d9"
},
"steps": {
"deprecated_dropped": 119,
"deprecated_kept": 2,
"rename_order_1": 244,
"rename_order_2": 256,
"rename_order_3": 14,
"merge": 5,
"reclassified": 2,
"classes_created": 1,
"reparented": 1,
"to_literal_property": 2,
"to_literal_values": 6,
"to_literal_dropped": 18,
"isAbstract_declared": 31,
"display_removed": 22,
"display_set": 25,
"labels_regenerated": 78,
"version_bumped": 1
},
"total_changes": 827,
"triples_before": {
"ontology/pr_metamodel.ttl": 853,
"instances/sodh.ttl": 2308,
"shapes/pr_metamodel_shapes.ttl": 395
},
"triples_after": {
"ontology/pr_metamodel.ttl": 734,
"instances/sodh.ttl": 2308,
"shapes/pr_metamodel_shapes.ttl": 395
},
"notes": [
"deprecated_kept: materializedAs still used 1 times",
"deprecated_kept: hasGranularity still used 1 times",
"reclassified: shortLabel",
"reclassified: acronym",
"classes_created: GovernanceLayerObject",
"reparented: Actor -> GovernanceLayerObject",
"to_literal_property: activationStatus",
"to_literal_property: environment",
"version_bumped: https://ontology.pernod-ricard.com/metamodel/2.0"
]
}
@@ -0,0 +1,52 @@
{
"attempt_timestamp": "2026-08-05T14:11:08",
"mode": "dry-run",
"from_version": "1.6",
"to_version": "2.0",
"input_checksums": {
"ontology/pr_metamodel.ttl": "552bbe0e9c0883d869ac5d6de40503bb",
"instances/sodh.ttl": "9eb1dbb96e325dbd61c6e880b43eaa44",
"shapes/pr_metamodel_shapes.ttl": "f6a1b4681e5817f8852c81424312e3d9"
},
"steps": {
"deprecated_dropped": 132,
"rename_order_1": 244,
"rename_order_2": 256,
"rename_order_3": 14,
"abox_aligned": 353,
"merge": 5,
"reclassified": 2,
"classes_created": 1,
"reparented": 1,
"to_literal_property": 2,
"to_literal_values": 6,
"to_literal_dropped": 18,
"isAbstract_declared": 31,
"display_removed": 22,
"display_set": 25,
"labels_regenerated": 76,
"version_bumped": 1,
"shapes_conformsTo": 1
},
"total_changes": 1190,
"triples_before": {
"ontology/pr_metamodel.ttl": 853,
"instances/sodh.ttl": 2308,
"shapes/pr_metamodel_shapes.ttl": 395
},
"triples_after": {
"ontology/pr_metamodel.ttl": 723,
"instances/sodh.ttl": 2308,
"shapes/pr_metamodel_shapes.ttl": 396
},
"notes": [
"reclassified: shortLabel",
"reclassified: acronym",
"classes_created: GovernanceLayerObject",
"reparented: Actor -> GovernanceLayerObject",
"to_literal_property: activationStatus",
"to_literal_property: environment",
"version_bumped: https://ontology.pernod-ricard.com/metamodel/2.0",
"shapes_conformsTo: https://ontology.pernod-ricard.com/metamodel/2.0"
]
}
@@ -0,0 +1,52 @@
{
"attempt_timestamp": "2026-08-05T14:14:31",
"mode": "apply",
"from_version": "1.6",
"to_version": "2.0",
"input_checksums": {
"ontology/pr_metamodel.ttl": "552bbe0e9c0883d869ac5d6de40503bb",
"instances/sodh.ttl": "9eb1dbb96e325dbd61c6e880b43eaa44",
"shapes/pr_metamodel_shapes.ttl": "f6a1b4681e5817f8852c81424312e3d9"
},
"steps": {
"deprecated_dropped": 132,
"rename_order_1": 244,
"rename_order_2": 256,
"rename_order_3": 14,
"abox_aligned": 353,
"merge": 5,
"reclassified": 2,
"classes_created": 1,
"reparented": 1,
"to_literal_property": 2,
"to_literal_values": 6,
"to_literal_dropped": 18,
"isAbstract_declared": 31,
"display_removed": 22,
"display_set": 25,
"labels_regenerated": 76,
"version_bumped": 1,
"shapes_conformsTo": 1
},
"total_changes": 1190,
"triples_before": {
"ontology/pr_metamodel.ttl": 853,
"instances/sodh.ttl": 2308,
"shapes/pr_metamodel_shapes.ttl": 395
},
"triples_after": {
"ontology/pr_metamodel.ttl": 723,
"instances/sodh.ttl": 2308,
"shapes/pr_metamodel_shapes.ttl": 396
},
"notes": [
"reclassified: shortLabel",
"reclassified: acronym",
"classes_created: GovernanceLayerObject",
"reparented: Actor -> GovernanceLayerObject",
"to_literal_property: activationStatus",
"to_literal_property: environment",
"version_bumped: https://ontology.pernod-ricard.com/metamodel/2.0",
"shapes_conformsTo: https://ontology.pernod-ricard.com/metamodel/2.0"
]
}
@@ -0,0 +1,30 @@
{
"attempt_timestamp": "2026-08-05T14:14:43",
"mode": "dry-run",
"from_version": "1.6",
"to_version": "2.0",
"input_checksums": {
"ontology/pr_metamodel.ttl": "32929c506d402738807b8b30c04fbb48",
"instances/sodh.ttl": "6bef7bad65c091738cd58cf5f0f1a395",
"shapes/pr_metamodel_shapes.ttl": "f87572aa372d4f6944c21e3cb12fb164"
},
"steps": {
"rename_order_1": 0,
"rename_order_2": 0,
"rename_order_3": 0,
"abox_aligned": 0,
"merge": 0
},
"total_changes": 0,
"triples_before": {
"ontology/pr_metamodel.ttl": 723,
"instances/sodh.ttl": 2308,
"shapes/pr_metamodel_shapes.ttl": 396
},
"triples_after": {
"ontology/pr_metamodel.ttl": 723,
"instances/sodh.ttl": 2308,
"shapes/pr_metamodel_shapes.ttl": 396
},
"notes": []
}
@@ -0,0 +1,33 @@
{
"attempt_timestamp": "2026-08-05T14:17:11",
"mode": "dry-run",
"from_version": "1.6",
"to_version": "2.0",
"input_checksums": {
"ontology/pr_metamodel.ttl": "32929c506d402738807b8b30c04fbb48",
"instances/sodh.ttl": "6bef7bad65c091738cd58cf5f0f1a395",
"shapes/pr_metamodel_shapes.ttl": "f6a1b4681e5817f8852c81424312e3d9"
},
"steps": {
"rename_order_1": 9,
"rename_order_2": 2,
"rename_order_3": 0,
"abox_aligned": 0,
"merge": 0,
"shapes_conformsTo": 1
},
"total_changes": 12,
"triples_before": {
"ontology/pr_metamodel.ttl": 723,
"instances/sodh.ttl": 2308,
"shapes/pr_metamodel_shapes.ttl": 395
},
"triples_after": {
"ontology/pr_metamodel.ttl": 723,
"instances/sodh.ttl": 2308,
"shapes/pr_metamodel_shapes.ttl": 398
},
"notes": [
"shapes_conformsTo: https://ontology.pernod-ricard.com/metamodel/2.0"
]
}
@@ -0,0 +1,33 @@
{
"attempt_timestamp": "2026-08-05T14:18:45",
"mode": "apply",
"from_version": "1.6",
"to_version": "2.0",
"input_checksums": {
"ontology/pr_metamodel.ttl": "32929c506d402738807b8b30c04fbb48",
"instances/sodh.ttl": "6bef7bad65c091738cd58cf5f0f1a395",
"shapes/pr_metamodel_shapes.ttl": "f6a1b4681e5817f8852c81424312e3d9"
},
"steps": {
"rename_order_1": 9,
"rename_order_2": 2,
"rename_order_3": 0,
"abox_aligned": 0,
"merge": 0,
"shapes_conformsTo": 1
},
"total_changes": 12,
"triples_before": {
"ontology/pr_metamodel.ttl": 723,
"instances/sodh.ttl": 2308,
"shapes/pr_metamodel_shapes.ttl": 395
},
"triples_after": {
"ontology/pr_metamodel.ttl": 723,
"instances/sodh.ttl": 2308,
"shapes/pr_metamodel_shapes.ttl": 398
},
"notes": [
"shapes_conformsTo: https://ontology.pernod-ricard.com/metamodel/2.0"
]
}
@@ -0,0 +1,30 @@
{
"attempt_timestamp": "2026-08-05T14:18:59",
"mode": "dry-run",
"from_version": "1.6",
"to_version": "2.0",
"input_checksums": {
"ontology/pr_metamodel.ttl": "ab4100d1eb8f0e16231339f675f80797",
"instances/sodh.ttl": "6bef7bad65c091738cd58cf5f0f1a395",
"shapes/pr_metamodel_shapes.ttl": "baa6fdf5edae6d8e5b1772f5ee467df9"
},
"steps": {
"rename_order_1": 0,
"rename_order_2": 0,
"rename_order_3": 0,
"abox_aligned": 0,
"merge": 0
},
"total_changes": 0,
"triples_before": {
"ontology/pr_metamodel.ttl": 723,
"instances/sodh.ttl": 2308,
"shapes/pr_metamodel_shapes.ttl": 398
},
"triples_after": {
"ontology/pr_metamodel.ttl": 723,
"instances/sodh.ttl": 2308,
"shapes/pr_metamodel_shapes.ttl": 398
},
"notes": []
}
@@ -0,0 +1,53 @@
{
"attempt_timestamp": "2026-08-05T14:25:50",
"mode": "dry-run",
"from_version": "1.6",
"to_version": "2.0",
"input_checksums": {
"ontology/pr_metamodel.ttl": "552bbe0e9c0883d869ac5d6de40503bb",
"instances/sodh.ttl": "9eb1dbb96e325dbd61c6e880b43eaa44",
"shapes/pr_metamodel_shapes.ttl": "f6a1b4681e5817f8852c81424312e3d9"
},
"steps": {
"deprecated_dropped": 130,
"rename_order_1": 244,
"rename_order_2": 256,
"rename_order_3": 14,
"abox_aligned": 356,
"queries_rewritten": 1,
"merge": 5,
"reclassified": 2,
"classes_created": 1,
"reparented": 1,
"to_literal_property": 2,
"to_literal_values": 6,
"to_literal_dropped": 18,
"isAbstract_declared": 31,
"display_removed": 22,
"display_set": 25,
"labels_regenerated": 76,
"version_bumped": 1,
"shapes_conformsTo": 1
},
"total_changes": 1192,
"triples_before": {
"ontology/pr_metamodel.ttl": 853,
"instances/sodh.ttl": 2308,
"shapes/pr_metamodel_shapes.ttl": 395
},
"triples_after": {
"ontology/pr_metamodel.ttl": 723,
"instances/sodh.ttl": 2308,
"shapes/pr_metamodel_shapes.ttl": 398
},
"notes": [
"reclassified: shortLabel",
"reclassified: acronym",
"classes_created: GovernanceLayerObject",
"reparented: Actor -> GovernanceLayerObject",
"to_literal_property: activationStatus",
"to_literal_property: environment",
"version_bumped: https://ontology.pernod-ricard.com/metamodel/2.0",
"shapes_conformsTo: https://ontology.pernod-ricard.com/metamodel/2.0"
]
}
@@ -0,0 +1,53 @@
{
"attempt_timestamp": "2026-08-05T14:27:09",
"mode": "apply",
"from_version": "1.6",
"to_version": "2.0",
"input_checksums": {
"ontology/pr_metamodel.ttl": "552bbe0e9c0883d869ac5d6de40503bb",
"instances/sodh.ttl": "9eb1dbb96e325dbd61c6e880b43eaa44",
"shapes/pr_metamodel_shapes.ttl": "f6a1b4681e5817f8852c81424312e3d9"
},
"steps": {
"deprecated_dropped": 130,
"rename_order_1": 244,
"rename_order_2": 256,
"rename_order_3": 14,
"abox_aligned": 356,
"queries_rewritten": 1,
"merge": 5,
"reclassified": 2,
"classes_created": 1,
"reparented": 1,
"to_literal_property": 2,
"to_literal_values": 6,
"to_literal_dropped": 18,
"isAbstract_declared": 31,
"display_removed": 22,
"display_set": 25,
"labels_regenerated": 76,
"version_bumped": 1,
"shapes_conformsTo": 1
},
"total_changes": 1192,
"triples_before": {
"ontology/pr_metamodel.ttl": 853,
"instances/sodh.ttl": 2308,
"shapes/pr_metamodel_shapes.ttl": 395
},
"triples_after": {
"ontology/pr_metamodel.ttl": 723,
"instances/sodh.ttl": 2308,
"shapes/pr_metamodel_shapes.ttl": 398
},
"notes": [
"reclassified: shortLabel",
"reclassified: acronym",
"classes_created: GovernanceLayerObject",
"reparented: Actor -> GovernanceLayerObject",
"to_literal_property: activationStatus",
"to_literal_property: environment",
"version_bumped: https://ontology.pernod-ricard.com/metamodel/2.0",
"shapes_conformsTo: https://ontology.pernod-ricard.com/metamodel/2.0"
]
}
@@ -0,0 +1,31 @@
{
"attempt_timestamp": "2026-08-05T14:27:20",
"mode": "dry-run",
"from_version": "1.6",
"to_version": "2.0",
"input_checksums": {
"ontology/pr_metamodel.ttl": "32929c506d402738807b8b30c04fbb48",
"instances/sodh.ttl": "6bef7bad65c091738cd58cf5f0f1a395",
"shapes/pr_metamodel_shapes.ttl": "d67b69756389a9864e306241d53114f1"
},
"steps": {
"rename_order_1": 0,
"rename_order_2": 0,
"rename_order_3": 0,
"abox_aligned": 0,
"queries_rewritten": 0,
"merge": 0
},
"total_changes": 0,
"triples_before": {
"ontology/pr_metamodel.ttl": 723,
"instances/sodh.ttl": 2308,
"shapes/pr_metamodel_shapes.ttl": 398
},
"triples_after": {
"ontology/pr_metamodel.ttl": 723,
"instances/sodh.ttl": 2308,
"shapes/pr_metamodel_shapes.ttl": 398
},
"notes": []
}
+440
View File
@@ -0,0 +1,440 @@
#!/usr/bin/env python3
"""
Migrate the Pernod Ricard Data MetaModel from v1.6 to v2.0.
USAGE
python3 migrate_tbox_v2_0.py # dry run, writes nothing
python3 migrate_tbox_v2_0.py --apply # writes in place
python3 migrate_tbox_v2_0.py --apply --log-dir logs/
--ontology path to the ontology TTL (default ../ontology/pr_metamodel.ttl)
--instances path to an instance TTL (repeatable)
--shapes path to a shapes TTL (repeatable)
DESIGN
EV-004 dry run is the default; nothing is written without --apply
EV-005 every edit goes through rdflib, never a regex on the text
EV-006 guards test the target state, so a replay reports zero change
EV-015 an execution log is written for every attempt
Run on GrosseBertha, inside the pinned venv:
. venv/bin/activate && python3 migrate_tbox_v2_0.py
"""
import argparse
import datetime
import hashlib
import json
import os
import re
import sys
from collections import OrderedDict
import yaml
from rdflib import Graph, Literal, Namespace, RDF, RDFS, OWL, URIRef, XSD
from rdflib.namespace import SH
HERE = os.path.dirname(os.path.abspath(__file__))
SPEC = os.path.join(HERE, "renames.yaml")
RULES = os.path.normpath(os.path.join(HERE, "..", "rules.yaml"))
DCTERMS = Namespace("http://purl.org/dc/terms/")
# ----------------------------------------------------------------- utilities
class Report(object):
"""Counts every step, so that idempotence is provable and not merely hoped."""
def __init__(self):
self.steps = OrderedDict()
self.notes = []
def add(self, step, n, detail=None):
self.steps[step] = self.steps.get(step, 0) + n
if detail:
self.notes.append("%s: %s" % (step, detail))
@property
def total(self):
return sum(self.steps.values())
def md5(path):
h = hashlib.md5()
with open(path, "rb") as f:
for chunk in iter(lambda: f.read(65536), b""):
h.update(chunk)
return h.hexdigest()
def local(uri, ns):
s = str(uri)
return s[len(ns):] if s.startswith(ns) else None
def decamelise(name, is_class):
"""TN-018. Consecutive capitals are kept together: they carry an acronym."""
spaced = re.sub(r"(?<=[a-z0-9])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])", " ", name)
return spaced if is_class else spaced[0].lower() + spaced[1:]
# ------------------------------------------------------------------ the work
def rename(graph, pr, old, new, report, step):
"""EV-001. Rewrite every triple naming the identifier, in all three positions."""
src, dst = pr[old], pr[new]
if (dst, None, None) in graph and (src, None, None) not in graph:
return 0 # EV-006: already done
n = 0
for s, p, o in list(graph):
ns, np_, no = s, p, o
if s == src:
ns = dst
if p == src:
np_ = dst
if o == src:
no = dst
if (ns, np_, no) != (s, p, o):
graph.remove((s, p, o))
graph.add((ns, np_, no))
n += 1
report.add(step, n)
return n
def merge(graph, pr, src_name, dst_name, report):
"""The source disappears into an existing target, which keeps its declaration."""
src, dst = pr[src_name], pr[dst_name]
n = 0
for s, p, o in list(graph.triples((None, src, None))):
graph.remove((s, p, o))
graph.add((s, dst, o))
n += 1
for s, p, o in list(graph.triples((src, None, None))):
graph.remove((s, p, o)) # drop the source declaration
n += 1
report.add("merge", n)
return n
def drop_subject(graph, subject, report, step):
"""EV-003. Remove the block AND every reference naming it, or inference rebuilds it."""
n = 0
for t in list(graph.triples((subject, None, None))):
graph.remove(t)
n += 1
for t in list(graph.triples((None, None, subject))):
graph.remove(t)
n += 1
for t in list(graph.triples((None, subject, None))):
graph.remove(t)
n += 1
report.add(step, n)
return n
def count_instances(graphs, term):
"""EV-002. The proof required before any permanent withdrawal.
Counted on INSTANCE graphs only. A mention inside a shapes graph is not an
instantiation: a shape carrying sh:maxCount 0 on a term is a guard forbidding
its reappearance, which is the opposite of a use, and it keeps working after
the term is gone since it checks an identifier rather than a declaration.
"""
n = 0
for g in graphs:
n += len(list(g.triples((None, RDF.type, term))))
n += len(list(g.triples((None, term, None))))
n += len(list(g.triples((None, None, term))))
return n
def rewrite_queries(graph, renames, report):
"""Rename inside embedded SPARQL queries.
A sh:select or sh:ask body is a literal, so no triple-level rename reaches
it: the query keeps naming a term that no longer exists and the constraint
silently stops matching. This is not the line-by-line editing EV-005
forbids: the graph is parsed, the literal is retrieved as a value, and what
is rewritten is a structured query language where a prefixed name has clear
token boundaries.
"""
n = 0
for prop in (SH.select, SH.ask, SH.construct):
for s, p, o in list(graph.triples((None, prop, None))):
text = str(o)
new = text
for old, target in renames:
new = re.sub(r"\bpr:%s\b" % re.escape(old), "pr:%s" % target, new)
if new != text:
graph.remove((s, p, o))
graph.add((s, p, Literal(new)))
n += 1
report.add("queries_rewritten", n)
return n
def migrate(onto, instances, shapes, spec, rules, report):
ns = spec["meta"]["namespace"]
pr = Namespace(ns)
others = instances + shapes
all_graphs = [onto] + others
# 1 — deprecated terms are withdrawn outright (phase clause), proof first.
# The proof is taken on instance graphs only: a mention in a shapes graph
# is a guard against reappearance, not a use.
# Reference cleaning likewise stops at the ontology and the instances.
# EV-003 guards against phantom nodes rebuilt by inference in the model
# graph; a mention inside a shape is a constraint on an identifier, in a
# separate graph, taking part in no inference over the ontology. Removing
# it would strip the sh:path from a guard and leave a property shape
# carrying a maxCount and no path — an invalid shape, and the loss of the
# very rule that forbids the term from coming back.
if spec["structural"].get("drop_deprecated"):
for subj in list(onto.subjects(OWL.deprecated, Literal(True))):
name = local(subj, ns) or str(subj)
used = count_instances(instances, subj)
if used:
report.add("deprecated_kept", 1, "%s instantiated %d times" % (name, used))
continue
for g in [onto] + instances:
drop_subject(g, subj, report, "deprecated_dropped")
# 2 — vocabulary renames, in dependency order
for order in (1, 2, 3):
for r in [x for x in spec["renames"] if x["order"] == order]:
for g in all_graphs:
rename(g, pr, r["from"], r["to"], report, "rename_order_%d" % order)
# 3 — instance-side alignment: forms the instances use that the vocabulary
# never declared and that the TN-012 renames do not converge on.
# Applied to the shapes too: they target these forms in sh:path.
for r in spec.get("abox_renames") or []:
for g in all_graphs:
rename(g, pr, r["from"], r["to"], report, "abox_aligned")
# 4 — embedded SPARQL queries carry term names as text, invisible to a
# triple-level rename. Rewritten with the same map, after it.
pairs = [(r["from"], r["to"]) for r in spec["renames"]]
pairs += [(r["from"], r["to"]) for r in (spec.get("abox_renames") or [])]
pairs += [(m["from"], m["into"]) for m in (spec.get("merges") or [])]
for g in all_graphs:
rewrite_queries(g, pairs, report)
# 5 — merges
for m in spec.get("merges") or []:
for g in all_graphs:
merge(g, pr, m["from"], m["into"], report)
# 6 — reclassify display properties (TN-003)
for rc in spec["structural"]["reclassify"]:
term = pr[rc["term"]]
if (term, RDF.type, OWL.AnnotationProperty) not in onto:
onto.remove((term, RDF.type, getattr(OWL, rc["from"])))
onto.add((term, RDF.type, OWL.AnnotationProperty))
report.add("reclassified", 1, rc["term"])
# 7 — create the governance layer root (TN-027)
for c in spec["structural"].get("create_classes") or []:
term = pr[c["term"]]
if (term, RDF.type, OWL.Class) not in onto:
onto.add((term, RDF.type, OWL.Class))
onto.add((term, RDFS.subClassOf, pr[c["parent"]]))
onto.add((term, RDFS.label, Literal(c["label"])))
onto.add((term, RDFS.comment, Literal(" ".join(c["comment"].split()))))
onto.add((term, pr.isAbstract, Literal(True)))
report.add("classes_created", 1, c["term"])
# 8 — reparent (TN-027)
for rp in spec["structural"].get("reparent") or []:
term, parent = pr[rp["term"]], pr[rp["parent"]]
if (term, RDFS.subClassOf, parent) not in onto:
onto.add((term, RDFS.subClassOf, parent))
report.add("reparented", 1, "%s -> %s" % (rp["term"], rp["parent"]))
# 9 — sub-properties (TN-028)
for sp in spec["structural"].get("subproperties") or []:
term, parent = pr[sp["term"]], pr[sp["parent"]]
if (term, RDFS.subPropertyOf, parent) not in onto:
onto.add((term, RDFS.subPropertyOf, parent))
report.add("subproperties", 1, "%s -> %s" % (sp["term"], sp["parent"]))
# 10 — controlled values become literals (TN-016, TN-017)
for conv in spec["structural"].get("to_literal") or []:
prop = pr[conv["property"]]
if (prop, RDF.type, OWL.DatatypeProperty) not in onto:
onto.remove((prop, RDF.type, OWL.ObjectProperty))
onto.add((prop, RDF.type, OWL.DatatypeProperty))
onto.remove((prop, RDFS.range, None))
onto.add((prop, RDFS.range, XSD.string))
onto.add((prop, RDFS.domain, pr[conv["domain"]]))
report.add("to_literal_property", 1, conv["property"])
for g in all_graphs: # rewrite the asserted values
for s, p, o in list(g.triples((None, prop, None))):
name = local(o, ns)
if name and name in conv["value_map"]:
g.remove((s, p, o))
g.add((s, p, Literal(conv["value_map"][name])))
report.add("to_literal_values", 1)
for ind in conv["drop_individuals"] + [conv["drop_class"]]:
subj = pr[ind]
if (subj, None, None) in onto:
for g in [onto] + instances: # shapes keep their guards
drop_subject(g, subj, report, "to_literal_dropped")
# 11 — abstractness, from the rulebook annex (TN-006, TN-007)
for a in rules["abstractness"]:
term = pr[a["term"]]
if (term, RDF.type, OWL.Class) not in onto:
report.add("abstract_missing_class", 1, a["term"])
continue
want = Literal(bool(a["is_abstract"]))
if (term, pr.isAbstract, want) not in onto:
onto.remove((term, pr.isAbstract, None))
onto.add((term, pr.isAbstract, want))
report.add("isAbstract_declared", 1)
# 12 — display annotations, from the rulebook annex (TN-022)
# The annex is authoritative AND exhaustive: a null value means any existing
# annotation is removed. A short label identical to the label carries no
# information and is one more thing to keep in step.
for d in rules["display"]:
term = pr[d["iri"]]
for prop, value in ((pr.shortLabel, d.get("short_label")),
(pr.acronym, d.get("acronym"))):
current = list(onto.objects(term, prop))
if value:
if (term, prop, Literal(value)) not in onto:
onto.remove((term, prop, None))
onto.add((term, prop, Literal(value)))
report.add("display_set", 1)
elif current:
onto.remove((term, prop, None))
report.add("display_removed", len(current))
# 13 — regenerate every label by derivation (TN-018, TN-023)
kinds = (OWL.Class, OWL.ObjectProperty, OWL.DatatypeProperty, OWL.AnnotationProperty)
for kind in kinds:
for term in set(onto.subjects(RDF.type, kind)):
name = local(term, ns)
if not name:
continue
want = Literal(decamelise(name, kind == OWL.Class))
if (term, RDFS.label, want) not in onto:
onto.remove((term, RDFS.label, None))
onto.add((term, RDFS.label, want))
report.add("labels_regenerated", 1)
# 14 — bump the ontology version (EV-014: the namespace itself never moves)
target = URIRef(ns.rstrip("/") + "/" + spec["meta"]["to_version"])
for onto_iri in set(onto.subjects(RDF.type, OWL.Ontology)):
if (onto_iri, OWL.versionIRI, target) not in onto:
onto.remove((onto_iri, OWL.versionIRI, None))
onto.add((onto_iri, OWL.versionIRI, target))
report.add("version_bumped", 1, str(target))
# 15 — the shapes declare the ontology version they target (EV-010).
# The shapes graph carries no ontology node, so it is created here.
so = spec["structural"].get("shapes_ontology")
if so:
node = URIRef(so["iri"])
for g in shapes:
if (node, DCTERMS.conformsTo, target) not in g:
g.remove((node, DCTERMS.conformsTo, None))
g.add((node, RDF.type, OWL.Ontology))
g.add((node, RDFS.label, Literal(so["label"])))
g.add((node, DCTERMS.conformsTo, target))
report.add("shapes_conformsTo", 1, str(target))
# ----------------------------------------------------------------------- main
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--apply", action="store_true", help="write the files (default: dry run)")
ap.add_argument("--ontology", default=os.path.join(HERE, "..", "ontology", "pr_metamodel.ttl"))
ap.add_argument("--instances", action="append", default=[])
ap.add_argument("--shapes", action="append", default=[])
ap.add_argument("--log-dir", default=os.path.join(HERE, "logs"))
args = ap.parse_args()
spec = yaml.safe_load(open(SPEC, encoding="utf-8"))
rules = yaml.safe_load(open(RULES, encoding="utf-8"))
paths = [args.ontology] + args.instances + args.shapes
missing = [p for p in paths if not os.path.exists(p)]
if missing:
print("MISSING INPUT")
for p in missing:
print(" " + p)
sys.exit(1)
inputs = OrderedDict((p, md5(p)) for p in paths)
onto = Graph()
onto.parse(args.ontology, format="turtle")
instances, shapes = [], []
for p in args.instances:
g = Graph()
g.parse(p, format="turtle")
instances.append(g)
for p in args.shapes:
g = Graph()
g.parse(p, format="turtle")
g.bind("dcterms", DCTERMS)
shapes.append(g)
others = instances + shapes
other_paths = args.instances + args.shapes
before = [len(onto)] + [len(g) for g in others]
report = Report()
migrate(onto, instances, shapes, spec, rules, report)
after = [len(onto)] + [len(g) for g in others]
print("MIGRATION %s -> %s %s"
% (spec["meta"]["from_version"], spec["meta"]["to_version"],
"APPLY" if args.apply else "DRY RUN"))
print()
for step, n in report.steps.items():
print(" %-28s %6d" % (step, n))
print(" %-28s %6d" % ("total changes", report.total))
print()
for p, b, a in zip(paths, before, after):
print(" %-46s %6d -> %6d triples" % (os.path.basename(p), b, a))
if report.notes:
print()
for n in report.notes:
print(" note: " + n)
log = {
"attempt_timestamp": datetime.datetime.now().isoformat(timespec="seconds"),
"mode": "apply" if args.apply else "dry-run",
"from_version": spec["meta"]["from_version"],
"to_version": spec["meta"]["to_version"],
"input_checksums": inputs,
"steps": report.steps,
"total_changes": report.total,
"triples_before": dict(zip(paths, before)),
"triples_after": dict(zip(paths, after)),
"notes": report.notes,
}
os.makedirs(args.log_dir, exist_ok=True)
stamp = datetime.datetime.now().strftime("%Y%m%dT%H%M%S")
log_path = os.path.join(args.log_dir, "migration_%s.json" % stamp)
json.dump(log, open(log_path, "w", encoding="utf-8"), indent=2)
print("\n log: %s" % log_path)
if not args.apply:
print("\n DRY RUN — nothing written. Re-run with --apply when the counts "
"above are what you expect.")
return
onto.serialize(destination=args.ontology, format="turtle")
for p, g in zip(other_paths, others):
g.serialize(destination=p, format="turtle")
print("\n written. Replay this script now: a second run must report zero "
"changes (EV-006).")
if __name__ == "__main__":
main()
+175
View File
@@ -0,0 +1,175 @@
# T-BOX MIGRATION TO v2.0 — RENAME MAP AND STRUCTURAL OPERATIONS
# =============================================================================
# Data consumed by migrate_tbox_v2_0.py. Kept separate from the script so that
# the transformation is reviewable without reading code (EV-004).
#
# Not listed here and derived from other sources at run time:
# - isAbstract declarations -> rules.yaml, annex `abstractness`
# - shortLabel and acronym -> rules.yaml, annex `display`
# - deprecated terms to drop -> found in the graph by owl:deprecated true
# Deriving them keeps a single source of truth for each fact (EV-008).
# =============================================================================
meta:
from_version: "1.6"
to_version: "2.0"
namespace: "https://ontology.pernod-ricard.com/metamodel/"
# -----------------------------------------------------------------------------
# RENAMES — 48. Applied everywhere the identifier appears: subject, predicate,
# object. Nothing cascades in RDF (EV-001).
#
# `order` is a migration constraint, not a preference:
# 1 ordinary terms
# 2 terms named in SHACL shapes or SPARQL constraints
# 3 the four attributes borne by MetaModelObject, therefore by every subject
# -----------------------------------------------------------------------------
renames:
# --- classes
- {from: SubDomain, to: DataSubDomain, kind: class, order: 1}
- {from: SubDomainOwner, to: DataSubDomainOwner, kind: class, order: 1}
- {from: KPI, to: KeyPerformanceIndicator, kind: class, order: 1}
- {from: BIWorkspace, to: BusinessIntelligenceWorkspace, kind: class, order: 1}
- {from: BIDataSource, to: BusinessIntelligenceDataSource, kind: class, order: 1}
- {from: BIField, to: BusinessIntelligenceField, kind: class, order: 1}
- {from: BIReport, to: BusinessIntelligenceReport, kind: class, order: 1}
# --- individual
- {from: BITool, to: BusinessIntelligenceTool, kind: individual, order: 1}
# --- relations
- {from: hasDGL, to: hasGovernanceLead, kind: relation, order: 1}
- {from: aboutConcept, to: isAbout, kind: relation, order: 1}
- {from: primaryLocation, to: primarilyStoredIn, kind: relation, order: 1}
- {from: inDatabase, to: isInDatabase, kind: relation, order: 1}
- {from: inSchema, to: isInSchema, kind: relation, order: 1}
- {from: inWorkspace, to: isInBIWorkspace, kind: relation, order: 1}
- {from: inBIDataSource, to: isInBIDataSource, kind: relation, order: 1}
- {from: hasBIField, to: containsBIField, kind: relation, order: 1}
- {from: hasBIOwner, to: hasPublisher, kind: relation, order: 1}
- {from: fromField, to: referencesSourceField, kind: relation, order: 1}
- {from: toField, to: referencesTargetField, kind: relation, order: 1}
- {from: biDerivedFrom, to: derivedFromSourceField, kind: relation, order: 1}
- {from: owningDomain, to: ownedByDomain, kind: relation, order: 2}
# --- attributes
- {from: hasBusinessDefinition, to: businessDefinition, kind: attribute, order: 1}
- {from: hasTechnicalDefinition, to: technicalDefinition, kind: attribute, order: 1}
- {from: hasSynonym, to: synonym, kind: attribute, order: 1}
- {from: hasFormula, to: formula, kind: attribute, order: 1}
- {from: hasUnit, to: unit, kind: attribute, order: 1}
- {from: hasTimeAggregation, to: timeAggregation, kind: attribute, order: 1}
- {from: hasFormat, to: logicalFormat, kind: attribute, order: 1}
- {from: hasPhysicalDataType, to: physicalDataType, kind: attribute, order: 1}
- {from: hasViewDefinition, to: viewDefinition, kind: attribute, order: 1}
- {from: hasExpression, to: expression, kind: attribute, order: 1}
- {from: hasBusinessRule, to: businessRule, kind: attribute, order: 1}
- {from: hasExampleValue, to: exampleValue, kind: attribute, order: 1}
- {from: createdOn, to: creationDate, kind: attribute, order: 1}
- {from: lastReviewedOn, to: lastReviewDate, kind: attribute, order: 1}
- {from: harvestedOn, to: harvestDate, kind: attribute, order: 1}
- {from: lastQueriedOn, to: lastQueryDate, kind: attribute, order: 1}
- {from: definedBy, to: definitionAddress, kind: attribute, order: 1}
- {from: queryCount30d, to: queryCount, kind: attribute, order: 1}
- {from: hasShortLabel, to: shortLabel, kind: annotation, order: 2}
- {from: hasAcronym, to: acronym, kind: annotation, order: 2}
- {from: hasIdentifier, to: identifier, kind: attribute, order: 3}
- {from: hasName, to: canonicalName, kind: attribute, order: 3}
- {from: hasStatus, to: status, kind: attribute, order: 3}
- {from: hasVersion, to: version, kind: attribute, order: 3}
# --- relations that change nature (TN-016): object property -> datatype property
- {from: hasActivationStatus, to: activationStatus, kind: relation_to_attribute, order: 1}
- {from: deployedIn, to: environment, kind: relation_to_attribute, order: 1}
# -----------------------------------------------------------------------------
# MERGES — the source identifier disappears into an existing target.
# Unlike a rename, the target already exists and keeps its own declaration.
# -----------------------------------------------------------------------------
merges:
- from: biConsumes
into: consumes
widen_domain: true
reason: >
Same verb, same target, identical meaning. The domain widens and scope is
controlled per class by SHACL. Ranges are compatible, so no RDFS retyping
is introduced (TN-028).
# -----------------------------------------------------------------------------
# A-BOX ALIGNMENT
# The instance graph was written against nominal forms that the vocabulary never
# declared: pr:identifier, pr:version, pr:status, pr:businessDefinition, pr:unit,
# pr:formula, pr:shortLabel, pr:physicalName. The TN-012 renames reconcile the
# two sides, since the vocabulary moves to the forms the instances already use.
# Two of them do not converge on their own and are renamed on the instance side.
# -----------------------------------------------------------------------------
abox_renames:
- {from: name, to: canonicalName, reason: "the vocabulary target is canonicalName; name alone is too generic to stand as an identity"}
- {from: format, to: logicalFormat, reason: "the vocabulary target is logicalFormat; format alone does not say which of the two"}
# -----------------------------------------------------------------------------
# STRUCTURAL OPERATIONS
# -----------------------------------------------------------------------------
structural:
reclassify:
- {term: shortLabel, from: DatatypeProperty, to: AnnotationProperty, rule: TN-003}
- {term: acronym, from: DatatypeProperty, to: AnnotationProperty, rule: TN-003}
create_classes:
- term: GovernanceLayerObject
parent: MetaModelObject
is_abstract: true
label: Governance Layer Object
comment: >
Root of the governance layer. Holds the actors and governance objects that
are not data objects and therefore sit outside the data layers. Created by
insertion above Actor rather than by renaming it, so that every range
pointing at Actor keeps stating the nature of its target rather than its
position in the model.
rule: TN-027
reparent:
- {term: Actor, parent: GovernanceLayerObject, rule: TN-027}
- {term: PhysicalLayerObject, parent: MetaModelObject, rule: TN-027}
subproperties:
- {term: primarilyStoredIn, parent: storedIn, rule: TN-028}
# TN-016: closed governance states become literals. The classes and their
# individuals are withdrawn, and the properties become datatype properties.
to_literal:
- property: activationStatus
domain: DataDomain
drop_class: ActivationStatus
drop_individuals: [NotActivated, LightActivation, FullActivation]
value_map:
NotActivated: NOT_ACTIVATED
LightActivation: LIGHT_ACTIVATION
FullActivation: FULL_ACTIVATION
- property: environment
domain: CapturedObject
drop_class: Environment
drop_individuals: [Development, UserAcceptance, Production]
value_map:
Development: DEVELOPMENT
UserAcceptance: USER_ACCEPTANCE
Production: PRODUCTION
# Phase clause: before v2.0 is published the project is in design, so
# deprecated terms are removed outright rather than kept as stubs. EV-002
# still applies — the count must be zero.
drop_deprecated: true
# EV-010: the shapes graph carries no ontology node at all, so the declaration
# has to be created rather than aligned. Without it, an instance graph can be
# validated against shapes written for another version and the run reports
# violations that read exactly like a regression of the model.
shapes_ontology:
iri: "https://ontology.pernod-ricard.com/shapes"
label: PR Data MetaModel SHACL Shapes
+375
View File
@@ -0,0 +1,375 @@
# PERNOD RICARD DATA METAMODEL — T-BOX PROCESSBOOK
# =============================================================================
# SINGLE SOURCE OF TRUTH for the procedures. The BPMN 2.0 files, the Mermaid
# views and the Markdown document are GENERATED from this file (EV-008).
#
# python3 generate_bpmn.py -> bpmn/<id>.bpmn (strict, queryable)
# python3 generate_processbook_md.py -> PR_TBox_Processbook.md (Mermaid views)
#
# NODE TYPES
# start | end events
# userTask a human decides or writes; cannot be automated
# scriptTask fully automated
# callActivity invokes another procedure by its id
# gateway exclusive decision; every outgoing flow is guarded
#
# Every node may carry `rules`, the identifiers of the rulebook rules it
# enforces. That list is what makes the BPMN queryable and what feeds the
# control.procedure field back into the rulebook.
# =============================================================================
meta:
title: Pernod Ricard Data MetaModel — T-Box Processbook
version: "0.1"
status: Draft for review
date: "2026-08-03"
scope: >
Procedures for changing the vocabulary of the model. Each procedure names
the rules it enforces, the steps a person must perform, and the points at
which the work stops rather than continues.
namespace: "https://ontology.pernod-ricard.com/process/"
families:
Create: Bringing a new element into the vocabulary.
Verify: Establishing that what exists conforms.
Update: Changing an element that already exists.
Delete: Removing an element from the vocabulary.
Release: Propagating a change and publishing a version.
processes:
# ---------------------------------------------------------------------- X1
- id: X1
family: Release
name: Propagate a change and publish a version
scope: Any validated modification of the vocabulary.
trigger: A change to the vocabulary has been agreed and is ready to apply.
inputs: [the agreed change, the target version, the list of affected artifacts]
outputs: [a merged branch, a tagged version, an execution log]
note: >
The most frequently invoked procedure of the processbook: every other
procedure ends by calling it. Two of its steps are gateways rather than
checks, because an idempotence failure and a validation failure must stop
the work rather than be noted in passing.
flow:
- {id: start, type: start, name: Change agreed}
- {id: branch, type: scriptTask, name: Create the migration branch, rules: [EV-009]}
- {id: apply, type: scriptTask, name: Apply the change through an RDF parser, rules: [EV-005, EV-006]}
- {id: labels, type: scriptTask, name: Regenerate labels by derivation, rules: [TN-023]}
- {id: abox, type: scriptTask, name: Propagate to the instances, rules: [EV-009]}
- {id: shapes, type: scriptTask, name: Update the shapes and align conformsTo, rules: [EV-010]}
- {id: artifacts, type: scriptTask, name: Regenerate every derived artifact, rules: [EV-008]}
- {id: replay, type: scriptTask, name: Replay the chain a second time, rules: [EV-006, EV-007]}
- {id: idem, type: gateway, name: "Second run reports zero change?"}
- {id: verify_voc, type: callActivity, calls: R1, name: Verify the vocabulary}
- {id: verify_abox, type: callActivity, calls: R2, name: Validate the instances}
- {id: log, type: scriptTask, name: Write the execution log for this attempt, rules: [EV-015]}
- {id: clean, type: gateway, name: "Any violation?"}
- {id: decide, type: userTask, name: Correct or abandon, rules: [EV-004, EV-007]}
- {id: drop, type: scriptTask, name: Destroy the branch and restart from the published version, rules: [EV-007]}
- {id: review, type: userTask, name: Review the merge, rules: [EV-004, EV-009]}
- {id: merge, type: scriptTask, name: Merge as a block, tag, push the tag separately, rules: [EV-009]}
- {id: bump, type: scriptTask, name: Increment the ontology version, rules: [EV-014]}
- {id: end, type: end, name: Version published}
- {id: aborted, type: end, name: Change abandoned}
flows:
- {from: start, to: branch}
- {from: branch, to: apply}
- {from: apply, to: labels}
- {from: labels, to: abox}
- {from: abox, to: shapes}
- {from: shapes, to: artifacts}
- {from: artifacts, to: replay}
- {from: replay, to: idem}
- {from: idem, to: verify_voc, condition: "zero change"}
- {from: idem, to: decide, condition: "the chain is not replayable"}
- {from: verify_voc, to: verify_abox}
- {from: verify_abox, to: log}
- {from: log, to: clean}
- {from: clean, to: review, condition: "none"}
- {from: clean, to: decide, condition: "at least one"}
- {from: decide, to: apply, condition: correct}
- {from: decide, to: drop, condition: abandon}
- {from: drop, to: aborted}
- {from: review, to: merge}
- {from: merge, to: bump}
- {from: bump, to: end}
# ---------------------------------------------------------------------- C1
- id: C1
family: Create
name: Create a term
scope: class | relation | attribute | annotation
parameter: >
The nature of the term selects the naming rules applied at the naming step:
a class takes TN-001, TN-002, TN-004, TN-005, TN-006, TN-007 and TN-027; a
relation takes TN-001, TN-002, TN-009, TN-010, TN-011 and TN-028; an
attribute takes TN-001, TN-002, TN-004, TN-012, TN-013, TN-014 and TN-015;
an annotation takes TN-001, TN-002 and TN-003.
trigger: A missing concept, edge or field has been identified.
inputs: [the intended meaning, the nature, the target layer, the provenance, the parent or the domain and range]
outputs: [a declared term, a published version]
note: >
The first step has no tool today. Checking that no existing term already
covers the need is the semantic uniqueness control that SHACL cannot
perform, and it stays a human task until R3 exists.
flow:
- {id: start, type: start, name: Need identified}
- {id: unique, type: userTask, name: Check that no existing term covers the need}
- {id: exists, type: gateway, name: "A term already covers it?"}
- {id: reuse, type: end, name: Reuse the existing term}
- {id: name, type: scriptTask, name: Name the term according to its nature, rules: [TN-001, TN-002, TN-003, TN-004, TN-005, TN-006, TN-009, TN-010, TN-011, TN-012, TN-013, TN-014, TN-015, TN-028]}
- {id: label, type: scriptTask, name: Derive the label, rules: [TN-018, TN-019, TN-020, TN-021, TN-023]}
- {id: short, type: userTask, name: Decide the short label and the acronym, rules: [TN-022]}
- {id: comment, type: userTask, name: Write the comment, rules: [TN-024]}
- {id: declare, type: scriptTask, name: Declare typing, provenance and attachment, rules: [TN-007, TN-025, TN-026, TN-027]}
- {id: validate, type: userTask, name: Validate before applying, rules: [EV-004]}
- {id: check, type: callActivity, calls: R1, name: Verify against the rulebook}
- {id: release, type: callActivity, calls: X1, name: Propagate and publish}
- {id: end, type: end, name: Term available}
flows:
- {from: start, to: unique}
- {from: unique, to: exists}
- {from: exists, to: reuse, condition: "yes"}
- {from: exists, to: name, condition: "no"}
- {from: name, to: label}
- {from: label, to: short}
- {from: short, to: comment}
- {from: comment, to: declare}
- {from: declare, to: validate}
- {from: validate, to: check}
- {from: check, to: release}
- {from: release, to: end}
# ---------------------------------------------------------------------- C2
- id: C2
family: Create
name: Create a controlled value
scope: typed individual | literal
parameter: >
The form is not a style choice but the outcome of the first decision. The
two branches have different consequences: the literal branch edits the
shapes and therefore the vocabulary, so it falls under the version freeze;
the individual branch touches nothing else.
trigger: A new value is needed in a controlled set.
inputs: [the value, the set it belongs to, whether a domain may add others]
outputs: [a declared value, a published version]
flow:
- {id: start, type: start, name: New value needed}
- {id: decide, type: userTask, name: "Must it be defined, owned or extended by a domain?", rules: [TN-016]}
- {id: form, type: gateway, name: "Which form?"}
- {id: indiv, type: scriptTask, name: Create the typed individual and derive its label, rules: [TN-001, TN-018, TN-022]}
- {id: literal, type: scriptTask, name: Write the literal in upper snake case, rules: [TN-017]}
- {id: shape, type: callActivity, calls: C3, name: Extend the closed list in the shapes}
- {id: validate, type: userTask, name: Validate before applying, rules: [EV-004]}
- {id: check, type: callActivity, calls: R1, name: Verify against the rulebook}
- {id: release, type: callActivity, calls: X1, name: Propagate and publish}
- {id: end, type: end, name: Value available}
flows:
- {from: start, to: decide}
- {from: decide, to: form}
- {from: form, to: indiv, condition: "extensible by a domain"}
- {from: form, to: literal, condition: "closed governance state"}
- {from: indiv, to: validate}
- {from: literal, to: shape}
- {from: shape, to: validate}
- {from: validate, to: check}
- {from: check, to: release}
- {from: release, to: end}
# ---------------------------------------------------------------------- C3
- id: C3
family: Create
name: Create a shape
scope: Any SHACL constraint added to the validation set.
trigger: A rule needs an executor, or a new class enters the validation perimeter.
inputs: [the rule to enforce, the target class or property]
outputs: [a shape, a rulebook entry naming it as executor]
note: >
The step that is forgotten is the last one before release: recording the
shape as the executor of its rule. Without it a rule stays declared
blocking with nothing enforcing it, which EV-011 forbids.
flow:
- {id: start, type: start, name: Constraint needed}
- {id: write, type: userTask, name: Write the constraint}
- {id: pattern, type: scriptTask, name: Check that every pattern is expressed positively, rules: [EV-013]}
- {id: portable, type: gateway, name: "Expressible within the specification?"}
- {id: demote, type: userTask, name: Move the rule to the script tier, rules: [EV-012]}
- {id: conforms, type: scriptTask, name: Align the declared ontology version, rules: [EV-010]}
- {id: test, type: callActivity, calls: R2, name: Test against real instances}
- {id: record, type: userTask, name: Record the shape as the executor of its rule, rules: [EV-011, EV-012]}
- {id: release, type: callActivity, calls: X1, name: Propagate and publish}
- {id: end, type: end, name: Shape in force}
flows:
- {from: start, to: write}
- {from: write, to: pattern}
- {from: pattern, to: portable}
- {from: portable, to: conforms, condition: "yes"}
- {from: portable, to: demote, condition: "no"}
- {from: demote, to: record}
- {from: conforms, to: test}
- {from: test, to: record}
- {from: record, to: release}
- {from: release, to: end}
# ---------------------------------------------------------------------- D1
- id: D1
family: Delete
name: Deprecate a term
scope: class | relation | attribute | annotation | individual
trigger: A term is superseded or no longer needed.
inputs: [the term, its replacement if any]
outputs: [a deprecated stub, a published version]
note: >
The count at step two is informative, not a condition: a term may be
deprecated whether or not it is instantiated. It becomes a condition only
in D2.
flow:
- {id: start, type: start, name: Term superseded}
- {id: replacement, type: userTask, name: Identify the replacement, or record that there is none}
- {id: count, type: scriptTask, name: Count the instances for information, rules: [EV-015]}
- {id: mark, type: scriptTask, name: Mark deprecated and declare the replacement, rules: [EV-001]}
- {id: strip, type: scriptTask, name: Strip the stub of every edge, rules: [EV-001]}
- {id: label, type: scriptTask, name: Remove any mention of state from the label, rules: [TN-020]}
- {id: release, type: callActivity, calls: X1, name: Propagate and publish}
- {id: end, type: end, name: Term deprecated}
flows:
- {from: start, to: replacement}
- {from: replacement, to: count}
- {from: count, to: mark}
- {from: mark, to: strip}
- {from: strip, to: label}
- {from: label, to: release}
- {from: release, to: end}
# ---------------------------------------------------------------------- D2
- id: D2
family: Delete
name: Withdraw a term permanently
scope: class | relation | attribute | annotation | individual
trigger: A deprecated term is to be removed from the vocabulary.
inputs: [the deprecated term]
outputs: [a vocabulary without the term, a proof of non-instantiation, a published version]
note: >
The reference cleaning step is what prevents phantom nodes: a subject
removed while its identifier is still cited elsewhere is reconstructed by
inference, present in traversals and absent from every control. It is a step
of the procedure, not a check at the end of a script.
flow:
- {id: start, type: start, name: Withdrawal requested}
- {id: count, type: scriptTask, name: Run the counting query, rules: [EV-002, EV-015]}
- {id: instantiated, type: gateway, name: "Any instance found?"}
- {id: keep, type: end, name: Kept deprecated}
- {id: proof, type: scriptTask, name: Attach the proof to the commit, rules: [EV-002, EV-015]}
- {id: remove, type: scriptTask, name: Remove the subject block}
- {id: refs, type: scriptTask, name: Remove every reference naming the subject, rules: [EV-003]}
- {id: check_voc, type: callActivity, calls: R1, name: Verify the vocabulary}
- {id: check_abox, type: callActivity, calls: R2, name: Validate the instances}
- {id: release, type: callActivity, calls: X1, name: Propagate and publish}
- {id: end, type: end, name: Term withdrawn}
flows:
- {from: start, to: count}
- {from: count, to: instantiated}
- {from: instantiated, to: keep, condition: "yes"}
- {from: instantiated, to: proof, condition: "no"}
- {from: proof, to: remove}
- {from: remove, to: refs}
- {from: refs, to: check_voc}
- {from: check_voc, to: check_abox}
- {from: check_abox, to: release}
- {from: release, to: end}
# ---------------------------------------------------------------------- R1
- id: R1
family: Verify
name: Verify the vocabulary against the rulebook
scope: The whole vocabulary, or the subset touched by a change.
trigger: A term has been created, changed or withdrawn, or a release is prepared.
inputs: [the vocabulary, the rulebook source]
outputs: [a conformance report]
note: >
Two tiers run in sequence, not in parallel: the script settles everything
mechanisable, and a person answers only for the rules no script can judge.
Reversing the order wastes review time on findings the script would have
caught.
flow:
- {id: start, type: start, name: Verification requested}
- {id: script, type: scriptTask, name: "Run the naming and declaration checks", rules: [TN-001, TN-002, TN-003, TN-004, TN-005, TN-006, TN-007, TN-009, TN-010, TN-011, TN-012, TN-013, TN-014, TN-015, TN-017, TN-018, TN-019, TN-020, TN-021, TN-023, TN-025, TN-026, TN-027, TN-028, EV-014]}
- {id: mechanised, type: gateway, name: "Any mechanised violation?"}
- {id: report_fail, type: end, name: Report returned with violations}
- {id: human, type: userTask, name: "Review the rules no script can judge", rules: [TN-008, TN-016, TN-022, TN-024]}
- {id: judged, type: gateway, name: "Reviewer raises an issue?"}
- {id: report_ok, type: end, name: Vocabulary conforms}
flows:
- {from: start, to: script}
- {from: script, to: mechanised}
- {from: mechanised, to: report_fail, condition: "at least one"}
- {from: mechanised, to: human, condition: none}
- {from: human, to: judged}
- {from: judged, to: report_fail, condition: "yes"}
- {from: judged, to: report_ok, condition: "no"}
# ---------------------------------------------------------------------- R2
- id: R2
family: Verify
name: Validate instances against the shapes
scope: Any instance graph in the validation perimeter.
trigger: Instances have changed, shapes have changed, or a release is prepared.
inputs: [the instance graph, the shapes, the ontology]
outputs: [a validation report]
note: >
The version check comes first and aborts rather than warns. Validating
against shapes that target another version of the ontology does not fail
loudly: it returns a long list of violations that reads exactly like a
regression of the model.
flow:
- {id: start, type: start, name: Validation requested}
- {id: conforms, type: scriptTask, name: "Compare the declared target version with the ontology version", rules: [EV-010]}
- {id: match, type: gateway, name: "Versions match?"}
- {id: abort, type: end, name: Aborted on version mismatch}
- {id: run, type: scriptTask, name: Run the shape validation}
- {id: violations, type: gateway, name: "Any violation?"}
- {id: fail, type: end, name: Report returned with violations}
- {id: ok, type: end, name: Instances conform}
flows:
- {from: start, to: conforms}
- {from: conforms, to: match}
- {from: match, to: abort, condition: "no"}
- {from: match, to: run, condition: "yes"}
- {from: run, to: violations}
- {from: violations, to: fail, condition: "at least one"}
- {from: violations, to: ok, condition: none}
# ---------------------------------------------------------------------- R3
- id: R3
family: Verify
name: Audit what the shapes cannot see
scope: The whole vocabulary and its source file.
trigger: Periodic audit, or before a version is published.
inputs: [the vocabulary, its source file]
outputs: [a shortlist of suspected duplicates, a list of duplicated blocks]
note: >
Shape validation reads a graph, not a file and not meaning. Two identifiers
standing for the same notion produce two individually valid graphs; a
duplicated block of text produces identical triples and no complaint. This
procedure is the tier those rules fall to, and its output is a shortlist for
a person rather than a verdict.
flow:
- {id: start, type: start, name: Audit requested}
- {id: index, type: scriptTask, name: "Build the normalised label index", rules: [EV-012]}
- {id: hash, type: scriptTask, name: "Hash every normalised subject block", rules: [EV-012]}
- {id: shortlist, type: gateway, name: "Any candidate found?"}
- {id: clean, type: end, name: Nothing to arbitrate}
- {id: review, type: userTask, name: Arbitrate each candidate}
- {id: act, type: gateway, name: "Duplication confirmed?"}
- {id: merge, type: end, name: Referred to the merge procedure}
- {id: dismissed, type: end, name: Candidates dismissed}
flows:
- {from: start, to: index}
- {from: index, to: hash}
- {from: hash, to: shortlist}
- {from: shortlist, to: clean, condition: none}
- {from: shortlist, to: review, condition: "at least one"}
- {from: review, to: act}
- {from: act, to: merge, condition: "yes"}
- {from: act, to: dismissed, condition: "no"}
+3
View File
@@ -0,0 +1,3 @@
PyYAML>=6.0,<7
openpyxl>=3.1,<3.2
File diff suppressed because it is too large Load Diff
+2278 -1159
View File
File diff suppressed because it is too large Load Diff
+792 -743
View File
File diff suppressed because it is too large Load Diff
+14 -1
View File
@@ -95,7 +95,7 @@ NEW_CONCEPTS = [
# ------------------------------------------------------------------- new object # ------------------------------------------------------------------- new object
NEW_OBJECTS = [ 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: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: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]"), "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 continue
block = m.group(1) 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: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 clause = ' pr:aboutConcept %s ;\n' % about
if uses: if uses:
clause += ' pr:usesConcept %s ;\n' % " , ".join(uses) clause += ' pr:usesConcept %s ;\n' % " , ".join(uses)
@@ -244,6 +250,13 @@ def main():
for line in report: for line in report:
print(" " + line) print(" " + line)
print("=" * W) 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'(?<![\w:])%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"))
bo = len(re.findall(r'a pr:BusinessObject', text)) bo = len(re.findall(r'a pr:BusinessObject', text))
bc = len(re.findall(r'a pr:BusinessConcept', text)) bc = len(re.findall(r'a pr:BusinessConcept', text))
print(" Business Objects %d | Business Concepts %d | composedOf left %d" print(" Business Objects %d | Business Concepts %d | composedOf left %d"
+11 -8
View File
@@ -71,14 +71,14 @@ DE_CONCEPT_EXACT = {
"DE-04.01-0006": "ex:BC_04_01_002", # Outlet Type -> Outlet "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-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-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-0008": "ex:BC_04_03_001", # Country Code -> Country
"DE-04.01-0009": "ex:BC_04_01_003", # Country Name -> 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-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-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.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-0001": "ex:BC_16_03_001", # Marketing Entity -> Marketing Entity
"DE-16.02-0002": "ex:BC_16_01_004", # Fiscal Period Code -> Fiscal Period "DE-16.02-0002": "ex:BC_16_02_001", # Fiscal Period Code -> Fiscal Period
"DE-16.02-0003": "ex:BC_16_01_004", # Fiscal Quarter Code -> Fiscal Period "DE-16.02-0003": "ex:BC_16_02_001", # Fiscal Quarter Code -> Fiscal Period
} }
DE_CONCEPT_PREFIX = [ DE_CONCEPT_PREFIX = [
("DE-10.01", "ex:BC_10_01_001"), # product hierarchy -> Product ("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 ------------------------------------ # ---- concepts the XOR forces us to name ------------------------------------
NEW_CONCEPTS = [ 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, " "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 " "identified by its ISO 3166 code. Distinct from Customer and Outlet: it is where "
"they operate, not what they are."), "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, " "An organisational unit of the group holding commercial responsibility for a market, "
"and the level at which financial results are consolidated."), "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. " "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."), "Distinct from Calendar Date: the fiscal frame does not align with the Gregorian one."),
] ]
+202
View File
@@ -0,0 +1,202 @@
#!/usr/bin/env python3
"""
SODH - data fixes v1.2
=======================
Four corrections found while reviewing the viewer.
USAGE
python3 scripts/apply_fixes_v1_2.py # dry run
python3 scripts/apply_fixes_v1_2.py --apply # rewrite, .bak kept
1. DATA DOMAIN OWNERS
Six named owners added, plus the hasDomainOwner link. Ownership was
readable only through the DGL until now, which conflates two roles: the
owner is accountable for the domain, the DGL runs its governance.
2. hasSource CARRIED TWO MEANINGS
On a Data Element it named the system of origin (MDM Product, Group
Finance). On a Data Object it named the physical location (SODH Gold /
F_SELL_OUT). Same property, two concepts -- the defect we removed from
composedOf, still present one layer down. Split: physicalName holds the
relation name, hasSource holds the system. This is also what makes the
physical layer derivable: F_SELL_OUT plus 132 column names.
3. METRIC UNITS ARE PLURAL
Five mother metrics compute elements in several units (9L and L, EUR and
LC and USD). A metric states every unit its variants are expressed in; a
Data Element states exactly one. Recomputed from the elements rather than
asserted, so the two can never drift.
4. Sell Out Distribution -> Retail Distribution, matching the concept: sell
out counts units sold, distribution measures presence in store.
"""
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
OWNERS = [
("ex:DDO_DD_06", "Ludovic Cottier", "ex:DD_06"),
("ex:DDO_DD_04", "David Carpio", "ex:DD_04"),
("ex:DDO_DD_10", "Alberto Lupano", "ex:DD_10"),
("ex:DDO_DD_05", "David Carpio", "ex:DD_05"),
("ex:DDO_DD_16", "Julien Soisson", "ex:DD_16"),
("ex:DDO_DD_21", "Vincent Meunier", "ex:DD_21"),
]
RENAME = {"ex:BO_06_01_003": ("Sell Out Distribution", "Retail Distribution")}
# system of origin, once the physical location is taken out of it
SOURCE_FIX = {
"Snowflake (SODH Gold)": "SODH Gold",
"SODH Gold (flag)": "SODH Gold",
}
def clauses(body):
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):
"""Clause-level set: survives mid-line properties and semicolons in literals."""
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 clauses(m.group(2))
if c.strip() and not re.match(r'\s*%s\s' % re.escape(prop), c)]
if value is not None:
kept.insert(0, "\n %s %s " % (prop, value))
body = " ;".join(kept).rstrip() + "\n "
return text[:m.start()] + m.group(1) + body + m.group(3) + text[m.end():], True
def get_prop(text, subject, prop):
m = re.search(r'^%s a pr:\w+ ;(.*?)\.\s*\n' % re.escape(subject), text, re.S | re.M)
if not m:
return None
for c in clauses(m.group(1)):
mm = re.match(r'\s*%s\s+(.*)$' % re.escape(prop), c, re.S)
if mm:
return mm.group(1).strip()
return None
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 = []
# ---- 1. domain owners -------------------------------------------------
todo = [o for o in OWNERS if not re.search(r'^%s a ' % re.escape(o[0]), text, re.M)]
if todo:
blocks = "\n".join('%s a pr:DataDomainOwner ; pr:hasName "%s" .' % (i, n)
for i, n, _ in todo)
anchor = re.search(r'^ex:ST_\w+ a pr:DataSteward', text, re.M)
pos = anchor.start() if anchor else 0
text = (text[:pos]
+ "# --- data domain owners ----------------------------------------------\n"
+ "# Accountable for the domain. Distinct from the DGL, who runs its\n"
+ "# governance: two roles that hasDGL alone was conflating.\n"
+ blocks + "\n\n" + text[pos:])
for i, n, dd in OWNERS:
text, _ = set_prop(text, dd, "pr:hasDomainOwner", i)
report.append("%d data domain owners named and linked" % len(OWNERS))
# ---- 2. physical location out of hasSource ----------------------------
n_do = 0
for m in list(re.finditer(r'^(ex:DO_\w+) a pr:DataObject ;', text, re.M)):
subj = m.group(1)
src = get_prop(text, subj, "pr:hasSource")
if src and "/" in src:
system, relation = [p.strip().strip('"') for p in src.strip('"').split("/", 1)]
text, _ = set_prop(text, subj, "pr:hasSource", '"%s"' % system)
text, _ = set_prop(text, subj, "pr:physicalName", '"%s"' % relation)
n_do += 1
report.append("%d data objects: physical relation moved out of hasSource" % n_do)
n_de = 0
for old, new in SOURCE_FIX.items():
text, k = re.subn(r'pr:hasSource "%s"' % re.escape(old),
'pr:hasSource "%s"' % new, text)
n_de += k
report.append("%d source labels normalised to the system of origin" % n_de)
# ---- 3. metric units, recomputed from the elements ---------------------
de_unit = {}
for m in re.finditer(r'^(ex:DE_\w+) a pr:DataElement ;(.*?)\.\s*\n', text, re.S | re.M):
u = re.search(r'pr:hasUnit\s+"([^"]*)"', m.group(2))
if u:
de_unit[m.group(1)] = u.group(1)
wired = {}
for m in re.finditer(r'(ex:M_\w+)\s+pr:computedBy(.*?)\.', text, re.S):
wired[m.group(1)] = re.findall(r'ex:DE_\w+', m.group(2))
n_m = 0
for metric, des in wired.items():
units = sorted({de_unit[d] for d in des if d in de_unit and de_unit[d] != "-"})
if not units:
continue
value = " , ".join('"%s"' % u for u in units)
before = get_prop(text, metric, "pr:hasUnit")
text, ok = set_prop(text, metric, "pr:hasUnit", value)
if ok and before != value:
n_m += 1
report.append("%d metrics now list every unit their elements use" % n_m)
# ---- 4. rename --------------------------------------------------------
for subj, (old, new) in RENAME.items():
if get_prop(text, subj, "pr:hasName") == '"%s"' % old:
text, _ = set_prop(text, subj, "pr:hasName", '"%s"' % new)
report.append("renamed %s -> %s" % (old, new))
# ---- report -----------------------------------------------------------
print()
print("SODH DATA FIXES v1.2 %s" % ("APPLY" if apply_changes else "DRY RUN"))
print("=" * W)
for line in report:
print(" " + line)
print("=" * W)
multi = len(re.findall(r'pr:hasUnit\s+"[^"]*"\s*,', text))
phys = len(re.findall(r'^ex:DO_\w+ a pr:DataObject ;(?:(?!\.\s*\n).)*?physicalName',
text, re.S | re.M))
doubled = []
for m in re.finditer(r'^(ex:\w+) a pr:\w+ ;(.*?)\.\s*\n', text, re.S | re.M):
for p in ("pr:hasName", "pr:hasSource", "pr:physicalName", "pr:hasDomainOwner"):
if len(re.findall(r'(?<![\w:])%s\s' % re.escape(p), m.group(2))) > 1:
doubled.append("%s/%s" % (m.group(1), p))
print(" metrics with several units %d | data objects with a physical name %d"
% (multi, phys))
print(" duplicated single-valued properties: %s"
% (", ".join(doubled) if doubled else "none"))
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()
+261
View File
@@ -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()
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+207
View File
@@ -0,0 +1,207 @@
#!/usr/bin/env python3
"""
SODH - migration v1.1 -> v1.2
==============================
USAGE
python3 scripts/migrate_instances_v1_2.py # dry run
python3 scripts/migrate_instances_v1_2.py --apply # rewrite, .bak kept
WHAT IT DOES
1. physicalizedIn -> storedIn everywhere (no instances yet, done for
completeness so the term never appears again).
2. hasSource removed from every Business Concept. A concept has no source
of truth: its authority is its owning domain, its definition belongs in
the glossary. Asking which system defines it has no answer, because
every system that uses it would be a candidate.
3. hasSource on Metrics recomputed from the elements that implement them.
It said "SODH back-doc v0.7" -- the provenance of the record, not the
source of the data. Asserted rather than derived, by exception, because
"where can I get this metric" is a first-order question and the
derivation chain will have gaps until harvesting; a shape then checks
the assertion against the elements.
4. The SODH Data Product, its contract and its two interfaces.
WHY THE PRODUCT OWNER IS EMPTY
Nobody is appointed. The field stays absent and the shape reports a
warning rather than a violation. Inventing a name to satisfy a blocking
rule is exactly how BR-004 produced padding in v0.6.
"""
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
PRODUCT = "ex:DP_06_01_001"
CONTRACT = "ex:DC_06_01_001"
IFACE_WH = "ex:DI_06_01_001"
IFACE_FILE = "ex:DI_06_01_002"
def clauses(body):
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):
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 clauses(m.group(2))
if c.strip() and not re.match(r'\s*%s\s' % re.escape(prop), c)]
if value is not None:
kept.insert(0, "\n %s %s " % (prop, value))
body = " ;".join(kept).rstrip() + "\n "
return text[:m.start()] + m.group(1) + body + m.group(3) + text[m.end():], True
def get_prop(text, subject, prop):
m = re.search(r'^%s a pr:\w+ ;(.*?)\.\s*\n' % re.escape(subject), text, re.S | re.M)
if not m:
return None
for c in clauses(m.group(1)):
mm = re.match(r'\s*%s\s+(.*)$' % re.escape(prop), c, re.S)
if mm:
return mm.group(1).strip()
return None
def product_blocks(objects):
packages = " ,\n ".join(objects)
return """# --- data product ------------------------------------------------------
# The unit of delivery: what carries a version, an owner, a changelog and a
# list of consumers to warn. It has no single technical counterpart on purpose
# -- rename the schema and the product is unchanged; change its SLA and it is
# not. Its physical footprint is derivable through packages, never asserted.
%s a pr:DataProduct ;
pr:hasIdentifier "DP-06.01-001" ; pr:hasName "Sell Out Data Hub - Gold" ;
pr:owningDomain ex:DD_06 ;
pr:operatedBy ex:SD_06_01 ;
pr:packages %s ;
pr:governedBy %s ;
pr:exposes %s , %s ;
pr:hasStatus "DRAFT" ; pr:hasVersion "1.2" ; pr:hasSource "SODH Gold" .
%s a pr:DataContract ;
pr:hasIdentifier "DC-06.01-001" ; pr:hasName "Sell Out Data Hub - Gold service commitment" ;
pr:owningDomain ex:DD_06 ;
pr:hasBusinessRule "Refreshed weekly, available Tuesday 06:00 CET. Panel coverage published with every load. Schema stable within a minor version; 60 days notice on any breaking change. Support through the Sales Performance data team." ;
pr:hasStatus "DRAFT" ; pr:hasVersion "1.2" .
%s a pr:DataInterface ;
pr:hasIdentifier "DI-06.01-001" ; pr:hasName "SODH Gold consumption schema" ;
pr:owningDomain ex:DD_06 ;
pr:hasBusinessDefinition "Read access to the Gold layer through Snowflake grants. The channel BI tools and analysts consume."@en ;
pr:hasStatus "DRAFT" ; pr:hasVersion "1.2" ; pr:hasSource "SODH Gold" .
%s a pr:DataInterface ;
pr:hasIdentifier "DI-06.01-002" ; pr:hasName "Panel partner weekly extract" ;
pr:owningDomain ex:DD_06 ;
pr:hasBusinessDefinition "File extract delivered to the panel provider each week. Outside Snowflake, and the boundary where lineage leaves the warehouse."@en ;
pr:hasStatus "DRAFT" ; pr:hasVersion "1.2" ; pr:hasSource "SODH Gold" .
""" % (PRODUCT, packages, CONTRACT, IFACE_WH, IFACE_FILE,
CONTRACT, IFACE_WH, IFACE_FILE)
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 = []
# 1. term rename
text, n = re.subn(r'\bpr:physicalizedIn\b', 'pr:storedIn', text)
report.append("physicalizedIn -> storedIn: %d occurrence(s)" % n)
# 2. no source of truth on a concept
n_bc = 0
for m in list(re.finditer(r'^(ex:BC_\w+) a pr:BusinessConcept ;', text, re.M)):
if get_prop(text, m.group(1), "pr:hasSource") is not None:
text, _ = set_prop(text, m.group(1), "pr:hasSource", None)
n_bc += 1
report.append("hasSource removed from %d business concepts" % n_bc)
# 3. metric source of truth, from the elements that implement it
de_src = {}
for m in re.finditer(r'^(ex:DE_\w+) a pr:DataElement ;(.*?)\.\s*\n', text, re.S | re.M):
v = re.search(r'pr:hasSource\s+"([^"]*)"', m.group(2))
if v:
de_src[m.group(1)] = v.group(1)
wired = {}
for m in re.finditer(r'(ex:M_\w+)\s+pr:computedBy(.*?)\.', text, re.S):
wired[m.group(1)] = re.findall(r'ex:DE_\w+', m.group(2))
n_m, mixed = 0, []
for metric, des in wired.items():
srcs = sorted({de_src[d] for d in des if d in de_src})
if not srcs:
continue
if len(srcs) > 1:
mixed.append(metric)
text, ok = set_prop(text, metric, "pr:hasSource",
" , ".join('"%s"' % x for x in srcs))
n_m += ok
report.append("source of truth recomputed on %d metrics%s"
% (n_m, "" if not mixed else
" (%d blend several systems)" % len(mixed)))
# 4. the data product
if not re.search(r'^%s a ' % re.escape(PRODUCT), text, re.M):
objs = re.findall(r'^(ex:DO_\w+) a pr:DataObject ;', text, re.M)
text = text.rstrip() + "\n\n\n" + product_blocks(objs)
report.append("data product created, packaging %d data objects" % len(objs))
else:
report.append("data product already present, skipped")
# ---- report
print()
print("SODH MIGRATION v1.1 -> v1.2 %s" % ("APPLY" if apply_changes else "DRY RUN"))
print("=" * W)
for line in report:
print(" " + line)
print("=" * W)
bc_src = len(re.findall(r'^ex:BC_\w+ a pr:BusinessConcept ;(?:(?!\.\s*\n).)*?hasSource',
text, re.S | re.M))
defined = set(re.findall(r'^(ex:\w+) a ', text, re.M))
dangling = sorted(set(re.findall(r'\bex:\w+', text)) - defined)
doubled = []
for m in re.finditer(r'^(ex:\w+) a pr:\w+ ;(.*?)\.\s*\n', text, re.S | re.M):
for p in ("pr:hasName", "pr:hasSource", "pr:operatedBy", "pr:governedBy"):
if len(re.findall(r'(?<![\w:])%s\s' % re.escape(p), m.group(2))) > 1:
doubled.append("%s/%s" % (m.group(1), p))
print(" concepts still carrying a source: %d (must be 0)" % bc_src)
print(" physicalizedIn left: %d (must be 0)"
% len(re.findall(r'pr:physicalizedIn', text)))
print(" dangling references: %s" % (", ".join(dangling) if dangling else "none"))
print(" duplicated single-valued properties: %s"
% (", ".join(doubled) if doubled else "none"))
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()
+148
View File
@@ -0,0 +1,148 @@
#!/usr/bin/env python3
"""
SODH - migration v1.2 -> v1.3
==============================
USAGE
python3 scripts/migrate_instances_v1_3.py # dry run
python3 scripts/migrate_instances_v1_3.py --apply # rewrite, .bak kept
1. references -> qualifiedBy. The name now says why the link exists rather
than how it is implemented.
2. hasSource removed from every Business Object. Same argument as for the
Business Concepts in v1.2, and the values showed why it was needed: across
the 15 objects the property held physical locations ("SODH Gold /
F_SELL_OUT"), migration notes ("MDM Product -> SODH [concepts TBC by
DD-10]") and arbitration remarks ("DGO proposal, pending ratification"),
all under one name. A Business Object is a governance grouping; its data
lives in the Data Objects that represent it, and those carry the system.
3. Short labels where the long name is unwieldy on screen. The Data Product
was called "Sell Out Data Hub - Gold" on its own card and "SODH Gold"
everywhere else, because the viewers each kept a display dictionary.
4. Instance versions aligned on the model version. Business Objects were
still stamped 0.6 while the model had moved to 1.3.
"""
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
SHORT_LABELS = {
"ex:DP_06_01_001": "SODH Gold",
"ex:DC_06_01_001": "SODH Gold contract",
"ex:DI_06_01_001": "Consumption schema",
"ex:DI_06_01_002": "Panel extract",
"ex:DO_06_01_001": "Sell Out Volume",
"ex:DO_06_01_003": "Sell Out Distribution",
"ex:DO_10_01_001": "Product",
"ex:DO_04_01_001": "Customer",
"ex:DO_16_01_001": "Currency",
"ex:DO_21_01_001": "Calendar",
}
MODEL_VERSION = "1.3"
def clauses(body):
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):
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 clauses(m.group(2))
if c.strip() and not re.match(r'\s*%s\s' % re.escape(prop), c)]
if value is not None:
kept.insert(0, "\n %s %s " % (prop, value))
body = " ;".join(kept).rstrip() + "\n "
return text[:m.start()] + m.group(1) + body + m.group(3) + text[m.end():], True
def has_prop(text, subject, prop):
m = re.search(r'^%s a pr:\w+ ;(.*?)\.\s*\n' % re.escape(subject), text, re.S | re.M)
return bool(m) and any(re.match(r'\s*%s\s' % re.escape(prop), c)
for c in clauses(m.group(1)))
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 = []
text, n = re.subn(r'\bpr:references\b', 'pr:qualifiedBy', text)
report.append("references -> qualifiedBy: %d occurrence(s)" % n)
n_bo = 0
for m in list(re.finditer(r'^(ex:BO_\w+) a pr:BusinessObject ;', text, re.M)):
if has_prop(text, m.group(1), "pr:hasSource"):
text, _ = set_prop(text, m.group(1), "pr:hasSource", None)
n_bo += 1
report.append("hasSource removed from %d business objects" % n_bo)
n_sl = 0
for subj, label in SHORT_LABELS.items():
if re.search(r'^%s a ' % re.escape(subj), text, re.M):
text, ok = set_prop(text, subj, "pr:hasShortLabel", '"%s"' % label)
n_sl += ok
report.append("short label set on %d objects" % n_sl)
n_v = 0
for m in list(re.finditer(r'^(ex:\w+) a pr:\w+ ;', text, re.M)):
if has_prop(text, m.group(1), "pr:hasVersion"):
text, ok = set_prop(text, m.group(1), "pr:hasVersion",
'"%s"' % MODEL_VERSION)
n_v += ok
report.append("version aligned on %s for %d objects" % (MODEL_VERSION, n_v))
print()
print("SODH MIGRATION v1.2 -> v1.3 %s" % ("APPLY" if apply_changes else "DRY RUN"))
print("=" * W)
for line in report:
print(" " + line)
print("=" * W)
left_bo = len([1 for m in re.finditer(r'^(ex:BO_\w+) a pr:BusinessObject ;', text, re.M)
if has_prop(text, m.group(1), "pr:hasSource")])
doubled = []
for m in re.finditer(r'^(ex:\w+) a pr:\w+ ;(.*?)\.\s*\n', text, re.S | re.M):
for p in ("pr:hasName", "pr:hasShortLabel", "pr:hasVersion", "pr:hasSource"):
if len(re.findall(r'(?<![\w:])%s\s' % re.escape(p), m.group(2))) > 1:
doubled.append("%s/%s" % (m.group(1), p))
print(" business objects still carrying a source: %d (must be 0)" % left_bo)
print(" references left: %d (must be 0)" % len(re.findall(r'pr:references', text)))
print(" duplicated single-valued properties: %s"
% (", ".join(doubled) if doubled else "none"))
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()
+176
View File
@@ -0,0 +1,176 @@
#!/usr/bin/env python3
"""
SODH - migration v1.3 -> v1.4
==============================
USAGE
python3 scripts/migrate_instances_v1_4.py # dry run
python3 scripts/migrate_instances_v1_4.py --apply # rewrite, .bak kept
1. SYSTEMS BECOME NODES. hasSource held a string: "SODH Gold", "MDM Product",
"Group Finance". Six systems are created and sourcedFrom points at them.
"Which objects depend on MDM Product?" was unanswerable while MDM Product
was text.
The systems are seeded here by hand, once. Everything below them --
databases, schemas, tables, columns -- is meant to be harvested, and this
script does not invent any of it.
2. Three hasSource values were not systems at all but migration notes ("DGO
proposal, split of a compound sub-domain"). They are dropped rather than
turned into a system: provenance of a record is not a property of the thing
the record describes.
3. materializedAs -> storedIn on the Data Objects. Nothing carries it yet,
done for completeness so the term never reappears.
"""
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
# (iri, identifier, name, short label, system type, the string it replaces)
SYSTEMS = [
("ex:SYS_SODH_GOLD", "SYS-001", "Sell Out Data Hub - Gold layer", "SODH Gold",
"pr:DataPlatform", "SODH Gold"),
("ex:SYS_MDM_PRODUCT", "SYS-002", "Master Data Management - Product", "MDM Product",
"pr:MasterDataSystem", "MDM Product"),
("ex:SYS_MDM_CUSTOMER", "SYS-003", "Master Data Management - Customer", "MDM Customer",
"pr:MasterDataSystem", "MDM Customer"),
("ex:SYS_GROUP_FINANCE", "SYS-004", "Group Finance consolidation", "Group Finance",
"pr:SourceApplication", "Group Finance"),
("ex:SYS_CORPORATE_CALENDAR", "SYS-005", "Corporate standard calendar", "Standard Calendar",
"pr:SourceApplication", "Standard Calendar"),
]
# strings that were never a system: provenance notes about the record itself
NOT_A_SYSTEM = ["DGO proposal"]
def clauses(body):
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):
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 clauses(m.group(2))
if c.strip() and not re.match(r'\s*%s\s' % re.escape(prop), c)]
if value is not None:
kept.insert(0, "\n %s %s " % (prop, value))
body = " ;".join(kept).rstrip() + "\n "
return text[:m.start()] + m.group(1) + body + m.group(3) + text[m.end():], True
def get_source(text, subject):
m = re.search(r'^%s a pr:\w+ ;(.*?)\.\s*\n' % re.escape(subject), text, re.S | re.M)
if not m:
return None
for c in clauses(m.group(1)):
mm = re.match(r'\s*pr:hasSource\s+"([^"]*)"', c)
if mm:
return mm.group(1)
return None
def system_blocks(todo):
head = ("# --- systems -----------------------------------------------------------\n"
"# Seeded by hand, once. A system was a string until now, which is why\n"
"# 'what depends on MDM Product' had no answer. Everything below a system --\n"
"# databases, schemas, tables, columns -- is meant to be harvested, and\n"
"# nothing here invents any of it.\n\n")
out = []
for iri, ident, name, short, stype, _ in todo:
out.append('%s a pr:System ;\n'
' pr:hasIdentifier "%s" ; pr:hasName "%s" ;\n'
' pr:hasShortLabel "%s" ;\n'
' pr:hasSystemType %s ;\n'
' pr:hasStatus "DRAFT" ; pr:hasVersion "1.4" .'
% (iri, ident, name, short, stype))
return head + "\n".join(out) + "\n"
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 = []
todo = [sy for sy in SYSTEMS if not re.search(r'^%s a ' % re.escape(sy[0]), text, re.M)]
if todo:
anchor = re.search(r'^ex:DD_\w+ a pr:DataDomain', text, re.M)
pos = anchor.start() if anchor else 0
text = text[:pos] + system_blocks(todo) + "\n" + text[pos:]
report.append("%d systems created%s" % (len(todo), "" if todo else " (already present)"))
by_string = {sy[5]: sy[0] for sy in SYSTEMS}
n_ok = n_drop = 0
for m in list(re.finditer(r'^(ex:\w+) a pr:\w+ ;', text, re.M)):
subj = m.group(1)
src = get_source(text, subj)
if src is None:
continue
if src in by_string:
text, _ = set_prop(text, subj, "pr:hasSource", None)
text, _ = set_prop(text, subj, "pr:sourcedFrom", by_string[src])
n_ok += 1
elif any(k in src for k in NOT_A_SYSTEM):
text, _ = set_prop(text, subj, "pr:hasSource", None)
n_drop += 1
report.append("sourcedFrom set on %d objects" % n_ok)
report.append("%d provenance notes dropped rather than turned into a system" % n_drop)
text, n = re.subn(r'\bpr:materializedAs\b', 'pr:storedIn', text)
report.append("materializedAs -> storedIn: %d occurrence(s)" % n)
print()
print("SODH MIGRATION v1.3 -> v1.4 %s" % ("APPLY" if apply_changes else "DRY RUN"))
print("=" * W)
for line in report:
print(" " + line)
print("=" * W)
left = re.findall(r'pr:hasSource\s+"([^"]*)"', text)
defined = set(re.findall(r'^(ex:\w+) a ', text, re.M))
dangling = sorted(set(re.findall(r'\bex:\w+', text)) - defined)
doubled = []
for m in re.finditer(r'^(ex:\w+) a pr:\w+ ;(.*?)\.\s*\n', text, re.S | re.M):
for p in ("pr:sourcedFrom", "pr:hasName", "pr:hasShortLabel"):
if len(re.findall(r'(?<![\w:])%s\s' % re.escape(p), m.group(2))) > 1:
doubled.append("%s/%s" % (m.group(1), p))
print(" hasSource left: %d %s" % (len(left), sorted(set(left)) if left else "(must be 0)"))
print(" sourcedFrom references: %d" % len(re.findall(r'pr:sourcedFrom', text)))
print(" dangling references: %s" % (", ".join(dangling) if dangling else "none"))
print(" duplicated single-valued properties: %s"
% (", ".join(doubled) if doubled else "none"))
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()
+354 -411
View File
@@ -1,442 +1,385 @@
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix dcterms: <http://purl.org/dc/terms/> . @prefix dcterms: <http://purl.org/dc/terms/> .
@prefix pr: <https://ontology.pernod-ricard.com/metamodel/> . @prefix owl: <http://www.w3.org/2002/07/owl#> .
@prefix prs: <https://ontology.pernod-ricard.com/metamodel/shapes/> . @prefix pr: <https://ontology.pernod-ricard.com/metamodel/> .
@prefix prs: <https://ontology.pernod-ricard.com/metamodel/shapes/> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
################################################################# prs: dcterms:modified "2026-07-30"^^xsd:date ;
# PR DATA META MODEL - SHACL SHAPES v0.2
# Aligned with T-Box v1.1 and Rulebook v0.3.x
#
# WHAT CHANGED vs v0.1.1
# ----------------------
# 1. SPLIT BY PROVENANCE. v0.1.1 targeted pr:MetaModelObject and demanded an
# identifier, a name, an owner, an owning domain, a status and a version on
# everything. Applied to the 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
#################################################################
<https://ontology.pernod-ricard.com/metamodel/shapes/>
dcterms:title "PR Data Meta Model - SHACL Shapes" ; dcterms:title "PR Data Meta Model - SHACL Shapes" ;
dcterms:modified "2026-07-26"^^xsd:date ;
rdfs:comment "Executable form of the rulebook. Validated by scripts/run_shacl_validation.py." . rdfs:comment "Executable form of the rulebook. Validated by scripts/run_shacl_validation.py." .
prs:BIFieldShape a sh:NodeShape ;
################################################################# rdfs:comment "realizesMetric is a transitional control: asserted directly, with no rule written against the lineage chain. Making the assertion conditional on intermediate mappings existing would create confusion and discourage people from filling it in at all." ;
# IDENTITY AND LIFECYCLE -- curated objects only sh:property [ sh:class pr:Metric ;
################################################################# sh:maxCount 1 ;
sh:message "Which certified Metric this BI field implements. Comparing expressions across fields realizing one Metric is the harmonisation measure." ;
prs:CuratedObjectShape a sh:NodeShape ; sh:path pr:exposesMetric ;
sh:targetClass pr:CuratedObject ; sh:severity sh:Info ] ;
rdfs:comment "Applies to everything governance asserts. Observed objects are deliberately exempt: see ObservedObjectShape." ; sh:targetClass pr:BusinessIntelligenceField .
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 "A published object should carry a version." ] .
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." ;
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" .
}""" ] ;
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 }
}""" ] .
#################################################################
# 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:hasDomainOwner ; sh:maxCount 1 ;
sh:severity sh:Warning ;
sh:message "A Data Domain should name its owner." ] ;
sh:property [
sh:path pr:hasDGL ; sh:maxCount 1 ;
sh:severity sh:Warning ;
sh:message "A Data Domain should name its Data Governance Lead." ] ;
sh:property [
sh:path pr:hasActivationStatus ; sh:maxCount 1 ;
sh:severity sh:Info ] .
#################################################################
# BUSINESS OBJECT -- subject, perimeter, stewardship
#################################################################
prs:BusinessObjectShape a sh:NodeShape ;
sh:targetClass pr:BusinessObject ;
sh:property [
sh:path pr:belongsTo ; sh:class pr:SubDomain ;
sh:minCount 1 ; sh:maxCount 1 ;
sh:severity sh:Violation ;
sh:message "A Business Object belongs to exactly one Sub-Domain." ] ;
sh:property [
sh:path pr:aboutConcept ; sh:class pr:BusinessConcept ;
sh:minCount 1 ; sh:maxCount 1 ;
sh:severity sh:Violation ;
sh:message "BR-004 -- a Business Object is about exactly one Business Concept. Replaces the old 3..10 minimum, which was satisfied in v0.6 by attaching unrelated concepts. If the subject cannot be named, the object is not scoped." ] ;
sh:property [
sh:path pr:usesConcept ; sh:class pr:BusinessConcept ;
sh:severity sh:Info ;
sh:message "Other notions needed to read this object. Zero is a legitimate answer." ] ;
sh:property [
sh:path pr:monitoredBy ; sh:class pr:DataSteward ;
sh:minCount 1 ;
sh:severity sh:Warning ;
sh:message "The Business Object is where a steward is declared -- Data Objects and Data Elements inherit from here." ] ;
sh:property [
sh:path pr: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 ; prs:BusinessConceptShape a sh:NodeShape ;
sh:targetClass pr:BusinessConcept ;
rdfs:comment "Note what is NOT here: no requirement that a Concept be realized by any Data Element or measured by any Metric. The business layer is allowed to run ahead of the data. Coverage is a dashboard metric, not a rule." ; rdfs:comment "Note what is NOT here: no requirement that a Concept be realized by any Data Element or measured by any Metric. The business layer is allowed to run ahead of the data. Coverage is a dashboard metric, not a rule." ;
sh:property [ sh:datatype xsd:string ;
sh:maxCount 1 ;
sh:message "A Business Concept exists to carry meaning: it must hold exactly one business definition of at least 20 characters." ;
sh:minCount 1 ;
sh:minLength 20 ;
sh:path pr:businessDefinition ;
sh:severity sh:Violation ],
[ sh:maxCount 0 ;
sh:message "BR-003 -- the presence of a formula is what makes something a Metric. A Concept with a formula is a misclassified Metric." ;
sh:path pr:formula ;
sh:severity sh:Violation ],
[ sh:maxCount 0 ;
sh:message "A Business Concept has no system of truth. Its authority is its owning domain and its definition belongs in the glossary. Naming a system here would ask which of the systems that USE the concept defines it -- a question with no answer." ;
sh:path pr:sourcedFrom ;
sh:severity sh:Violation ] ;
sh:targetClass pr:BusinessConcept .
sh:property [ prs:BusinessObjectShape a sh:NodeShape ;
sh:path pr:hasBusinessDefinition ; sh:datatype xsd:string ; sh:property [ sh:class pr:DataSubDomain ;
sh:minCount 1 ; sh:maxCount 1 ; sh:minLength 20 ; sh:maxCount 1 ;
sh:severity sh:Violation ; sh:message "A Business Object belongs to exactly one Sub-Domain." ;
sh:message "A Business Concept exists to carry meaning: it must hold exactly one business definition of at least 20 characters." ] ; sh:minCount 1 ;
sh:path pr:belongsTo ;
sh:severity sh:Violation ],
[ sh:class pr:BusinessConcept ;
sh:maxCount 1 ;
sh:message "BR-004 -- a Business Object is about exactly one Business Concept. Replaces the old 3..10 minimum, which was satisfied in v0.6 by attaching unrelated concepts. If the subject cannot be named, the object is not scoped." ;
sh:minCount 1 ;
sh:path pr:isAbout ;
sh:severity sh:Violation ],
[ sh:class pr:BusinessConcept ;
sh:message "Other notions needed to read this object. Zero is a legitimate answer." ;
sh:path pr:usesConcept ;
sh:severity sh:Info ],
[ sh:class pr:DataSteward ;
sh:message "The Business Object is where a steward is declared -- Data Objects and Data Elements inherit from here." ;
sh:minCount 1 ;
sh:path pr:monitoredBy ;
sh:severity sh:Warning ],
[ sh:maxCount 0 ;
sh:message "A Business Object carries no definition of its own: meaning belongs to the Business Concept named by aboutConcept. Two definitions for one notion are two definitions free to diverge. MIGRATION: move the text to that Concept." ;
sh:path pr:businessDefinition ;
sh:severity sh:Violation ],
[ sh:maxCount 0 ;
sh:message "A Business Object has no system of truth. It is a governance grouping, not data: its data lives in the Data Objects that represent it, and those name the system. In v0.6 this property held a grab-bag on the 15 objects -- physical locations, migration notes, arbitration remarks -- all under one name." ;
sh:path pr:sourcedFrom ;
sh:severity sh:Violation ] ;
sh:targetClass pr:BusinessObject .
sh:property [ prs:CapturedObjectShape a sh:NodeShape ;
sh:path pr:hasFormula ; sh:maxCount 0 ; 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:severity sh:Violation ; sh:property [ sh:datatype xsd:string ;
sh:message "BR-003 -- the presence of a formula is what makes something a Metric. A Concept with a formula is a misclassified Metric." ] . sh:maxCount 1 ;
sh:message "A native immutable source id makes reconciliation idempotent and survives renames, which names do not." ;
sh:path pr:sourceIdentifier ;
sh:severity sh:Warning ],
[ sh:datatype xsd:dateTime ;
sh:maxCount 1 ;
sh:message "Every captured object records when it was harvested. Without it there is no way to tell a current fact from a stale one." ;
sh:minCount 1 ;
sh:path pr:harvestDate ;
sh:severity sh:Violation ],
[ sh:maxCount 0 ;
sh:message "Captured objects are not attributed directly -- ownership flows from the Data Element mapped to them." ;
sh:path pr:ownedByDomain ;
sh:severity sh:Warning ] ;
sh:targetClass pr:CapturedObject .
prs:DataDomainShape a sh:NodeShape ;
sh:property [ sh:maxCount 1 ;
sh:message "A Data Domain should name its owner." ;
sh:path pr:hasDomainOwner ;
sh:severity sh:Warning ],
[ sh:maxCount 1 ;
sh:message "A Data Domain should name its Data Governance Lead." ;
sh:path pr:hasGovernanceLead ;
sh:severity sh:Warning ],
[ sh:maxCount 1 ;
sh:path pr:activationStatus ;
sh:severity sh:Info ] ;
sh:targetClass pr:DataDomain .
prs:MetricShape a sh:NodeShape ; prs:DataElementMeaningShape a sh:NodeShape ;
sh:targetClass pr:Metric ; rdfs:comment "A Data Element reaches business meaning by exactly one of two routes: it represents a Business Concept (dimensional), or a Metric is computedBy it (measure). sh:xone rejects zero routes and two alike. A measure element must NOT also represent a Concept: the chain Metric -> measures -> Concept already carries the meaning, and carries more, since it names which metric it travels through." ;
sh:message "A Data Element must reach business meaning by exactly one route: represents a Business Concept (dimensional), or be computedBy a Metric (measure). Never both, never neither. A defined column with no business meaning is debt, not an acceptable state." ;
sh:severity sh:Violation ;
sh:targetClass pr:DataElement ;
sh:xone ( [ sh:property [ sh:maxCount 1 ;
sh:minCount 1 ;
sh:path pr:represents ] ] [ sh:property [ sh:minCount 1 ;
sh:path [ sh:inversePath pr:computedBy ] ] ] ) .
sh:property [ prs:DataElementShape a sh:NodeShape ;
sh:path pr:hasFormula ; sh:datatype xsd:string ; sh:property [ sh:datatype xsd:string ;
sh:minCount 1 ; sh:maxCount 1 ; sh:maxCount 1 ;
sh:severity sh:Violation ; sh:message "NR-009 -- the physical name is recorded verbatim and never altered." ;
sh:message "BR-003 -- a Metric is a measurement with a calculation rule. No formula, no Metric." ] ; sh:path pr:physicalName ;
sh:severity sh:Warning ],
[ sh:maxCount 1 ;
sh:path pr:fullyQualifiedName ;
sh:severity sh:Info ],
[ sh:maxCount 1 ;
sh:message "A Data Element carries exactly one unit. This is the counterpart of the Metric rule: the mother metric lists the units its variants use, each variant states the single unit it is actually expressed in. An element with two units is two elements." ;
sh:path pr:unit ;
sh:severity sh:Violation ],
[ sh:class pr:BusinessConcept ;
sh:maxCount 1 ;
sh:message "A Data Element represents at most one Business Concept. If a column carries two business notions, it should almost certainly be split -- so the case where a steward cannot choose is exactly the one worth looking at." ;
sh:path pr:represents ;
sh:severity sh:Violation ],
[ sh:maxCount 0 ;
sh:message "Stewardship is inherited from the Business Object, not declared here." ;
sh:path pr:monitoredBy ;
sh:severity sh:Warning ] ;
sh:targetClass pr:DataElement .
sh:property [ prs:DataInterfaceShape a sh:NodeShape ;
sh:path pr:hasUnit ; sh:minCount 1 ; sh:maxCount 1 ; sh:property [ sh:message "An interface should name what physically backs it. Unlike the old Data Asset test, this is verifiable: a grant, a share or an endpoint either exists or it does not." ;
sh:severity sh:Violation ; sh:minCount 1 ;
sh:message "A Metric states its unit." ] ; sh:path pr:servedBy ;
sh:severity sh:Warning ] ;
sh:property [ sh:targetClass pr:DataInterface .
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/>
SELECT $this WHERE {
$this pr:hasStatus "PUBLISHED" .
FILTER NOT EXISTS { $this pr:computedBy ?de }
}""" ] .
#################################################################
# DATA OBJECT -- the pivot, and grain
#################################################################
prs:DataObjectShape a sh:NodeShape ; prs:DataObjectShape a sh:NodeShape ;
sh:targetClass pr:DataObject ; sh:property [ sh:class pr:BusinessObject ;
sh:maxCount 1 ;
sh:message "BR-006 -- a Data Object represents exactly one Business Object. This is the pivot: the single junction between governance and technology." ;
sh:minCount 1 ;
sh:path pr:represents ;
sh:severity sh:Violation ],
[ sh:class pr:DataElement ;
sh:message "A Data Object holds at least one Data Element." ;
sh:minCount 1 ;
sh:path pr:hasElement ;
sh:severity sh:Violation ],
[ sh:class pr:DataElement ;
sh:message "State the grain: the elements whose combination makes a row unique. Checkable against the harvested primary key." ;
sh:path pr:hasGrainElement ;
sh:severity sh:Warning ],
[ sh:maxCount 0 ;
sh:message "Stewardship is declared on the Business Object and inherited through represents. Declaring it here allows two answers to the same question." ;
sh:path pr:monitoredBy ;
sh:severity sh:Warning ],
[ sh:class pr:System ;
sh:maxCount 1 ;
sh:message "Name the System holding the authoritative data, as a reference rather than a string. A string could not answer 'what depends on MDM Product'." ;
sh:path pr:sourcedFrom ;
sh:severity sh:Warning ],
[ sh:class pr:DataStructure ;
sh:message "Where the object actually sits: a table, a view, later a node label. The seam between what governance defines and what a harvester captures -- proposed by the reconciliation job, confirmed by a steward." ;
sh:path pr:storedIn ;
sh:severity sh:Warning ],
[ sh:maxCount 0 ;
sh:message "Deprecated in v1.4: storedIn now serves both granularities." ;
sh:path pr:materializedAs ;
sh:severity sh:Warning ] ;
sh:targetClass pr:DataObject .
sh:property [ prs:DataProductShape a sh:NodeShape ;
sh:path pr:represents ; sh:class pr:BusinessObject ; sh:property [ sh:class pr:DataSubDomain ;
sh:minCount 1 ; sh:maxCount 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:message "BR-006 -- a Data Object represents exactly one Business Object. This is the pivot: the single junction between governance and technology." ] ; sh:minCount 1 ;
sh:path pr:operatedBy ;
sh:severity sh:Violation ],
[ sh:class pr:DataProductOwner ;
sh:maxCount 1 ;
sh:message "A Data Product should name exactly one Product Owner. A warning rather than a block: the SODH owner is genuinely not appointed, and a blocking rule that cannot be satisfied truthfully is how BR-004 produced padding." ;
sh:path pr:hasProductOwner ;
sh:severity sh:Warning ],
[ sh:class pr:DataObject ;
sh:message "A Data Product packages at least one Data Object. The Business Objects it makes available are derived from these, which is why no direct link to a Business Object is needed." ;
sh:minCount 1 ;
sh:path pr:packages ;
sh:severity sh:Violation ],
[ sh:maxCount 1 ;
sh:message "A Data Product should be governed by a Data Contract." ;
sh:path pr:governedBy ;
sh:severity sh:Warning ],
[ sh:class pr:DataInterface ;
sh:message "The channels through which consumers actually get the data." ;
sh:path pr:exposes ;
sh:severity sh:Info ] ;
sh:targetClass pr:DataProduct .
sh:property [ prs:DataStructureShape a sh:NodeShape ;
sh:path pr:hasElement ; sh:class pr:DataElement ; sh:property [ sh:maxCount 1 ;
sh:minCount 1 ; sh:message "Optional on purpose: not every engine has a schema level." ;
sh:severity sh:Violation ; sh:path pr:isInSchema ;
sh:message "A Data Object holds at least one Data Element." ] ; sh:severity sh:Info ],
[ sh:maxCount 1 ;
sh:path pr:sourcedFrom ;
sh:severity sh:Info ] ;
sh:targetClass pr:DataStructure .
sh:property [ prs:DefinedObjectShape a sh:NodeShape ;
sh:path pr:hasGrainElement ; sh:class pr:DataElement ; rdfs:comment "Applies to everything governance writes. Captured objects are deliberately exempt: see CapturedObjectShape." ;
sh:severity sh:Warning ; sh:property [ sh:datatype xsd:string ;
sh:message "State the grain: the elements whose combination makes a row unique. Checkable against the harvested primary key." ] ; sh:maxCount 1 ;
sh:message "DQ-001 -- every defined object carries exactly one identifier. NOTE: uniqueness across the graph is NOT tested here; SHACL checks cardinality only. The reconciliation job tests uniqueness." ;
sh:minCount 1 ;
sh:path pr:identifier ;
sh:severity sh:Violation ],
[ sh:maxCount 1 ;
sh:message "A published object should carry a version." ;
sh:path pr:version ;
sh:severity sh:Warning ],
[ sh:datatype xsd:string ;
sh:maxCount 1 ;
sh:message "Every defined object carries exactly one canonical name." ;
sh:minCount 1 ;
sh:minLength 3 ;
sh:path pr:canonicalName ;
sh:severity sh:Violation ],
[ sh:in ( "DRAFT" "UNDER_REVIEW" "PUBLISHED" "DEPRECATED" "RETIRED" ) ;
sh:message "LC-001 -- status must be one of DRAFT, UNDER_REVIEW, PUBLISHED, DEPRECATED, RETIRED." ;
sh:minCount 1 ;
sh:path pr:status ;
sh:severity sh:Violation ],
[ sh:maxCount 1 ;
sh:message "OW-001 -- at most one owning Data Domain. On a Data Element this may be re-declared to differ from the parent chain: that marks a borrowed element." ;
sh:path pr:ownedByDomain ;
sh:severity sh:Violation ] ;
sh:targetClass pr:DefinedObject .
sh:property [ prs:FieldShape a sh:NodeShape ;
sh:path pr:monitoredBy ; sh:maxCount 0 ; sh:property [ sh:datatype xsd:string ;
sh:severity sh:Warning ; sh:maxCount 1 ;
sh:message "Stewardship is declared on the Business Object and inherited through represents. Declaring it here allows two answers to the same question." ] . sh:message "A Field carries DATABASE.SCHEMA.RELATION.COLUMN. A bare column name is ambiguous as soon as it exists in two tables, so the FQN is the real join key against INFORMATION_SCHEMA." ;
sh:minCount 1 ;
sh:path pr:fullyQualifiedName ;
sh:pattern "^[A-Za-z0-9_$]+\\\\.[A-Za-z0-9_$]+\\\\.[A-Za-z0-9_$]+\\\\.[A-Za-z0-9_$]+$" ;
sh:severity sh:Violation ],
[ sh:maxCount 1 ;
sh:message "Every Field lives in exactly one Physical Relation." ;
sh:minCount 1 ;
sh:path pr:livesIn ;
sh:severity sh:Violation ] ;
sh:targetClass pr:Field .
# A declared grain element must actually belong to the object it grains.
prs:GrainConsistencyShape a sh:NodeShape ; prs:GrainConsistencyShape a sh:NodeShape ;
sh:targetClass pr:DataObject ; sh:sparql [ sh:message "A grain element must be one of the object's own Data Elements." ;
sh:sparql [ sh:select """
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/> PREFIX pr: <https://ontology.pernod-ricard.com/metamodel/>
SELECT $this ?grain WHERE { SELECT $this ?grain WHERE {
$this pr:hasGrainElement ?grain . $this pr:hasGrainElement ?grain .
FILTER NOT EXISTS { $this pr:hasElement ?grain } FILTER NOT EXISTS { $this pr:hasElement ?grain }
}""" ] . }""" ;
sh:severity sh:Violation ] ;
sh:targetClass pr:DataObject .
prs:MetricShape a sh:NodeShape ;
sh:property [ sh:datatype xsd:string ;
sh:maxCount 1 ;
sh:message "BR-003 -- a Metric is a measurement with a calculation rule. No formula, no Metric." ;
sh:minCount 1 ;
sh:path pr:formula ;
sh:severity sh:Violation ],
[ sh:message "A Metric states every unit its variants are expressed in. NOT capped at one: Sell Out Volume is computed in 9L and in L, Sell Out Value in EUR, USD and local currency. The single-unit rule belongs on the Data Element, where it holds -- see DataElementShape." ;
sh:minCount 1 ;
sh:path pr:unit ;
sh:severity sh:Violation ],
[ sh:class pr:BusinessConcept ;
sh:message "A Metric measures at least one Business Concept -- this is how a measure element reaches business meaning." ;
sh:minCount 1 ;
sh:path pr:measures ;
sh:severity sh:Violation ],
[ sh:maxCount 0 ;
sh:message "Grain moved off the Metric in v1.1. A Metric is a calculation rule and has no rows; grain belongs to the Data Object, stated with hasGrainElement and checkable against the real primary key." ;
sh:path pr:hasGranularity ;
sh:severity sh:Warning ],
[ sh:maxCount 0 ;
sh:message "A Metric must not carry a physical name -- that is a layer leak. Under BR-013 the physical name belongs on the Data Element." ;
sh:path pr:physicalName ;
sh:severity sh:Violation ] ;
sh:targetClass pr:Metric .
################################################################# prs:MetricSourceShape a sh:NodeShape ;
# DATA ELEMENT -- the XOR of meaning rdfs:comment "Rewritten in v1.4 to compare System references rather than strings. A string comparison would have called 'SODH Gold' and 'Snowflake (SODH Gold)' two different systems." ;
################################################################# sh:sparql [ sh:message "A Metric's system of truth must match that of at least one element computing it. If they disagree, either the metric points at the wrong system or an element has moved." ;
sh:select """
PREFIX pr: <https://ontology.pernod-ricard.com/metamodel/>
SELECT $this WHERE {
$this pr:sourcedFrom ?sys ; pr:computedBy ?de .
FILTER NOT EXISTS {
$this pr:computedBy ?any .
?any pr:sourcedFrom ?sys .
}
}""" ;
sh:severity sh:Violation ],
[ sh:message "Elements computing one Metric come from several systems. Legitimate for a metric blending sources, worth a look otherwise." ;
sh:select """
PREFIX pr: <https://ontology.pernod-ricard.com/metamodel/>
SELECT $this WHERE {
$this pr:computedBy ?a , ?b .
?a pr:sourcedFrom ?sa .
?b pr:sourcedFrom ?sb .
FILTER(?sa != ?sb)
}""" ;
sh:severity sh:Warning ] ;
sh:targetClass pr:Metric .
prs:DataElementShape a sh:NodeShape ; prs:MotherMetricShape a sh:NodeShape ;
sh:targetClass pr:DataElement ; sh:sparql [ sh:message "BR-013 -- a PUBLISHED Metric should be implemented by at least one Data Element through computedBy. A metric no element computes is a definition with nothing behind it." ;
sh:select """
sh:property [ PREFIX pr: <https://ontology.pernod-ricard.com/metamodel/>
sh:path pr:physicalName ; sh:datatype xsd:string ; SELECT $this WHERE {
sh:maxCount 1 ; $this pr:status "PUBLISHED" .
sh:severity sh:Warning ; FILTER NOT EXISTS { $this pr:computedBy ?de }
sh:message "NR-009 -- the physical name is recorded verbatim and never altered." ] ; }""" ;
sh:severity sh:Warning ] ;
sh:property [ sh:targetClass pr:Metric .
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 ; prs:NamingShape a sh:NodeShape ;
sh:targetClass pr:CuratedObject ; sh:property [ sh:message "NR-001 -- a canonical name must not end in a physical suffix. Written as sh:not[sh:pattern] rather than a lookbehind: sh:pattern is normatively XPath 2.0 regex, which has no lookbehind, so the v0.1.1 form only worked under pySHACL." ;
sh:property [ sh:not [ sh:pattern "_NM$" ] ;
sh:path pr:hasName ; sh:path pr:canonicalName ;
sh:not [ sh:pattern "_NM$" ] ; sh:severity sh:Warning ],
sh:severity sh:Warning ; [ sh:message "NR-002 -- a canonical name is written for humans, not as a snake_case identifier." ;
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:not [ sh:pattern "^[a-z_]+$" ] ;
sh:property [ sh:path pr:canonicalName ;
sh:path pr:hasName ; sh:severity sh:Warning ] ;
sh:not [ sh:pattern "^[a-z_]+$" ] ; sh:targetClass pr:DefinedObject .
sh:severity sh:Warning ;
sh:message "NR-002 -- a canonical name is written for humans, not as a snake_case identifier." ] . prs:PublishedShape a sh:NodeShape ;
rdfs:comment "Extra demands that only bite once an object claims to be PUBLISHED. Keeping these off the base shape is what lets a domain be documented progressively." ;
sh:sparql [ sh:message "OW-007 -- an object flagged TO_ARBITRATE cannot be PUBLISHED. Resolve at the Cross-Domain Council first." ;
sh:select """
PREFIX pr: <https://ontology.pernod-ricard.com/metamodel/>
SELECT $this WHERE {
$this pr:status "PUBLISHED" ;
pr:arbitrationStatus "TO_ARBITRATE" .
}""" ;
sh:severity sh:Violation ],
[ sh:message "OW-003 -- a PUBLISHED object must have an owning Data Domain." ;
sh:select """
PREFIX pr: <https://ontology.pernod-ricard.com/metamodel/>
SELECT $this WHERE {
$this pr:status "PUBLISHED" .
FILTER NOT EXISTS { $this pr:ownedByDomain ?d }
}""" ;
sh:severity sh:Violation ] ;
sh:targetClass pr:DefinedObject .
prs:SubDomainShape a sh:NodeShape ;
sh:property [ sh:class pr:DataDomain ;
sh:maxCount 1 ;
sh:message "A Sub-Domain belongs to exactly one Data Domain." ;
sh:minCount 1 ;
sh:path pr:belongsTo ;
sh:severity sh:Violation ],
[ sh:maxCount 1 ;
sh:message "A Sub-Domain should name its owner." ;
sh:path pr:hasSubDomainOwner ;
sh:severity sh:Warning ] ;
sh:targetClass pr:DataSubDomain .
<https://ontology.pernod-ricard.com/shapes> a owl:Ontology ;
rdfs:label "PR Data MetaModel SHACL Shapes" ;
dcterms:conformsTo pr:2.0 .