Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1c8ab77003 | |||
| f83b7d7491 | |||
| 76e95de471 | |||
| 7793def268 | |||
| 45ca9ffb68 | |||
| c8693b63bb |
@@ -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 |
|
||||
@@ -0,0 +1,941 @@
|
||||
# Pernod Ricard Data MetaModel — T-Box Rulebook
|
||||
|
||||
**Version 1.1** — 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 whenever the form derived under TN-018 is not what the organisation writes or says. The short label is free: acronyms, abbreviations, hyphens, short forms.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| 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. 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 |
|
||||
|
||||
## 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 |
|
||||
|---|---|---|---|---|
|
||||
| `DataDomain` | Data Domain | Domain | DD | DD is already the short form used inside A-Box identifiers under NR-014 |
|
||||
| `DataSubDomain` | Data Sub Domain | Sub Domain | SD | no hyphen |
|
||||
| `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 |
|
||||
| `DataSteward` | Data Steward | Steward | - | |
|
||||
| `DataGovernanceLead` | Data Governance Lead | Governance Lead | DGL | |
|
||||
| `DataElement` | Data Element | Element | - | drives the name of hasElement and hasGrainElement |
|
||||
| `BusinessConcept` | Business Concept | Concept | - | drives the name of usesConcept |
|
||||
| `BusinessObject` | Business Object | - | BO | |
|
||||
| `KeyPerformanceIndicator` | Key Performance Indicator | KPI | KPI | the acronym channel is what makes TN-002 bearable |
|
||||
| `BusinessIntelligenceWorkspace` | Business Intelligence Workspace | BI Workspace | - | |
|
||||
| `BusinessIntelligenceDataSource` | Business Intelligence Data Source | BI Data Source | - | |
|
||||
| `BusinessIntelligenceField` | Business Intelligence Field | BI Field | - | |
|
||||
| `BusinessIntelligenceReport` | Business Intelligence Report | BI Report | - | |
|
||||
| `OwnershipLayerObject` | Ownership Layer Object | - | - | the categorisation role moves into rdfs:comment |
|
||||
Binary file not shown.
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
+941
-550
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2081
-510
File diff suppressed because it is too large
Load Diff
+656
-194
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,18 @@
|
||||
# PR Data Meta Model - validation stack
|
||||
#
|
||||
# PINNED ON PURPOSE. The venv on GrosseBertha runs Python 3.9
|
||||
# (/usr/local/bin/python3.9, the only stable interpreter on DSM 7.4).
|
||||
#
|
||||
# Newer pyshacl releases advertise 3.9 support but use PEP 604 syntax
|
||||
# (str | X) in runtime annotations, which needs 3.10+. pip installs them
|
||||
# without complaint and the import then dies with:
|
||||
# TypeError: unsupported operand type(s) for |: 'type' and 'ABCMeta'
|
||||
#
|
||||
# 0.26.0 is the version verified working on this interpreter (2026-07-26).
|
||||
# Do not unpin without rebuilding the venv and re-running the validator.
|
||||
#
|
||||
# /usr/local/bin/python3.9 -m venv venv
|
||||
# . venv/bin/activate && pip install -r requirements.txt
|
||||
|
||||
pyshacl==0.26.0
|
||||
rdflib>=6.3.2,<7.1
|
||||
@@ -0,0 +1,279 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
SODH arbitration - composedOf -> aboutConcept + usesConcept
|
||||
============================================================
|
||||
Second half of the v1.0 -> v1.1 migration. migrate_instances_v1_1.py did the
|
||||
mechanical renames and deliberately stopped here, because everything below is a
|
||||
judgement someone had to make.
|
||||
|
||||
USAGE
|
||||
python3 scripts/apply_arbitration_sodh.py # dry run
|
||||
python3 scripts/apply_arbitration_sodh.py --apply # rewrite, .bak kept
|
||||
|
||||
WHAT WAS ARBITRATED (Bastien, 2026-07-27)
|
||||
-----------------------------------------
|
||||
The v0.6 padding is what made this necessary: four Business Objects belonging
|
||||
to other domains -- Product Structure, Customer & Outlet, Currency, Calendar --
|
||||
were all declared composedOf the same three Sell Out concepts, because BR-004
|
||||
demanded three and there were none to give. The subject rule replaces that
|
||||
minimum: exactly one subject, and if it cannot be named the object is not
|
||||
scoped.
|
||||
|
||||
1. Distribution -> Retail Distribution.
|
||||
"Distribution" is too generic to survive contact with other domains, and
|
||||
it is NOT a kind of sell out: sell out counts units sold, distribution
|
||||
measures product presence in store. Retail Distribution follows panel
|
||||
vocabulary (Nielsen/Circana), under which Numeric and Weighted
|
||||
Distribution are the metrics.
|
||||
|
||||
2. Sell Out Baseline becomes a concept in its own right, not a flavour of
|
||||
Sell Out: modelled non-promoted volume is a different notion from
|
||||
observed volume.
|
||||
|
||||
3. Customer & Outlet splits into two Business Objects, and Currency &
|
||||
Exchange Rates likewise. An object named "X & Y" has no single subject,
|
||||
which is precisely what the rule is meant to surface. The split is
|
||||
PROPOSED here; DD-04 and DD-16 own the decision.
|
||||
|
||||
4. Six concepts belonging to other domains are created with a DRAFT
|
||||
definition and arbitrationStatus TO_ARBITRATE. A draft to correct beats a
|
||||
blank to fill, and OW-007 blocks publication until the owning domain has
|
||||
ratified. These definitions are the DGO's proposal, not DD-04/10/16/21's
|
||||
word.
|
||||
|
||||
NOT DONE HERE
|
||||
hasGranularity on Metrics still needs re-expressing as hasGrainElement on
|
||||
Data Objects, and the BR-013 alignment (10 mother metrics, computedBy)
|
||||
is a separate migration. Until that lands, every Data Element will fail
|
||||
the XOR-of-meaning shape -- which is the correct, informative result.
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
|
||||
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
TARGET = os.path.join(REPO, "instances", "sodh.ttl")
|
||||
W = 78
|
||||
|
||||
# ---------------------------------------------------------------- new concepts
|
||||
# (iri, identifier, name, domain, steward, definition, arbitrate)
|
||||
NEW_CONCEPTS = [
|
||||
("ex:BC_06_01_005", "BC-06.01-005", "Sell Out Baseline", "ex:DD_06", "ex:ST_DD_06",
|
||||
"The modelled level of retail sales that would have occurred without promotional activity, "
|
||||
"used as the reference against which promotional uplift is measured. Distinct from Sell Out, "
|
||||
"which is observed rather than modelled.",
|
||||
False),
|
||||
("ex:BC_10_01_001", "BC-10.01-001", "Product", "ex:DD_10", "ex:ST_DD_10",
|
||||
"A commercial item sold by Pernod Ricard, identified at Stock Keeping Unit level and "
|
||||
"positioned in the corporate hierarchy from Brand through Category, Sub-Category and Flavor.",
|
||||
True),
|
||||
("ex:BC_04_01_001", "BC-04.01-001", "Customer", "ex:DD_04", "ex:ST_DD_04",
|
||||
"A commercial counterparty that buys Pernod Ricard products for resale, positioned in the "
|
||||
"trade hierarchy from Distributor through Tier-1 and Tier-2 Customer. Distinct from Outlet, "
|
||||
"which is a physical point of sale.",
|
||||
True),
|
||||
("ex:BC_04_01_002", "BC-04.01-002", "Outlet", "ex:DD_04", "ex:ST_DD_04",
|
||||
"A physical point of sale where products are made available to end consumers, characterised "
|
||||
"by its Channel. An Outlet belongs to a Customer but is not the same notion: one Customer "
|
||||
"operates many Outlets.",
|
||||
True),
|
||||
("ex:BC_16_01_001", "BC-16.01-001", "Currency", "ex:DD_16", "ex:ST_DD_16",
|
||||
"A monetary unit in which a transaction or a reported amount is denominated, identified by "
|
||||
"its ISO 4217 code.",
|
||||
True),
|
||||
("ex:BC_16_01_002", "BC-16.01-002", "Exchange Rate", "ex:DD_16", "ex:ST_DD_16",
|
||||
"The conversion factor applied between two currencies at a given date, used to restate local "
|
||||
"amounts into group reporting currency. Distinct from Currency: a rate is a dated "
|
||||
"relationship between two of them.",
|
||||
True),
|
||||
("ex:BC_21_01_001", "BC-21.01-001", "Calendar Date", "ex:DD_21", "ex:ST_DD_21",
|
||||
"A single day in the Gregorian calendar, the atomic unit from which Week, Month, Quarter and "
|
||||
"Year are derived and against which all time-based measures are positioned.",
|
||||
True),
|
||||
]
|
||||
|
||||
# ------------------------------------------------------------------- new object
|
||||
NEW_OBJECTS = [
|
||||
("ex:BO_04_02_001", "BO-04.02-001", "Outlet", "ex:DD_04", "ex:ST_DD_04", "ex:SD_04_02",
|
||||
"ex:BC_04_01_002", [], "MDM Customer -> SODH [split from BO-04.01-001, proposed to DD-04]"),
|
||||
("ex:BO_16_01_002", "BO-16.01-002", "Exchange Rate", "ex:DD_16", "ex:ST_DD_16", "ex:SD_16_01",
|
||||
"ex:BC_16_01_002", [], "Group Finance -> SODH [split from BO-16.01-001, proposed to DD-16]"),
|
||||
]
|
||||
|
||||
# ---------------------------------------------------- subject / used per object
|
||||
# iri -> (aboutConcept, [usesConcept...], new name or None)
|
||||
ARBITRATION = {
|
||||
"ex:BO_06_01_001": ("ex:BC_06_01_001", ["ex:BC_06_01_003", "ex:BC_05_01_001"], None),
|
||||
"ex:BO_06_01_002": ("ex:BC_06_01_001", ["ex:BC_06_01_003", "ex:BC_05_01_001",
|
||||
"ex:BC_16_01_001"], None),
|
||||
"ex:BO_06_01_003": ("ex:BC_06_01_004", ["ex:BC_06_01_003"], None),
|
||||
"ex:BO_06_01_004": ("ex:BC_06_01_002", ["ex:BC_06_01_001", "ex:BC_06_01_003"], None),
|
||||
"ex:BO_06_01_005": ("ex:BC_06_01_005", ["ex:BC_06_01_001", "ex:BC_05_01_001"], None),
|
||||
"ex:BO_10_01_001": ("ex:BC_10_01_001", [], None),
|
||||
"ex:BO_04_01_001": ("ex:BC_04_01_001", [], "Customer"),
|
||||
"ex:BO_16_01_001": ("ex:BC_16_01_001", [], "Currency"),
|
||||
"ex:BO_21_01_001": ("ex:BC_21_01_001", [], None),
|
||||
"ex:BO_05_01_001": ("ex:BC_05_01_001", [], None),
|
||||
}
|
||||
|
||||
RENAME_CONCEPT = {"ex:BC_06_01_004": ("Distribution", "Retail Distribution")}
|
||||
|
||||
|
||||
def concept_block(iri, ident, name, domain, steward, definition, arbitrate):
|
||||
lines = [
|
||||
'%s a pr:BusinessConcept ;' % iri,
|
||||
' pr:hasIdentifier "%s" ; pr:hasName "%s" ;' % (ident, name),
|
||||
' pr:owningDomain %s ; pr:ownedBy %s ;' % (domain, steward),
|
||||
' pr:hasBusinessDefinition "%s" ;' % definition,
|
||||
]
|
||||
if arbitrate:
|
||||
lines.append(' pr:arbitrationStatus "TO_ARBITRATE" ;')
|
||||
src = "DGO proposal, pending ratification by the owning domain"
|
||||
else:
|
||||
src = "SODH Gold"
|
||||
lines.append(' pr:hasStatus "DRAFT" ; pr:hasVersion "1.1" ; pr:hasSource "%s" .' % src)
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def object_block(iri, ident, name, domain, steward, subdomain, about, uses, source):
|
||||
lines = [
|
||||
'%s a pr:BusinessObject ;' % iri,
|
||||
' pr:hasIdentifier "%s" ; pr:hasName "%s" ;' % (ident, name),
|
||||
' pr:owningDomain %s ; pr:ownedBy %s ;' % (domain, steward),
|
||||
' pr:belongsTo %s ; pr:monitoredBy %s ;' % (subdomain, steward),
|
||||
' pr:aboutConcept %s ;' % about,
|
||||
]
|
||||
if uses:
|
||||
lines.append(' pr:usesConcept %s ;' % " , ".join(uses))
|
||||
lines.append(' pr:arbitrationStatus "TO_ARBITRATE" ;')
|
||||
lines.append(' pr:hasStatus "DRAFT" ; pr:hasVersion "1.1" ; pr:hasSource "%s" .' % source)
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def main():
|
||||
apply_changes = "--apply" in sys.argv
|
||||
if not os.path.exists(TARGET):
|
||||
print("Not found: %s" % TARGET)
|
||||
sys.exit(2)
|
||||
|
||||
text = original = open(TARGET, encoding="utf-8").read()
|
||||
report = []
|
||||
|
||||
# ---- 1. rename the over-generic concept ----------------------------------
|
||||
for iri, (old, new) in RENAME_CONCEPT.items():
|
||||
pat = re.compile(r'(%s\b.*?pr:hasName )"%s"' % (re.escape(iri), re.escape(old)), re.S)
|
||||
text, n = pat.subn(r'\1"%s"' % new, text, count=1)
|
||||
if n:
|
||||
report.append("renamed concept %s: %s -> %s" % (iri, old, new))
|
||||
|
||||
# ---- 2. rewrite each Business Object -------------------------------------
|
||||
for iri, (about, uses, newname) in ARBITRATION.items():
|
||||
m = re.search(r'(^%s a pr:BusinessObject ;.*?\n)(?=^ex:|\Z)' % re.escape(iri),
|
||||
text, re.S | re.M)
|
||||
if not m:
|
||||
report.append("!! block not found: %s" % iri)
|
||||
continue
|
||||
block = m.group(1)
|
||||
|
||||
# strip what a previous run may have written, so re-running is a no-op.
|
||||
# Duplicated clauses carrying the SAME value merge under RDF set
|
||||
# semantics: the graph stays valid and SHACL sees nothing, while the
|
||||
# file quietly grows a redundant line on every pass.
|
||||
new_block = re.sub(r'[ \t]*pr:composedOf\s+[^";]*;[ \t]*\n', '', block)
|
||||
new_block = re.sub(r'[ \t]*pr:(aboutConcept|usesConcept)\s+[^";]*;[ \t]*\n',
|
||||
'', new_block)
|
||||
clause = ' pr:aboutConcept %s ;\n' % about
|
||||
if uses:
|
||||
clause += ' pr:usesConcept %s ;\n' % " , ".join(uses)
|
||||
new_block = re.sub(r'(\n)([ \t]*pr:hasMetric|[ \t]*pr:hasBusinessDefinition|[ \t]*pr:hasStatus)',
|
||||
r'\1%s\2' % clause, new_block, count=1)
|
||||
|
||||
# meaning belongs to the Concept, never to the Object
|
||||
# match the full quoted literal: several definitions contain a semicolon
|
||||
# inside the string ("Owned by Product & Material; consumed by SODH."),
|
||||
# which a naive [^;]* would stop at, silently leaving the property behind
|
||||
new_block, ndef = re.subn(
|
||||
r'[ \t]*pr:hasBusinessDefinition\s+"(?:[^"\\]|\\.)*"\s*;[ \t]*\n',
|
||||
'', new_block)
|
||||
|
||||
if newname:
|
||||
new_block = re.sub(r'(pr:hasName )"[^"]*"', r'\1"%s"' % newname, new_block, count=1)
|
||||
new_block = re.sub(r'(pr:hasSource )"[^"]*"',
|
||||
r'\1"MDM -> SODH [split proposed, subject arbitrated 2026-07-27]"',
|
||||
new_block, count=1)
|
||||
|
||||
text = text[:m.start(1)] + new_block + text[m.end(1):]
|
||||
report.append("%-18s about=%s uses=%d%s%s"
|
||||
% (iri, about.replace("ex:BC_", "BC-"), len(uses),
|
||||
" def->concept" if ndef else "",
|
||||
" renamed=%s" % newname if newname else ""))
|
||||
|
||||
# ---- 3. insert the missing concepts --------------------------------------
|
||||
# idempotent: identical duplicated blocks merge in RDF, so SHACL would
|
||||
# never flag them -- the file grows while the graph stays valid.
|
||||
todo_c = [c for c in NEW_CONCEPTS
|
||||
if not re.search(r'^%s a ' % re.escape(c[0]), text, re.M)]
|
||||
anchor = re.search(r'\n(?=ex:BO_\w+ a pr:BusinessObject ;)', text)
|
||||
if anchor and todo_c:
|
||||
blocks = "\n".join(concept_block(*c) for c in todo_c)
|
||||
text = (text[:anchor.start()] + "\n\n" +
|
||||
"# --- concepts added in v1.1 migration -------------------------------\n" +
|
||||
"# Six of these belong to other domains: DRAFT definitions proposed by the\n" +
|
||||
"# DGO, flagged TO_ARBITRATE. OW-007 blocks publication until ratified.\n" +
|
||||
blocks + "\n" + text[anchor.start():])
|
||||
report.append("inserted %d Business Concepts (%d TO_ARBITRATE)"
|
||||
% (len(todo_c), sum(1 for c in todo_c if c[6])))
|
||||
elif not todo_c:
|
||||
report.append("all %d Business Concepts already present, skipped"
|
||||
% len(NEW_CONCEPTS))
|
||||
|
||||
# ---- 4. append the objects produced by the splits ------------------------
|
||||
todo_o = [o for o in NEW_OBJECTS
|
||||
if not re.search(r'^%s a ' % re.escape(o[0]), text, re.M)]
|
||||
tail = re.search(r'\n(?=ex:M_\w+ a pr:Metric ;|ex:DO_\w+ a pr:DataObject ;)', text)
|
||||
if tail and todo_o:
|
||||
blocks = "\n".join(object_block(*o) for o in todo_o)
|
||||
text = (text[:tail.start()] + "\n\n" +
|
||||
"# --- Business Objects from the v1.1 splits ---------------------------\n" +
|
||||
"# 'Customer & Outlet' and 'Currency & Exchange Rates' had no single\n" +
|
||||
"# subject. Split proposed to DD-04 and DD-16, hence TO_ARBITRATE.\n" +
|
||||
blocks + "\n" + text[tail.start():])
|
||||
report.append("inserted %d Business Objects from splits" % len(todo_o))
|
||||
elif not todo_o:
|
||||
report.append("Business Objects from splits already present, skipped")
|
||||
|
||||
# ---- report --------------------------------------------------------------
|
||||
print()
|
||||
print("SODH ARBITRATION %s" % ("APPLY" if apply_changes else "DRY RUN"))
|
||||
print("=" * W)
|
||||
for line in report:
|
||||
print(" " + line)
|
||||
print("=" * W)
|
||||
doubled = []
|
||||
for m in re.finditer(r'^(ex:\w+) a pr:\w+ ;(.*?)\.\s*\n', text, re.S | re.M):
|
||||
for prop in ("pr:hasName", "pr:aboutConcept", "pr:belongsTo"):
|
||||
if len(re.findall(r'(?<![\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))
|
||||
bc = len(re.findall(r'a pr:BusinessConcept', text))
|
||||
print(" Business Objects %d | Business Concepts %d | composedOf left %d"
|
||||
% (bo, bc, len(re.findall(r'pr:composedOf', text))))
|
||||
print(" Business Objects still carrying a definition: %d"
|
||||
% len([b for b in re.split(r'\.\s*\n(?=\S)', text)
|
||||
if "pr:BusinessObject" in b and "pr:hasBusinessDefinition" in b]))
|
||||
print("=" * W)
|
||||
|
||||
if text != original and apply_changes:
|
||||
shutil.copy2(TARGET, TARGET + ".bak")
|
||||
open(TARGET, "w", encoding="utf-8").write(text)
|
||||
print(" written, backup at %s.bak" % os.path.basename(TARGET))
|
||||
elif text != original:
|
||||
print(" dry run -- re-run with --apply to write")
|
||||
print()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,482 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
SODH - BR-013 alignment, last step of the v1.1 migration
|
||||
=========================================================
|
||||
Regenerates the Metric and Data Element sections of instances/sodh.ttl from the
|
||||
v0.7 back-doc, which has been ahead of the TTL since the divergence found at
|
||||
audit time. This is the migration that puts the flow back the right way round:
|
||||
after it, the TTL is the source and the workbook becomes a generated artefact.
|
||||
|
||||
USAGE
|
||||
python3 scripts/apply_br013_sodh.py # dry run
|
||||
python3 scripts/apply_br013_sodh.py --apply # rewrite, .bak kept
|
||||
|
||||
WHAT IT FIXES
|
||||
37 BLOCKING DataElementMeaningShape -- no element had a route to meaning
|
||||
15 BLOCKING physicalName on a Metric -- layer leak
|
||||
15 WARNING hasGranularity on a Metric
|
||||
6 WARNING monitoredBy on a Data Object
|
||||
|
||||
WHAT IT DOES
|
||||
1. 15 metrics -> 10 mother metrics carrying the harmonized calculation
|
||||
rule, each with a formula, a unit and a measured Concept. No physical
|
||||
name, no granularity: both belong elsewhere now.
|
||||
2. 95 measure Data Elements generated, each computedBy its mother metric,
|
||||
each carrying the physical name the metric used to hold.
|
||||
3. 37 dimensional Data Elements given a represents towards their Concept.
|
||||
With 2, this closes the XOR: every element reaches meaning by exactly
|
||||
one route.
|
||||
4. Three concepts created because the XOR forces them to be named --
|
||||
Country, Marketing Entity, Fiscal Period had elements but no notion.
|
||||
TO_ARBITRATE like the others.
|
||||
5. Steward removed from the 6 Data Objects: it is inherited now.
|
||||
6. hasGrainElement on the 4 DIMENSION Data Objects only.
|
||||
|
||||
WHY FACT TABLES GET NO GRAIN ELEMENTS
|
||||
A fact table's grain is carried by its foreign keys, and OW-006 says a
|
||||
foreign key is not a Data Element -- so a fact object has no element of its
|
||||
own to point at. Its grain is already expressed, by references towards the
|
||||
dimension objects it joins. Forcing hasGrainElement onto facts would mean
|
||||
either breaking OW-006 or pointing at another object's elements, which
|
||||
GrainConsistencyShape rejects. Nothing is lost: the information is there.
|
||||
"""
|
||||
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")
|
||||
BACKDOC = os.path.join(REPO, "instances", "SODH_data.xlsx")
|
||||
W = 78
|
||||
|
||||
# ---- mother metric -> the Concept it measures ------------------------------
|
||||
METRIC_CONCEPT = {
|
||||
"M-06.01-001": "ex:BC_06_01_001", # Sell Out Volume -> Sell Out
|
||||
"M-06.01-002": "ex:BC_06_01_001", # Sell Out Value -> Sell Out
|
||||
"M-06.01-003": "ex:BC_06_01_004", # Numeric Distribution -> Retail Distribution
|
||||
"M-06.01-004": "ex:BC_06_01_004", # Weighted Distribution -> Retail Distribution
|
||||
"M-06.01-005": "ex:BC_06_01_004", # Total Distribution Pts -> Retail Distribution
|
||||
"M-06.01-006": "ex:BC_06_01_002", # Forward Stock Volume -> Retailer Stock
|
||||
"M-06.01-007": "ex:BC_06_01_002", # Days of Coverage -> Retailer Stock
|
||||
"M-06.01-008": "ex:BC_06_01_002", # Stock Share -> Retailer Stock
|
||||
"M-06.01-009": "ex:BC_06_01_005", # Baseline Volume -> Sell Out Baseline
|
||||
"M-06.01-010": "ex:BC_06_01_005", # Incremental Volume -> Sell Out Baseline
|
||||
}
|
||||
|
||||
# ---- dimensional element -> the Concept it represents ----------------------
|
||||
# Exact ids first, then id prefixes. Every dimensional element must land
|
||||
# somewhere: that is what the XOR is for.
|
||||
DE_CONCEPT_EXACT = {
|
||||
"DE-04.01-0006": "ex:BC_04_01_002", # Outlet Type -> Outlet
|
||||
"DE-04.01-0007": "ex:BC_04_01_002", # Channel -> Outlet
|
||||
"DE-04.01-0010": "ex:BC_04_01_002", # Point of Sale Count -> Outlet
|
||||
"DE-04.01-0008": "ex:BC_04_03_001", # Country Code -> Country
|
||||
"DE-04.01-0009": "ex:BC_04_03_001", # Country Name -> Country
|
||||
"DE-16.01-0001": "ex:BC_16_01_001", # Currency Code -> Currency
|
||||
"DE-16.01-0002": "ex:BC_16_01_002", # FX Rate to-Euro -> Exchange Rate
|
||||
"DE-16.01-0003": "ex:BC_16_01_002", # FX Rate to-USD -> Exchange Rate
|
||||
"DE-16.02-0001": "ex:BC_16_03_001", # Marketing Entity -> Marketing Entity
|
||||
"DE-16.02-0002": "ex:BC_16_02_001", # Fiscal Period Code -> Fiscal Period
|
||||
"DE-16.02-0003": "ex:BC_16_02_001", # Fiscal Quarter Code -> Fiscal Period
|
||||
}
|
||||
DE_CONCEPT_PREFIX = [
|
||||
("DE-10.01", "ex:BC_10_01_001"), # product hierarchy -> Product
|
||||
("DE-04.01", "ex:BC_04_01_001"), # trade hierarchy -> Customer
|
||||
("DE-21.01", "ex:BC_21_01_001"), # calendar -> Calendar Date
|
||||
("DE-05.01", "ex:BC_05_01_001"), # promotion flags -> Promotion
|
||||
]
|
||||
|
||||
# ---- concepts the XOR forces us to name ------------------------------------
|
||||
NEW_CONCEPTS = [
|
||||
# Final identifiers from the outset. Creating them under one id and
|
||||
# renumbering them later broke idempotence: the second run no longer found
|
||||
# the original id and recreated the concept alongside the renamed one.
|
||||
("ex:BC_04_03_001", "BC-04.03-001", "Country", "ex:DD_04", "ex:ST_DD_04",
|
||||
"A sovereign territory used as the geographic frame for retail measurement, "
|
||||
"identified by its ISO 3166 code. Distinct from Customer and Outlet: it is where "
|
||||
"they operate, not what they are."),
|
||||
("ex:BC_16_03_001", "BC-16.03-001", "Marketing Entity", "ex:DD_16", "ex:ST_DD_16",
|
||||
"An organisational unit of the group holding commercial responsibility for a market, "
|
||||
"and the level at which financial results are consolidated."),
|
||||
("ex:BC_16_02_001", "BC-16.02-001", "Fiscal Period", "ex:DD_16", "ex:ST_DD_16",
|
||||
"A reporting interval of the Pernod Ricard fiscal year, which runs July to June. "
|
||||
"Distinct from Calendar Date: the fiscal frame does not align with the Gregorian one."),
|
||||
]
|
||||
|
||||
# ---- grain of the DIMENSION objects (fact grain lives in references) -------
|
||||
GRAIN = {
|
||||
"ex:DO_10_01_001": ["ex:DE_10_01_0005"], # Product Dimension -> SKU Code
|
||||
"ex:DO_04_01_001": ["ex:DE_04_01_0005"], # Customer Dimension -> Customer Tier-2 Code
|
||||
"ex:DO_16_01_001": ["ex:DE_16_01_0001"], # Currency Dimension -> Currency Code
|
||||
"ex:DO_21_01_001": ["ex:DE_21_01_0001"], # Calendar Dimension -> Calendar Date
|
||||
}
|
||||
|
||||
UNIT_FIX = {"9L / L": "9L", "Currency EUR/USD/LC": "EUR", "%": "%",
|
||||
"Index": "Index", "Days": "Days"}
|
||||
|
||||
|
||||
def iri(ident):
|
||||
"""BO-06.01-001 -> ex:BO_06_01_001"""
|
||||
return "ex:" + ident.replace("-", "_", 1).replace(".", "_").replace("-", "_")
|
||||
|
||||
|
||||
def esc(text):
|
||||
return text.replace("\\", "\\\\").replace('"', '\\"')
|
||||
|
||||
|
||||
def _xlsx_rows(path):
|
||||
"""
|
||||
Minimal .xlsx reader: zipfile + ElementTree, no third-party dependency.
|
||||
|
||||
openpyxl would do this in three lines, but the validation venv is already
|
||||
pinned tightly (pyshacl 0.26.0 for Python 3.9) and adding a dependency to a
|
||||
migration script that runs once is a poor trade. Handles what a back-doc
|
||||
needs: shared strings, inline strings, numbers, and sheet names.
|
||||
"""
|
||||
import zipfile
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
NS = "{http://schemas.openxmlformats.org/spreadsheetml/2006/main}"
|
||||
REL = "{http://schemas.openxmlformats.org/officeDocument/2006/relationships}"
|
||||
PKG = "{http://schemas.openxmlformats.org/package/2006/relationships}"
|
||||
|
||||
with zipfile.ZipFile(path) as z:
|
||||
shared = []
|
||||
if "xl/sharedStrings.xml" in z.namelist():
|
||||
for si in ET.fromstring(z.read("xl/sharedStrings.xml")):
|
||||
shared.append("".join(t.text or "" for t in si.iter(NS + "t")))
|
||||
|
||||
rels = {}
|
||||
for rel in ET.fromstring(z.read("xl/_rels/workbook.xml.rels")):
|
||||
rels[rel.get("Id")] = rel.get("Target").lstrip("/")
|
||||
|
||||
sheets = []
|
||||
for sh in ET.fromstring(z.read("xl/workbook.xml")).iter(NS + "sheet"):
|
||||
target = rels.get(sh.get(REL + "id"), "")
|
||||
if not target.startswith("xl/"):
|
||||
target = "xl/" + target
|
||||
sheets.append((sh.get("name"), target))
|
||||
|
||||
for name, target in sheets:
|
||||
if target not in z.namelist():
|
||||
continue
|
||||
rows = []
|
||||
for row in ET.fromstring(z.read(target)).iter(NS + "row"):
|
||||
cells = []
|
||||
for c in row.iter(NS + "c"):
|
||||
v = c.find(NS + "v")
|
||||
if c.get("t") == "s" and v is not None:
|
||||
cells.append(shared[int(v.text)])
|
||||
elif c.get("t") == "inlineStr":
|
||||
cells.append("".join(t.text or "" for t in c.iter(NS + "t")))
|
||||
else:
|
||||
cells.append(v.text if v is not None else "")
|
||||
rows.append([(x or "").strip() for x in cells])
|
||||
yield name, rows
|
||||
|
||||
|
||||
def _text_rows(path):
|
||||
"""Tab-separated fallback, for back-docs exported as plain text."""
|
||||
sheet, rows = None, []
|
||||
for raw in open(path, encoding="utf-8", errors="replace"):
|
||||
if raw.startswith("## Sheet:"):
|
||||
if sheet:
|
||||
yield sheet, rows
|
||||
sheet, rows = raw.split(":", 1)[1].strip(), []
|
||||
elif sheet is not None:
|
||||
rows.append([c.strip() for c in raw.rstrip("\n").split("\t")])
|
||||
if sheet:
|
||||
yield sheet, rows
|
||||
|
||||
|
||||
def parse_backdoc(path):
|
||||
"""
|
||||
Read the back-doc, whichever form it takes.
|
||||
|
||||
A real .xlsx is a ZIP (magic PK\x03\x04); some pipelines hand over a
|
||||
tab-separated text export of the same content. Sniff rather than assume:
|
||||
guessing from the extension is what made this script fail the first time.
|
||||
"""
|
||||
with open(path, "rb") as fh:
|
||||
is_zip = fh.read(4) == b"PK\x03\x04"
|
||||
reader = _xlsx_rows if is_zip else _text_rows
|
||||
|
||||
out = {"metrics": [], "elements": []}
|
||||
for name, rows in reader(path):
|
||||
key = None
|
||||
if name.strip().startswith("6"):
|
||||
key = "metrics"
|
||||
elif name.strip().startswith("7"):
|
||||
key = "elements"
|
||||
if not key:
|
||||
continue
|
||||
for cells in rows:
|
||||
if len(cells) < 5 or not cells[0]:
|
||||
continue
|
||||
if key == "metrics" and cells[0].startswith("M-"):
|
||||
out[key].append(cells)
|
||||
elif key == "elements" and cells[0].startswith("DE-"):
|
||||
out[key].append(cells)
|
||||
if not out["metrics"] or not out["elements"]:
|
||||
raise SystemExit(
|
||||
"Back-doc read but empty: %d metrics, %d elements.\n"
|
||||
"Expected sheets starting with '6.' (Mother Metrics) and '7.' "
|
||||
"(Data Elements) in %s" % (len(out["metrics"]), len(out["elements"]), path))
|
||||
return out
|
||||
|
||||
|
||||
def concept_for(de_id):
|
||||
if de_id in DE_CONCEPT_EXACT:
|
||||
return DE_CONCEPT_EXACT[de_id]
|
||||
for prefix, bc in DE_CONCEPT_PREFIX:
|
||||
if de_id.startswith(prefix):
|
||||
return bc
|
||||
return None
|
||||
|
||||
|
||||
def build_metrics(metrics):
|
||||
out = ["# --- mother metrics, BR-013 -----------------------------------------",
|
||||
"# One harmonized calculation rule each. The granular variants are Data",
|
||||
"# Elements linked by computedBy. No physical name (layer leak) and no",
|
||||
"# granularity (a calculation rule has no rows).", ""]
|
||||
for r in metrics:
|
||||
mid, name, bo, formula, unit = r[0], r[1], r[2], r[3], r[4]
|
||||
unit = UNIT_FIX.get(unit.split(" (")[0], unit.split(" (")[0])
|
||||
out.append("%s a pr:Metric ;" % iri(mid))
|
||||
out.append(' pr:hasIdentifier "%s" ; pr:hasName "%s" ;' % (mid, esc(name)))
|
||||
out.append(" pr:owningDomain ex:DD_06 ; pr:ownedBy ex:ST_DD_06 ;")
|
||||
out.append(' pr:hasFormula "%s" ;' % esc(formula))
|
||||
out.append(' pr:hasUnit "%s" ;' % esc(unit))
|
||||
out.append(" pr:measures %s ;" % METRIC_CONCEPT[mid])
|
||||
out.append(' pr:hasStatus "DRAFT" ; pr:hasVersion "1.1" ; '
|
||||
'pr:hasSource "SODH back-doc v0.7" .')
|
||||
return "\n".join(out)
|
||||
|
||||
|
||||
def build_elements(elements):
|
||||
measure, dimensional = [], []
|
||||
for r in elements:
|
||||
(deid, name, domain, computed, fmt, unit, source, phys, do) = r[:9]
|
||||
dom = "ex:DD_" + deid.split("-")[1].split(".")[0]
|
||||
block = ["%s a pr:DataElement ;" % iri(deid),
|
||||
' pr:hasIdentifier "%s" ; pr:hasName "%s" ;' % (deid, esc(name)),
|
||||
" pr:owningDomain %s ;" % dom,
|
||||
' pr:hasFormat "%s" ;' % esc(fmt)]
|
||||
if unit and unit != "-":
|
||||
block.append(' pr:hasUnit "%s" ;' % esc(unit))
|
||||
if phys and phys != "-":
|
||||
block.append(' pr:physicalName "%s" ;' % esc(phys))
|
||||
if computed == "(dimensional)":
|
||||
bc = concept_for(deid)
|
||||
if not bc:
|
||||
raise SystemExit("No concept mapped for %s -- the XOR would fail." % deid)
|
||||
block.append(" pr:represents %s ;" % bc)
|
||||
block.append(' pr:hasStatus "DRAFT" ; pr:hasVersion "1.1" ; '
|
||||
'pr:hasSource "%s" .' % esc(source))
|
||||
dimensional.append("\n".join(block))
|
||||
else:
|
||||
block.append(' pr:hasStatus "DRAFT" ; pr:hasVersion "1.1" ; '
|
||||
'pr:hasSource "%s" .' % esc(source))
|
||||
measure.append("\n".join(block))
|
||||
return measure, dimensional
|
||||
|
||||
|
||||
def build_computed_by(elements):
|
||||
"""Metric -> its granular elements. Asserted from the metric side."""
|
||||
by_metric = {}
|
||||
name_to_id = {r[1]: r[0] for r in parse_backdoc(BACKDOC)["metrics"]}
|
||||
for r in elements:
|
||||
if r[3] == "(dimensional)":
|
||||
continue
|
||||
mid = name_to_id.get(r[3])
|
||||
if mid:
|
||||
by_metric.setdefault(mid, []).append(iri(r[0]))
|
||||
out = ["# --- BR-013 wiring: each mother metric and the variants that implement it",
|
||||
""]
|
||||
for mid, des in by_metric.items():
|
||||
chunks = [des[i:i + 4] for i in range(0, len(des), 4)]
|
||||
lines = [" , ".join(c) for c in chunks]
|
||||
out.append("%s pr:computedBy %s ." % (iri(mid), " ,\n ".join(lines)))
|
||||
return "\n".join(out), sum(len(v) for v in by_metric.values())
|
||||
|
||||
|
||||
def rebuild_has_element(text, elements):
|
||||
"""
|
||||
Re-attach every Data Element to its Data Object, from the back-doc column.
|
||||
|
||||
Without this the 95 generated measure elements would be orphans: no path to
|
||||
the physical layer, and no steward, since stewardship is inherited through
|
||||
the object. Not caught by any shape -- which is why it is worth doing here
|
||||
rather than waiting for the validator to complain.
|
||||
"""
|
||||
by_do = {}
|
||||
for r in elements:
|
||||
do = r[8].strip()
|
||||
if do and do != "-":
|
||||
by_do.setdefault(iri(do), []).append(iri(r[0]))
|
||||
n = 0
|
||||
for do, des in by_do.items():
|
||||
pat = re.compile(r'(^%s a pr:DataObject ;.*?)\n\s*pr:hasElement[^;]*;' % re.escape(do),
|
||||
re.S | re.M)
|
||||
chunks = [des[i:i + 4] for i in range(0, len(des), 4)]
|
||||
clause = "\n pr:hasElement " + " ,\n ".join(" , ".join(c) for c in chunks) + " ;"
|
||||
if pat.search(text):
|
||||
text = pat.sub(lambda m: m.group(1) + clause, text, count=1)
|
||||
n += 1
|
||||
return text, n, {k: len(v) for k, v in by_do.items()}
|
||||
|
||||
|
||||
def rebuild_has_metric(text, metrics):
|
||||
"""
|
||||
Re-point hasMetric on the Business Objects at the mother metrics.
|
||||
|
||||
Removing the old metric definitions is not enough: the Business Objects
|
||||
still name them, and because hasMetric has rdfs:range pr:Metric, RDFS
|
||||
entailment types those dangling IRIs as metrics. They then become focus
|
||||
nodes carrying no identifier, no name, no formula -- ten ghosts, six
|
||||
violations each. Deleting a subject means deleting what points at it.
|
||||
"""
|
||||
by_bo = {}
|
||||
for r in metrics:
|
||||
by_bo.setdefault(iri(r[2]), []).append(iri(r[0]))
|
||||
|
||||
text = re.sub(r'\n\s*pr:hasMetric[^;]*;', '', text)
|
||||
n = 0
|
||||
for bo, ms in by_bo.items():
|
||||
pat = re.compile(r'(^%s a pr:BusinessObject ;.*?)(\n\s*pr:hasStatus)' % re.escape(bo),
|
||||
re.S | re.M)
|
||||
if pat.search(text):
|
||||
text = pat.sub(r'\1\n pr:hasMetric %s ;\2' % " , ".join(ms), text, count=1)
|
||||
n += 1
|
||||
return text, n, by_bo
|
||||
|
||||
|
||||
def concept_block(c):
|
||||
i, ident, name, dom, st, definition = c
|
||||
return ("%s a pr:BusinessConcept ;\n"
|
||||
' pr:hasIdentifier "%s" ; pr:hasName "%s" ;\n'
|
||||
" pr:owningDomain %s ; pr:ownedBy %s ;\n"
|
||||
' pr:hasBusinessDefinition "%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, esc(name), dom, st, esc(definition)))
|
||||
|
||||
|
||||
def main():
|
||||
apply_changes = "--apply" in sys.argv
|
||||
for f in (TTL, BACKDOC):
|
||||
if not os.path.exists(f):
|
||||
print("Not found: %s" % f)
|
||||
sys.exit(2)
|
||||
|
||||
doc = parse_backdoc(BACKDOC)
|
||||
text = original = open(TTL, encoding="utf-8").read()
|
||||
report = []
|
||||
|
||||
# 1. drop the old metric and data element blocks
|
||||
n_old_m = len(re.findall(r'^ex:M_\w+ a pr:Metric ;', text, re.M))
|
||||
n_old_de = len(re.findall(r'^ex:DE_\w+ a pr:DataElement ;', text, re.M))
|
||||
text = re.sub(r'^ex:M_\w+ a pr:Metric ;.*?\.\s*\n(?=^ex:|\Z)', '', text, flags=re.S | re.M)
|
||||
text = re.sub(r'^ex:DE_\w+ a pr:DataElement ;.*?\.\s*\n(?=^ex:|\Z)', '', text, flags=re.S | re.M)
|
||||
report.append("removed %d old metrics and %d old data elements" % (n_old_m, n_old_de))
|
||||
|
||||
# 2. steward is inherited, not declared on a Data Object
|
||||
n_st = 0
|
||||
for m in re.finditer(r'^ex:DO_\w+ a pr:DataObject ;.*?\.\s*\n', text, re.S | re.M):
|
||||
block = m.group(0)
|
||||
new = re.sub(r'\s*pr:monitoredBy\s+ex:\w+\s*;', ' ;', block)
|
||||
new = re.sub(r';\s*;', ' ;', new)
|
||||
if new != block:
|
||||
n_st += 1
|
||||
text = text.replace(block, new, 1)
|
||||
report.append("removed monitoredBy from %d Data Objects (inherited now)" % n_st)
|
||||
|
||||
# 3. grain, dimension objects only
|
||||
n_g = 0
|
||||
for do, grain in GRAIN.items():
|
||||
pat = re.compile(r'(^%s a pr:DataObject ;.*?)(\n\s*pr:hasStatus)' % re.escape(do),
|
||||
re.S | re.M)
|
||||
if pat.search(text):
|
||||
text = pat.sub(r'\1\n pr:hasGrainElement %s ;\2' % " , ".join(grain), text, count=1)
|
||||
n_g += 1
|
||||
report.append("grain declared on %d dimension Data Objects "
|
||||
"(fact grain stays in references)" % n_g)
|
||||
|
||||
# 4. the three concepts the XOR forces us to name
|
||||
# idempotent: a concept already in the file is left alone. Duplicated
|
||||
# blocks would be INVISIBLE to SHACL -- identical triples merge under RDF
|
||||
# set semantics, so the graph validates while the file carries redundant
|
||||
# text. A script that can be re-run must check before it inserts.
|
||||
todo = [c for c in NEW_CONCEPTS
|
||||
if not re.search(r'^%s a ' % re.escape(c[0]), text, re.M)]
|
||||
if len(todo) < len(NEW_CONCEPTS):
|
||||
report.append("skipped %d concept(s) already present"
|
||||
% (len(NEW_CONCEPTS) - len(todo)))
|
||||
anchor = re.search(r'\n(?=ex:BO_\w+ a pr:BusinessObject ;)', text)
|
||||
blocks = "\n".join(concept_block(c) for c in todo)
|
||||
if todo:
|
||||
text = (text[:anchor.start()] + "\n\n"
|
||||
+ "# --- concepts required by the meaning XOR ---------------------------\n"
|
||||
+ "# Country, Marketing Entity and Fiscal Period had Data Elements but no\n"
|
||||
+ "# notion behind them. The rule forced them to be named.\n"
|
||||
+ blocks + "\n" + text[anchor.start():])
|
||||
report.append("added %d concepts required by the XOR (TO_ARBITRATE)" % len(todo))
|
||||
|
||||
# 5. metrics, elements, wiring
|
||||
measure, dimensional = build_elements(doc["elements"])
|
||||
wiring, n_wired = build_computed_by(doc["elements"])
|
||||
text = text.rstrip() + "\n\n\n" + build_metrics(doc["metrics"]) + "\n\n"
|
||||
text += ("# --- dimensional data elements --------------------------------------\n"
|
||||
"# Each represents the Concept it carries: the first of the two routes\n"
|
||||
"# to business meaning.\n\n" + "\n".join(dimensional) + "\n\n")
|
||||
text += ("# --- granular measure data elements ---------------------------------\n"
|
||||
"# Same calculation rule as their mother metric, different analysis\n"
|
||||
"# context. They reach meaning through computedBy, never directly.\n\n"
|
||||
+ "\n".join(measure) + "\n\n" + wiring + "\n")
|
||||
report.append("wrote %d mother metrics, %d dimensional and %d measure elements"
|
||||
% (len(doc["metrics"]), len(dimensional), len(measure)))
|
||||
report.append("wired %d computedBy links" % n_wired)
|
||||
|
||||
text, n_bo, per_bo = rebuild_has_metric(text, doc["metrics"])
|
||||
report.append("re-pointed hasMetric on %d Business Objects (%s)"
|
||||
% (n_bo, ", ".join("%s=%d" % (k.replace("ex:BO_", "BO-"), len(v))
|
||||
for k, v in sorted(per_bo.items()))))
|
||||
|
||||
text, n_do, counts = rebuild_has_element(text, doc["elements"])
|
||||
report.append("re-attached elements to %d Data Objects (%s)"
|
||||
% (n_do, ", ".join("%s=%d" % (k.replace("ex:DO_", "DO-"), v)
|
||||
for k, v in sorted(counts.items()))))
|
||||
|
||||
print()
|
||||
print("SODH BR-013 ALIGNMENT %s" % ("APPLY" if apply_changes else "DRY RUN"))
|
||||
print("=" * W)
|
||||
for line in report:
|
||||
print(" " + line)
|
||||
print("=" * W)
|
||||
defined = set(re.findall(r'^(ex:M_\w+) a pr:Metric ;', text, re.M))
|
||||
referenced = set(re.findall(r'ex:M_\w+', text))
|
||||
dangling = referenced - defined
|
||||
print(" dangling metric references: %s"
|
||||
% (", ".join(sorted(dangling)) if dangling else "none"))
|
||||
print(" Metrics %d | Data Elements %d | Concepts %d | computedBy %d | "
|
||||
"physicalName on Metric %d"
|
||||
% (len(re.findall(r'a pr:Metric', text)),
|
||||
len(re.findall(r'a pr:DataElement', text)),
|
||||
len(re.findall(r'a pr:BusinessConcept', text)),
|
||||
n_wired,
|
||||
len(re.findall(r'a pr:Metric ;[^.]*?physicalName', text, re.S))))
|
||||
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()
|
||||
@@ -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()
|
||||
@@ -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
+1117
-751
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,214 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
PR Data Meta Model - instance migration v1.0 -> v1.1
|
||||
=====================================================
|
||||
Rewrites instance files for the T-Box v1.1 vocabulary. Text-level rewriting on
|
||||
purpose: it preserves comments, ordering and formatting, which a parse-and-
|
||||
reserialise round trip through rdflib would destroy -- and those comments carry
|
||||
the arbitration notes.
|
||||
|
||||
USAGE
|
||||
python3 scripts/migrate_instances_v1_1.py # dry run, report only
|
||||
python3 scripts/migrate_instances_v1_1.py --apply # rewrite in place, .bak kept
|
||||
python3 scripts/migrate_instances_v1_1.py --apply f.ttl # a single file
|
||||
|
||||
WHAT IT DOES AUTOMATICALLY
|
||||
namespace /metamodel/0.9/ -> /metamodel/ (unversioned terms)
|
||||
belongsToDomain, belongsToSubDomain -> belongsTo
|
||||
implements -> represents
|
||||
hasSteward -> monitoredBy
|
||||
usesDataObject -> packages
|
||||
DataAsset -> DataInterface
|
||||
hasGrainDimension, hasGranularity -> flagged, not converted
|
||||
|
||||
WHAT IT REFUSES TO DO
|
||||
composedOf -> aboutConcept / usesConcept is NOT automated. Splitting it
|
||||
requires deciding which concept is the SUBJECT of each Business Object, and
|
||||
guessing that is exactly how v0.6 ended up with Calendar declared as being
|
||||
composed of Sell Out. The script lists the choices to be made and leaves
|
||||
composedOf in place until a human answers.
|
||||
|
||||
hasBusinessDefinition on a Business Object is likewise reported, not moved:
|
||||
the text must go to the Concept named by aboutConcept, which does not exist
|
||||
yet at the time this runs.
|
||||
"""
|
||||
import glob
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
|
||||
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
INSTANCES = os.path.join(REPO, "instances")
|
||||
W = 78
|
||||
|
||||
# (pattern, replacement, label) -- applied in order, word-bounded
|
||||
RENAMES = [
|
||||
(r"https://ontology\.pernod-ricard\.com/metamodel/0\.9/",
|
||||
"https://ontology.pernod-ricard.com/metamodel/", "namespace 0.9 -> unversioned"),
|
||||
(r"\bpr:belongsToDomain\b", "pr:belongsTo", "belongsToDomain -> belongsTo"),
|
||||
(r"\bpr:belongsToSubDomain\b", "pr:belongsTo", "belongsToSubDomain -> belongsTo"),
|
||||
(r"\bpr:implements\b", "pr:represents", "implements -> represents"),
|
||||
(r"\bpr:hasSteward\b", "pr:monitoredBy", "hasSteward -> monitoredBy"),
|
||||
(r"\bpr:usesDataObject\b", "pr:packages", "usesDataObject -> packages"),
|
||||
(r"\bpr:DataAsset\b", "pr:DataInterface", "DataAsset -> DataInterface"),
|
||||
]
|
||||
|
||||
# things a human must decide, reported per file
|
||||
MANUAL = [
|
||||
(r"\bpr:composedOf\b", "composedOf",
|
||||
"Split into aboutConcept (exactly one -- the SUBJECT) and usesConcept "
|
||||
"(the rest). Not automated: guessing the subject is how v0.6 ended up "
|
||||
"with Calendar composed of Sell Out."),
|
||||
(r"\bpr:isComponentOf\b", "isComponentOf",
|
||||
"Inverse of the above. Re-express as usesConcept from the Business Object."),
|
||||
(r"\bpr:hasGranularity\b", "hasGranularity",
|
||||
"Grain moved off the Metric. Re-express as hasGrainElement on the Data "
|
||||
"Object, pointing at the Data Elements that make a row unique."),
|
||||
(r"\bpr:hasGrainDimension\b", "hasGrainDimension",
|
||||
"Same: hasGrainElement on the Data Object, targeting Data Elements."),
|
||||
]
|
||||
|
||||
|
||||
def subject_blocks(text):
|
||||
"""Yield (subject, block) for each top-level statement."""
|
||||
for block in re.split(r"\.\s*\n(?=\S)", text):
|
||||
m = re.match(r"\s*(\S+)", block)
|
||||
if m:
|
||||
yield m.group(1), block
|
||||
|
||||
|
||||
def business_objects_with_definition(text):
|
||||
"""Business Objects still carrying hasBusinessDefinition -- must move to the Concept."""
|
||||
out = []
|
||||
for subj, block in subject_blocks(text):
|
||||
if "pr:BusinessObject" in block and "pr:hasBusinessDefinition" in block:
|
||||
out.append(subj)
|
||||
return out
|
||||
|
||||
|
||||
def composed_of_map(text):
|
||||
"""Business Object -> list of concepts, so the subject can be chosen."""
|
||||
out = {}
|
||||
for subj, block in subject_blocks(text):
|
||||
if "pr:composedOf" not in block:
|
||||
continue
|
||||
m = re.search(r"pr:composedOf\s+([^;.]+)", block)
|
||||
if m:
|
||||
concepts = [c.strip() for c in m.group(1).split(",") if c.strip()]
|
||||
out[subj] = concepts
|
||||
return out
|
||||
|
||||
|
||||
def process(path, apply_changes):
|
||||
original = open(path, encoding="utf-8").read()
|
||||
text = original
|
||||
applied = []
|
||||
|
||||
for pattern, repl, label in RENAMES:
|
||||
text, n = re.subn(pattern, repl, text)
|
||||
if n:
|
||||
applied.append((label, n))
|
||||
|
||||
pending = []
|
||||
for pattern, label, advice in MANUAL:
|
||||
n = len(re.findall(pattern, original))
|
||||
if n:
|
||||
pending.append((label, n, advice))
|
||||
|
||||
bo_defs = business_objects_with_definition(original)
|
||||
comp = composed_of_map(original)
|
||||
|
||||
print("=" * W)
|
||||
print(os.path.relpath(path, REPO))
|
||||
print("=" * W)
|
||||
|
||||
if applied:
|
||||
print(" AUTOMATIC")
|
||||
for label, n in applied:
|
||||
print(" %-42s %4d occurrence(s)" % (label, n))
|
||||
else:
|
||||
print(" AUTOMATIC nothing to rewrite")
|
||||
|
||||
if pending:
|
||||
print(" MANUAL")
|
||||
for label, n, advice in pending:
|
||||
print(" %-42s %4d occurrence(s)" % (label, n))
|
||||
for line in _wrap(advice, 68):
|
||||
print(" " + line)
|
||||
|
||||
if bo_defs:
|
||||
print(" BUSINESS DEFINITIONS TO MOVE")
|
||||
print(" %d Business Object(s) still carry hasBusinessDefinition." % len(bo_defs))
|
||||
print(" Meaning belongs to the Concept. Move each text to the Concept")
|
||||
print(" named by that object's aboutConcept, then delete it here.")
|
||||
for s in bo_defs[:12]:
|
||||
print(" - %s" % s)
|
||||
if len(bo_defs) > 12:
|
||||
print(" ... and %d more" % (len(bo_defs) - 12))
|
||||
|
||||
if comp:
|
||||
print(" SUBJECT TO CHOOSE (composedOf -> aboutConcept + usesConcept)")
|
||||
for subj, concepts in list(comp.items())[:12]:
|
||||
print(" %s" % subj)
|
||||
print(" candidates: %s" % ", ".join(concepts))
|
||||
if len(comp) > 12:
|
||||
print(" ... and %d more" % (len(comp) - 12))
|
||||
|
||||
changed = text != original
|
||||
if changed and apply_changes:
|
||||
shutil.copy2(path, path + ".bak")
|
||||
open(path, "w", encoding="utf-8").write(text)
|
||||
print(" WRITTEN (backup at %s.bak)" % os.path.basename(path))
|
||||
elif changed:
|
||||
print(" DRY RUN -- re-run with --apply to write")
|
||||
print()
|
||||
return changed, len(pending) + len(bo_defs) + len(comp)
|
||||
|
||||
|
||||
def _wrap(text, width):
|
||||
words, line, out = text.split(), "", []
|
||||
for w in words:
|
||||
if len(line) + len(w) + 1 > width:
|
||||
out.append(line)
|
||||
line = w
|
||||
else:
|
||||
line = (line + " " + w).strip()
|
||||
if line:
|
||||
out.append(line)
|
||||
return out
|
||||
|
||||
|
||||
def main():
|
||||
apply_changes = "--apply" in sys.argv
|
||||
args = [a for a in sys.argv[1:] if not a.startswith("-")]
|
||||
files = ([a if os.path.isabs(a) else os.path.join(REPO, a) for a in args]
|
||||
or sorted(glob.glob(os.path.join(INSTANCES, "*.ttl"))))
|
||||
files = [f for f in files if os.path.exists(f)]
|
||||
|
||||
if not files:
|
||||
print("No instance file found.")
|
||||
sys.exit(2)
|
||||
|
||||
print()
|
||||
print("PR META MODEL - INSTANCE MIGRATION v1.0 -> v1.1 %s"
|
||||
% ("APPLY" if apply_changes else "DRY RUN"))
|
||||
print()
|
||||
|
||||
total_changed = total_manual = 0
|
||||
for f in files:
|
||||
changed, manual = process(f, apply_changes)
|
||||
total_changed += int(changed)
|
||||
total_manual += manual
|
||||
|
||||
print("=" * W)
|
||||
print("%d file(s) rewritten, %d decision(s) left to a human."
|
||||
% (total_changed, total_manual))
|
||||
if total_manual:
|
||||
print("Run the validator after the manual work: the XOR-of-meaning shape")
|
||||
print("on Data Elements will not pass until the concepts are wired.")
|
||||
print("=" * W)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -0,0 +1,519 @@
|
||||
@prefix sh: <http://www.w3.org/ns/shacl#> .
|
||||
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
|
||||
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
|
||||
@prefix dcterms: <http://purl.org/dc/terms/> .
|
||||
@prefix pr: <https://ontology.pernod-ricard.com/metamodel/> .
|
||||
@prefix prs: <https://ontology.pernod-ricard.com/metamodel/shapes/> .
|
||||
|
||||
#################################################################
|
||||
# PR DATA META MODEL - SHACL SHAPES v0.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 captured half of the graph that would fail on
|
||||
# every one of ~50 000 Snowflake columns. Defined and Captured 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:modified "2026-07-30"^^xsd:date ;
|
||||
rdfs:comment "Executable form of the rulebook. Validated by scripts/run_shacl_validation.py." .
|
||||
|
||||
|
||||
#################################################################
|
||||
# IDENTITY AND LIFECYCLE -- defined objects only
|
||||
#################################################################
|
||||
|
||||
prs:DefinedObjectShape a sh:NodeShape ;
|
||||
sh:targetClass pr:DefinedObject ;
|
||||
rdfs:comment "Applies to everything governance writes. Captured objects are deliberately exempt: see CapturedObjectShape." ;
|
||||
|
||||
sh:property [
|
||||
sh:path pr:identifier ; sh:datatype xsd:string ;
|
||||
sh:minCount 1 ; sh:maxCount 1 ;
|
||||
sh:severity sh:Violation ;
|
||||
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:property [
|
||||
sh:path pr:name ; sh:datatype xsd:string ;
|
||||
sh:minCount 1 ; sh:maxCount 1 ; sh:minLength 3 ;
|
||||
sh:severity sh:Violation ;
|
||||
sh:message "Every defined object carries exactly one canonical name." ] ;
|
||||
|
||||
sh:property [
|
||||
sh:path pr:status ; 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:version ; sh:maxCount 1 ;
|
||||
sh:severity sh:Warning ;
|
||||
sh:message "A published object should carry a version." ] .
|
||||
|
||||
|
||||
prs:PublishedShape a sh:NodeShape ;
|
||||
sh:targetClass pr:DefinedObject ;
|
||||
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:status "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:status "PUBLISHED" .
|
||||
FILTER NOT EXISTS { $this pr:owningDomain ?d }
|
||||
}""" ] .
|
||||
|
||||
|
||||
#################################################################
|
||||
# OBSERVED OBJECTS -- captured, never hand-edited
|
||||
#################################################################
|
||||
|
||||
prs:CapturedObjectShape a sh:NodeShape ;
|
||||
sh:targetClass pr:CapturedObject ;
|
||||
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 captured 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 "Captured objects are not attributed directly -- ownership flows from the Data Element mapped to them." ] .
|
||||
|
||||
|
||||
# inSchema is deliberately NOT required anywhere. Neo4j has no schema and
|
||||
# BigQuery calls the level a dataset; a minCount 1 here would break on the
|
||||
# first harvest outside Snowflake.
|
||||
prs:DataStructureShape a sh:NodeShape ;
|
||||
sh:targetClass pr:DataStructure ;
|
||||
sh:property [
|
||||
sh:path pr:inSchema ; sh:maxCount 1 ;
|
||||
sh:severity sh:Info ;
|
||||
sh:message "Optional on purpose: not every engine has a schema level." ] ;
|
||||
sh:property [
|
||||
sh:path pr:sourcedFrom ; sh:maxCount 1 ;
|
||||
sh:severity sh:Info ] .
|
||||
|
||||
|
||||
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:businessDefinition ; 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." ] ;
|
||||
|
||||
sh:property [
|
||||
sh:path pr:sourcedFrom ; sh:maxCount 0 ;
|
||||
sh:severity sh:Violation ;
|
||||
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." ] .
|
||||
|
||||
|
||||
#################################################################
|
||||
# BUSINESS CONCEPT AND METRIC
|
||||
#################################################################
|
||||
|
||||
prs:BusinessConceptShape a sh:NodeShape ;
|
||||
sh:targetClass pr:BusinessConcept ;
|
||||
rdfs:comment "Note what is NOT here: no requirement that a Concept be realized by any Data Element or measured by any Metric. The business layer is allowed to run ahead of the data. Coverage is a dashboard metric, not a rule." ;
|
||||
|
||||
sh:property [
|
||||
sh:path pr:businessDefinition ; sh:datatype xsd:string ;
|
||||
sh:minCount 1 ; sh:maxCount 1 ; sh:minLength 20 ;
|
||||
sh:severity sh:Violation ;
|
||||
sh:message "A Business Concept exists to carry meaning: it must hold exactly one business definition of at least 20 characters." ] ;
|
||||
|
||||
sh:property [
|
||||
sh:path pr:formula ; sh:maxCount 0 ;
|
||||
sh:severity sh:Violation ;
|
||||
sh:message "BR-003 -- the presence of a formula is what makes something a Metric. A Concept with a formula is a misclassified Metric." ] ;
|
||||
|
||||
sh:property [
|
||||
sh:path pr:sourcedFrom ; sh:maxCount 0 ;
|
||||
sh:severity sh:Violation ;
|
||||
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." ] .
|
||||
|
||||
|
||||
prs:MetricShape a sh:NodeShape ;
|
||||
sh:targetClass pr:Metric ;
|
||||
|
||||
sh:property [
|
||||
sh:path pr:formula ; sh:datatype xsd:string ;
|
||||
sh:minCount 1 ; sh:maxCount 1 ;
|
||||
sh:severity sh:Violation ;
|
||||
sh:message "BR-003 -- a Metric is a measurement with a calculation rule. No formula, no Metric." ] ;
|
||||
|
||||
sh:property [
|
||||
sh:path pr:unit ; sh:minCount 1 ;
|
||||
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:property [
|
||||
sh:path pr:measures ; sh:class pr:BusinessConcept ;
|
||||
sh:minCount 1 ;
|
||||
sh:severity sh:Violation ;
|
||||
sh:message "A Metric measures at least one Business Concept -- this is how a measure element reaches business meaning." ] ;
|
||||
|
||||
sh:property [
|
||||
sh:path pr:hasGranularity ; sh:maxCount 0 ;
|
||||
sh:severity sh:Warning ;
|
||||
sh:message "Grain moved off the Metric in v1.1. A Metric is a calculation rule and has no rows; grain belongs to the Data Object, stated with hasGrainElement and checkable against the real primary key." ] ;
|
||||
|
||||
sh:property [
|
||||
sh:path pr:physicalName ; sh:maxCount 0 ;
|
||||
sh:severity sh:Violation ;
|
||||
sh:message "A Metric must not carry a physical name -- that is a layer leak. Under BR-013 the physical name belongs on the Data Element." ] .
|
||||
|
||||
|
||||
# The source of truth of a Metric is asserted by exception, then checked.
|
||||
# "Where can I get this metric" is a first-order question and the derivation
|
||||
# chain will have gaps before harvesting, so the assertion stands on its own --
|
||||
# but it must agree with the elements that implement the metric, or the two
|
||||
# will drift the day an element moves.
|
||||
prs:MetricSourceShape a sh:NodeShape ;
|
||||
sh:targetClass pr:Metric ;
|
||||
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:severity sh:Violation ;
|
||||
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:sparql [
|
||||
sh:severity sh:Warning ;
|
||||
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)
|
||||
}""" ] .
|
||||
|
||||
|
||||
# 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:status "PUBLISHED" .
|
||||
FILTER NOT EXISTS { $this pr:computedBy ?de }
|
||||
}""" ] .
|
||||
|
||||
|
||||
#################################################################
|
||||
# DATA OBJECT -- the pivot, and grain
|
||||
#################################################################
|
||||
|
||||
prs:DataObjectShape a sh:NodeShape ;
|
||||
sh:targetClass pr:DataObject ;
|
||||
|
||||
sh:property [
|
||||
sh:path pr:represents ; sh:class pr:BusinessObject ;
|
||||
sh:minCount 1 ; sh:maxCount 1 ;
|
||||
sh:severity sh:Violation ;
|
||||
sh:message "BR-006 -- a Data Object represents exactly one Business Object. This is the pivot: the single junction between governance and technology." ] ;
|
||||
|
||||
sh:property [
|
||||
sh:path pr:hasElement ; sh:class pr:DataElement ;
|
||||
sh:minCount 1 ;
|
||||
sh:severity sh:Violation ;
|
||||
sh:message "A Data Object holds at least one Data Element." ] ;
|
||||
|
||||
sh:property [
|
||||
sh:path pr:hasGrainElement ; sh:class pr:DataElement ;
|
||||
sh:severity sh:Warning ;
|
||||
sh:message "State the grain: the elements whose combination makes a row unique. Checkable against the harvested primary key." ] ;
|
||||
|
||||
sh:property [
|
||||
sh:path pr:monitoredBy ; sh:maxCount 0 ;
|
||||
sh:severity sh:Warning ;
|
||||
sh:message "Stewardship is declared on the Business Object and inherited through represents. Declaring it here allows two answers to the same question." ] ;
|
||||
|
||||
sh:property [
|
||||
sh:path pr:sourcedFrom ; sh:class pr:System ; sh:maxCount 1 ;
|
||||
sh:severity sh:Warning ;
|
||||
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:property [
|
||||
sh:path pr:storedIn ; sh:class pr:DataStructure ;
|
||||
sh:severity sh:Warning ;
|
||||
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:property [
|
||||
sh:path pr:materializedAs ; sh:maxCount 0 ;
|
||||
sh:severity sh:Warning ;
|
||||
sh:message "Deprecated in v1.4: storedIn now serves both granularities." ] .
|
||||
|
||||
|
||||
# A declared grain element must actually belong to the object it grains.
|
||||
prs:GrainConsistencyShape a sh:NodeShape ;
|
||||
sh:targetClass pr:DataObject ;
|
||||
sh:sparql [
|
||||
sh:severity sh:Violation ;
|
||||
sh:message "A grain element must be one of the object's own Data Elements." ;
|
||||
sh:select """
|
||||
PREFIX pr: <https://ontology.pernod-ricard.com/metamodel/>
|
||||
SELECT $this ?grain WHERE {
|
||||
$this pr:hasGrainElement ?grain .
|
||||
FILTER NOT EXISTS { $this pr:hasElement ?grain }
|
||||
}""" ] .
|
||||
|
||||
|
||||
#################################################################
|
||||
# DATA ELEMENT -- the XOR of meaning
|
||||
#################################################################
|
||||
|
||||
prs:DataElementShape a sh:NodeShape ;
|
||||
sh:targetClass pr:DataElement ;
|
||||
|
||||
sh:property [
|
||||
sh:path pr:physicalName ; sh:datatype xsd:string ;
|
||||
sh:maxCount 1 ;
|
||||
sh:severity sh:Warning ;
|
||||
sh:message "NR-009 -- the physical name is recorded verbatim and never altered." ] ;
|
||||
|
||||
sh:property [
|
||||
sh:path pr:fullyQualifiedName ; sh:maxCount 1 ;
|
||||
sh:severity sh:Info ] ;
|
||||
|
||||
sh:property [
|
||||
sh:path pr:unit ; sh:maxCount 1 ;
|
||||
sh:severity sh:Violation ;
|
||||
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: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 defined 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:maxCount 1 ;
|
||||
sh:severity sh:Warning ;
|
||||
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: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:exposesMetric ; sh:class pr:Metric ;
|
||||
sh:maxCount 1 ;
|
||||
sh:severity sh:Info ;
|
||||
sh:message "Which certified Metric this BI field implements. Comparing expressions across fields realizing one Metric is the harmonisation measure." ] .
|
||||
|
||||
|
||||
#################################################################
|
||||
# NAMING -- portable regex, no lookbehind
|
||||
#################################################################
|
||||
|
||||
prs:NamingShape a sh:NodeShape ;
|
||||
sh:targetClass pr:DefinedObject ;
|
||||
sh:property [
|
||||
sh:path pr:name ;
|
||||
sh:not [ sh:pattern "_NM$" ] ;
|
||||
sh:severity sh:Warning ;
|
||||
sh:message "NR-001 -- a canonical name must not end in a physical suffix. Written as sh:not[sh:pattern] rather than a lookbehind: sh:pattern is normatively XPath 2.0 regex, which has no lookbehind, so the v0.1.1 form only worked under pySHACL." ] ;
|
||||
sh:property [
|
||||
sh:path pr:name ;
|
||||
sh:not [ sh:pattern "^[a-z_]+$" ] ;
|
||||
sh:severity sh:Warning ;
|
||||
sh:message "NR-002 -- a canonical name is written for humans, not as a snake_case identifier." ] .
|
||||
@@ -0,0 +1,312 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
PR Data Meta Model - SHACL validation runner
|
||||
=============================================
|
||||
The automated gate behind rule DQ-011. Validates an instance graph against the
|
||||
executable rulebook (SHACL shapes), with the T-Box supplying subclass
|
||||
entailment.
|
||||
|
||||
USAGE
|
||||
python3 scripts/run_shacl_validation.py # all instances
|
||||
python3 scripts/run_shacl_validation.py instances/sodh.ttl # one file
|
||||
python3 scripts/run_shacl_validation.py --quiet # CI mode
|
||||
|
||||
Paths resolve against the repository root (the parent of scripts/), so the
|
||||
script runs correctly from anywhere.
|
||||
|
||||
EXIT CODES
|
||||
0 conforms, no blocking violation -> eligible for PUBLISHED
|
||||
1 violations found -> stays DRAFT / UNDER_REVIEW
|
||||
2 the run itself is not trustworthy -> a green result would be meaningless
|
||||
|
||||
Exit code 2 is the important one. A validation gate that passes because no
|
||||
shape matched anything is worse than one that fails: it reports success while
|
||||
checking nothing. The pre-flight below refuses to let that happen.
|
||||
|
||||
DEPENDENCIES
|
||||
pyshacl, rdflib. On GrosseBertha these need a venv built with the STABLE
|
||||
python (never the DSM system python, which moves between DSM releases):
|
||||
|
||||
/usr/local/bin/python3.9 -m venv venv
|
||||
. venv/bin/activate
|
||||
pip install pyshacl rdflib
|
||||
"""
|
||||
import glob
|
||||
import os
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
|
||||
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
ONTOLOGY = os.path.join(REPO, "ontology", "pr_metamodel.ttl")
|
||||
SHAPES = os.path.join(REPO, "shapes", "pr_metamodel_shapes.ttl")
|
||||
INSTANCES = os.path.join(REPO, "instances")
|
||||
|
||||
W = 78
|
||||
BAR = "=" * W
|
||||
|
||||
|
||||
def rel(path):
|
||||
"""Path relative to the repo root, for readable output."""
|
||||
try:
|
||||
return os.path.relpath(path, REPO)
|
||||
except ValueError:
|
||||
return path
|
||||
|
||||
|
||||
def die(code, title, lines):
|
||||
print(BAR)
|
||||
print("PR META MODEL - SHACL VALIDATION")
|
||||
print(BAR)
|
||||
print(title)
|
||||
print("-" * W)
|
||||
for line in lines:
|
||||
print(" " + line)
|
||||
print(BAR)
|
||||
sys.exit(code)
|
||||
|
||||
|
||||
def load_deps():
|
||||
"""
|
||||
Import the validation stack.
|
||||
|
||||
Catches every exception, not just ImportError: a dependency that installs
|
||||
but cannot be imported (pyshacl built for a newer Python than the venv,
|
||||
an rdflib major-version clash) raises TypeError or AttributeError at import
|
||||
time. Letting those escape would end the process with an exit code CI reads
|
||||
as "the model has violations", when in fact validation never started.
|
||||
Anything wrong here is exit 2 -- the run is not trustworthy.
|
||||
"""
|
||||
try:
|
||||
from pyshacl import validate
|
||||
from rdflib import Graph
|
||||
from rdflib.namespace import RDF, SH
|
||||
return validate, Graph, RDF, SH
|
||||
except Exception as exc:
|
||||
hint = []
|
||||
if "unsupported operand type" in str(exc) or "|" in str(exc):
|
||||
hint = [
|
||||
"This is PEP 604 syntax (str | X) evaluated at import time:",
|
||||
"the installed pyshacl needs Python 3.10+, this venv is %d.%d."
|
||||
% (sys.version_info[0], sys.version_info[1]),
|
||||
"pyshacl advertises 3.9 support but its code does not honour it,",
|
||||
"so pip installs a version that cannot be imported.",
|
||||
"",
|
||||
"Pin a release that really supports this interpreter:",
|
||||
' pip install "pyshacl==0.26.0" "rdflib>=6.3.2,<7.1"',
|
||||
"or rebuild the venv on a newer Python if one exists.",
|
||||
]
|
||||
else:
|
||||
hint = [
|
||||
"Build the venv with the stable python, never the DSM system one:",
|
||||
" /usr/local/bin/python3.9 -m venv venv",
|
||||
" . venv/bin/activate && pip install pyshacl rdflib",
|
||||
]
|
||||
die(2, "DEPENDENCY UNUSABLE -- validation did not run",
|
||||
["%s: %s" % (type(exc).__name__, exc), ""] + hint)
|
||||
|
||||
|
||||
def term_namespaces(graph, predicate=None):
|
||||
"""
|
||||
Namespaces actually used by pr: terms in a graph, with usage counts.
|
||||
|
||||
Only real IRIs count. Literals are skipped: SPARQL constraint bodies
|
||||
(sh:select) embed their own PREFIX declarations, and counting those would
|
||||
put raw query text in the diagnostic table.
|
||||
"""
|
||||
counts = defaultdict(int)
|
||||
for s, p, o in graph:
|
||||
for term in (s, p, o):
|
||||
text = str(term)
|
||||
if "ontology.pernod-ricard.com/metamodel" not in text:
|
||||
continue
|
||||
if not text.startswith("http") or any(c in text for c in " \t\n\r<>\""):
|
||||
continue # literal, not an IRI
|
||||
counts[text.rsplit("/", 1)[0] + "/"] += 1
|
||||
return counts
|
||||
|
||||
|
||||
def preflight(data, shapes, onto, data_files):
|
||||
"""
|
||||
Refuse to produce a green result that means nothing.
|
||||
|
||||
Two ways a SHACL run passes while checking nothing:
|
||||
1. the shapes target classes in a namespace the instances never use;
|
||||
2. the ontology declares its classes in a third namespace, so subclass
|
||||
entailment cannot reach the instances and every shape targeting an
|
||||
abstract parent (MetaModelObject) finds zero focus nodes.
|
||||
Both look identical to a caller reading only "CONFORMS: True".
|
||||
"""
|
||||
problems = []
|
||||
ns_problem = False
|
||||
|
||||
if not len(data):
|
||||
problems.append("The instance graph is empty -- nothing to validate.")
|
||||
|
||||
ns_data = term_namespaces(data)
|
||||
ns_shapes = term_namespaces(shapes)
|
||||
ns_onto = term_namespaces(onto) if onto is not None else {}
|
||||
|
||||
if ns_data and ns_shapes and not (set(ns_data) & set(ns_shapes)):
|
||||
ns_problem = True
|
||||
problems.append(
|
||||
"Shapes and instances use disjoint term namespaces -- no shape can "
|
||||
"match any node.")
|
||||
|
||||
if ns_onto and ns_data and not (set(ns_onto) & set(ns_data)):
|
||||
ns_problem = True
|
||||
problems.append(
|
||||
"The T-Box declares its terms in a namespace the instances never "
|
||||
"use. Subclass entailment cannot reach the data, so every shape "
|
||||
"targeting an abstract parent finds zero focus nodes.")
|
||||
|
||||
if problems:
|
||||
lines = list(problems)
|
||||
lines.append("")
|
||||
lines.append("Namespaces in use:")
|
||||
for label, counts in (("instances", ns_data), ("shapes", ns_shapes),
|
||||
("ontology", ns_onto)):
|
||||
if counts:
|
||||
for ns, n in sorted(counts.items(), key=lambda kv: -kv[1]):
|
||||
lines.append(" %-10s %-52s %6d refs" % (label, ns, n))
|
||||
else:
|
||||
lines.append(" %-10s (none)" % label)
|
||||
if ns_problem:
|
||||
lines.append("")
|
||||
lines.append("Fix: align the namespaces before trusting any result.")
|
||||
lines.append(" sed -i 's|/metamodel/0.9/|/metamodel/|g' "
|
||||
"instances/*.ttl shapes/*.ttl")
|
||||
die(2, "PRE-FLIGHT FAILED -- a result here would be meaningless", lines)
|
||||
|
||||
|
||||
def main():
|
||||
args = [a for a in sys.argv[1:] if not a.startswith("-")]
|
||||
quiet = "--quiet" in sys.argv or "-q" in sys.argv
|
||||
|
||||
validate, Graph, RDF, SH = load_deps()
|
||||
|
||||
# ---- resolve inputs ---------------------------------------------------
|
||||
if args:
|
||||
data_files = [a if os.path.isabs(a) else os.path.join(REPO, a) for a in args]
|
||||
else:
|
||||
data_files = sorted(glob.glob(os.path.join(INSTANCES, "*.ttl")))
|
||||
|
||||
missing = [f for f in data_files if not os.path.exists(f)]
|
||||
if missing or not data_files:
|
||||
die(2, "NO INSTANCE DATA -- validation did not run",
|
||||
[rel(f) + " not found" for f in missing] or
|
||||
["instances/ contains no .ttl file"])
|
||||
|
||||
if not os.path.exists(SHAPES):
|
||||
die(2, "NO SHAPES -- validation did not run",
|
||||
[rel(SHAPES) + " not found"])
|
||||
|
||||
# ---- parse ------------------------------------------------------------
|
||||
data = Graph()
|
||||
for f in data_files:
|
||||
try:
|
||||
data.parse(f, format="turtle")
|
||||
except Exception as exc:
|
||||
die(2, "PARSE ERROR -- validation did not run",
|
||||
[rel(f), str(exc)])
|
||||
|
||||
try:
|
||||
shapes = Graph().parse(SHAPES, format="turtle")
|
||||
except Exception as exc:
|
||||
die(2, "PARSE ERROR in shapes -- validation did not run",
|
||||
[rel(SHAPES), str(exc)])
|
||||
|
||||
onto = None
|
||||
if os.path.exists(ONTOLOGY):
|
||||
try:
|
||||
onto = Graph().parse(ONTOLOGY, format="turtle")
|
||||
except Exception as exc:
|
||||
die(2, "PARSE ERROR in ontology -- validation did not run",
|
||||
[rel(ONTOLOGY), str(exc)])
|
||||
|
||||
preflight(data, shapes, onto, data_files)
|
||||
|
||||
# ---- validate ---------------------------------------------------------
|
||||
conforms, results, _ = validate(
|
||||
data_graph=data,
|
||||
shacl_graph=shapes,
|
||||
ont_graph=onto,
|
||||
inference="rdfs", # a Metric is also a MetaModelObject
|
||||
advanced=True, # SPARQL constraints (NR-002, OW-007, DQ-006)
|
||||
abort_on_first=False,
|
||||
meta_shacl=False,
|
||||
)
|
||||
|
||||
def shape_label(g, res, SH):
|
||||
"""
|
||||
A readable name for the shape that fired.
|
||||
|
||||
sh:sourceShape on a property constraint is a blank node, which prints as
|
||||
'ne775aabb...' and tells the reader nothing. Fall back, in order, to the
|
||||
constrained path, then to the named NodeShape that owns the blank node,
|
||||
then to the constraint component.
|
||||
"""
|
||||
shape = g.value(res, SH.sourceShape)
|
||||
if shape is not None and not str(shape).startswith("n"):
|
||||
name = str(shape).rsplit("/", 1)[-1]
|
||||
if name and not name.startswith("N"):
|
||||
return name
|
||||
path = g.value(res, SH.resultPath)
|
||||
if path is not None:
|
||||
owner = None
|
||||
for s_, p_, o_ in g.triples((None, SH.property, shape)):
|
||||
owner = s_
|
||||
break
|
||||
base = str(path).rsplit("/", 1)[-1].rsplit("#", 1)[-1]
|
||||
if owner is not None and not str(owner).startswith("n"):
|
||||
return "%s / %s" % (str(owner).rsplit("/", 1)[-1], base)
|
||||
return "path %s" % base
|
||||
comp = g.value(res, SH.sourceConstraintComponent)
|
||||
if comp is not None:
|
||||
return str(comp).rsplit("#", 1)[-1]
|
||||
return "(unnamed shape)"
|
||||
|
||||
rows = []
|
||||
for res in results.subjects(RDF.type, SH.ValidationResult):
|
||||
sev = str(results.value(res, SH.resultSeverity)).rsplit("#", 1)[-1]
|
||||
focus = str(results.value(res, SH.focusNode)).rsplit("/", 1)[-1]
|
||||
msg = str(results.value(res, SH.resultMessage) or "")
|
||||
rows.append((sev, shape_label(results, res, SH), focus, msg))
|
||||
|
||||
violations = sum(1 for r in rows if r[0] == "Violation")
|
||||
warnings = len(rows) - violations
|
||||
|
||||
# ---- report -----------------------------------------------------------
|
||||
print(BAR)
|
||||
print("PR META MODEL - SHACL VALIDATION")
|
||||
print(" data : " + ", ".join(rel(f) for f in data_files))
|
||||
print(" shapes : " + rel(SHAPES))
|
||||
print(" ontology : " + (rel(ONTOLOGY) if onto is not None else "(absent - no entailment)"))
|
||||
print(" triples : %d data / %d shapes" % (len(data), len(shapes)))
|
||||
print(BAR)
|
||||
print("CONFORMS : %s" % conforms)
|
||||
print("RESULTS : %d total | %d BLOCKING | %d MAJOR (warning)"
|
||||
% (len(rows), violations, warnings))
|
||||
|
||||
if rows and not quiet:
|
||||
print("-" * W)
|
||||
by_shape = defaultdict(list)
|
||||
for sev, src, focus, msg in rows:
|
||||
by_shape[(sev, src)].append((focus, msg))
|
||||
# blocking first, then by volume: the rules to fix first come first
|
||||
order = sorted(by_shape.items(),
|
||||
key=lambda kv: (kv[0][0] != "Violation", -len(kv[1])))
|
||||
for (sev, src), items in order:
|
||||
tag = "BLOCK" if sev == "Violation" else "WARN "
|
||||
print("[%s] %-42s %3d node(s)" % (tag, src or "(unnamed shape)", len(items)))
|
||||
print(" %s" % (items[0][1][:66] or "(no message)"))
|
||||
for focus, _ in items[:6]:
|
||||
print(" - %s" % focus)
|
||||
if len(items) > 6:
|
||||
print(" ... and %d more" % (len(items) - 6))
|
||||
|
||||
print(BAR)
|
||||
sys.exit(0 if violations == 0 else 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+501
-109
@@ -1,127 +1,519 @@
|
||||
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
|
||||
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
|
||||
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
|
||||
@prefix sh: <http://www.w3.org/ns/shacl#> .
|
||||
@prefix pr: <https://ontology.pernod-ricard.com/metamodel/0.9/> .
|
||||
@prefix prsh: <https://ontology.pernod-ricard.com/metamodel/0.9/shapes/> .
|
||||
@prefix sh: <http://www.w3.org/ns/shacl#> .
|
||||
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
|
||||
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
|
||||
@prefix dcterms: <http://purl.org/dc/terms/> .
|
||||
@prefix pr: <https://ontology.pernod-ricard.com/metamodel/> .
|
||||
@prefix prs: <https://ontology.pernod-ricard.com/metamodel/shapes/> .
|
||||
|
||||
#################################################################
|
||||
# PR Data Meta Model - SHACL Shapes v0.1.1
|
||||
# Executable encoding of BLOCKING (sh:Violation) & MAJOR (sh:Warning) rules.
|
||||
# FIX v0.1.1 (2026-07-23): NR-002 uniqueness query now restricts ?cls to
|
||||
# LEAF classes (via FILTER ... IN) so that objects of different layers that
|
||||
# share a name (e.g. Sub-Domain "Sell Out" vs Concept "Sell Out") are NOT
|
||||
# reported as duplicates. Rule stays "unique WITHIN its class".
|
||||
# Run: python run_shacl_validation.py SODH_data.ttl pr_metamodel_shapes_v0.1.ttl pr_metamodel_v0.9.ttl
|
||||
# PR DATA META MODEL - SHACL SHAPES v0.2
|
||||
# Aligned with T-Box v1.1 and Rulebook v0.3.x
|
||||
#
|
||||
# WHAT CHANGED vs v0.1.1
|
||||
# ----------------------
|
||||
# 1. SPLIT BY PROVENANCE. v0.1.1 targeted pr:MetaModelObject and demanded an
|
||||
# identifier, a name, an owner, an owning domain, a status and a version on
|
||||
# everything. Applied to the captured half of the graph that would fail on
|
||||
# every one of ~50 000 Snowflake columns. Defined and Captured objects now
|
||||
# have separate shapes with separate expectations.
|
||||
#
|
||||
# 2. SCOPE FOR THE UNIFIED RELATIONS. belongsTo and represents deliberately
|
||||
# carry no rdfs:domain/range in the T-Box, because each serves two classes
|
||||
# with different targets. The scope lives here instead, per class.
|
||||
#
|
||||
# 3. MEANING IS AN XOR, NOT A CHECKLIST. A Data Element reaches business
|
||||
# meaning by exactly one route. sh:xone rejects both zero routes and two.
|
||||
#
|
||||
# 4. NO COVERAGE RULES. Nothing here requires a Business Concept to be
|
||||
# realized, or a Business Object to hold N concepts. That was BR-004, and
|
||||
# encoding a coverage expectation as a blocking constraint does not produce
|
||||
# coverage -- it produces padding. Coverage is a dashboard metric.
|
||||
#
|
||||
# 5. PORTABLE REGEX. No lookbehind: sh:pattern is normatively XPath 2.0
|
||||
# regex, which does not support it. Written as sh:not [ sh:pattern ... ].
|
||||
#
|
||||
# SEVERITY MAPPING
|
||||
# sh:Violation -> BLOCKING, cannot reach PUBLISHED, fails CI (DQ-011)
|
||||
# sh:Warning -> MAJOR, must be reviewed, does not block
|
||||
# sh:Info -> GUIDELINE, informational
|
||||
#################################################################
|
||||
|
||||
prsh:MetaModelObjectShape a sh:NodeShape ;
|
||||
sh:targetClass pr:MetaModelObject ;
|
||||
sh:property [ sh:path pr:hasIdentifier ; sh:minCount 1 ; sh:maxCount 1 ; sh:datatype xsd:string ;
|
||||
sh:severity sh:Violation ; sh:message "DQ-001 (BLOCKING): exactly one hasIdentifier." ] ;
|
||||
sh:property [ sh:path pr:hasName ; sh:minCount 1 ; sh:maxCount 1 ; sh:datatype xsd:string ; sh:minLength 2 ;
|
||||
sh:severity sh:Violation ; sh:message "DQ-002 (BLOCKING): exactly one non-empty hasName." ] ;
|
||||
sh:property [ sh:path pr:ownedBy ; sh:minCount 1 ; sh:class pr:Actor ;
|
||||
sh:severity sh:Violation ; sh:message "OW-001 (BLOCKING): at least one owner (Actor)." ] ;
|
||||
sh:property [ sh:path pr:owningDomain ; sh:minCount 1 ; sh:maxCount 1 ; sh:class pr:DataDomain ;
|
||||
sh:severity sh:Violation ; sh:message "OW-001 (BLOCKING): exactly one owning Data Domain." ] ;
|
||||
sh:property [ sh:path pr:hasStatus ; sh:minCount 1 ; sh:in ( "DRAFT" "UNDER_REVIEW" "PUBLISHED" "DEPRECATED" ) ;
|
||||
sh:severity sh:Warning ; sh:message "DQ-012 (MAJOR): hasStatus in controlled list." ] ;
|
||||
sh:property [ sh:path pr:hasVersion ; sh:minCount 1 ;
|
||||
sh:severity sh:Warning ; sh:message "DQ-012 (MAJOR): hasVersion required." ] .
|
||||
<https://ontology.pernod-ricard.com/metamodel/shapes/>
|
||||
dcterms:title "PR Data Meta Model - SHACL Shapes" ;
|
||||
dcterms:modified "2026-07-30"^^xsd:date ;
|
||||
rdfs:comment "Executable form of the rulebook. Validated by scripts/run_shacl_validation.py." .
|
||||
|
||||
prsh:BackDocSourceShape a sh:NodeShape ;
|
||||
sh:targetClass pr:MetaModelObject ;
|
||||
sh:sparql [ sh:severity sh:Violation ;
|
||||
sh:message "DQ-006 (BLOCKING): a DRAFT back-doc object MUST carry hasSource." ;
|
||||
sh:select """
|
||||
PREFIX pr: <https://ontology.pernod-ricard.com/metamodel/0.9/>
|
||||
SELECT $this WHERE { $this pr:hasStatus "DRAFT" .
|
||||
FILTER NOT EXISTS { $this pr:hasSource ?s } }
|
||||
""" ] .
|
||||
|
||||
prsh:CanonicalNameShape a sh:NodeShape ;
|
||||
sh:targetClass pr:MetaModelObject ;
|
||||
sh:property [ sh:path pr:hasName ;
|
||||
sh:pattern "^(?!GL_)(?!ID_).*(?<!_NM)(?<!_CD)(?<!_FLG)(?<!_NR)(?<!_DT)$" ; sh:flags "i" ;
|
||||
#################################################################
|
||||
# IDENTITY AND LIFECYCLE -- defined objects only
|
||||
#################################################################
|
||||
|
||||
prs:DefinedObjectShape a sh:NodeShape ;
|
||||
sh:targetClass pr:DefinedObject ;
|
||||
rdfs:comment "Applies to everything governance writes. Captured objects are deliberately exempt: see CapturedObjectShape." ;
|
||||
|
||||
sh:property [
|
||||
sh:path pr:identifier ; sh:datatype xsd:string ;
|
||||
sh:minCount 1 ; sh:maxCount 1 ;
|
||||
sh:severity sh:Violation ;
|
||||
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:property [
|
||||
sh:path pr:name ; sh:datatype xsd:string ;
|
||||
sh:minCount 1 ; sh:maxCount 1 ; sh:minLength 3 ;
|
||||
sh:severity sh:Violation ;
|
||||
sh:message "Every defined object carries exactly one canonical name." ] ;
|
||||
|
||||
sh:property [
|
||||
sh:path pr:status ; 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:version ; sh:maxCount 1 ;
|
||||
sh:severity sh:Warning ;
|
||||
sh:message "NR-001 (MAJOR): canonical name free of physical prefixes/suffixes." ] .
|
||||
sh:message "A published object should carry a version." ] .
|
||||
|
||||
prsh:MetricShape a sh:NodeShape ;
|
||||
sh:targetClass pr:Metric ;
|
||||
sh:property [ sh:path pr:hasFormula ; sh:minCount 1 ; sh:datatype xsd:string ; sh:minLength 2 ;
|
||||
sh:severity sh:Violation ; sh:message "BR-003 / DQ-004 (BLOCKING): a Metric MUST have a formula." ] ;
|
||||
sh:property [ sh:path pr:hasUnit ; sh:minCount 1 ;
|
||||
sh:severity sh:Violation ; sh:message "DQ-004 (BLOCKING): a Metric MUST have a hasUnit." ] ;
|
||||
sh:property [ sh:path pr:hasGranularity ; sh:minCount 1 ;
|
||||
sh:severity sh:Violation ; sh:message "DQ-004 (BLOCKING): a Metric MUST have a hasGranularity." ] .
|
||||
|
||||
prsh:BusinessConceptShape a sh:NodeShape ;
|
||||
sh:targetClass pr:BusinessConcept ;
|
||||
sh:property [ sh:path pr:hasFormula ; sh:maxCount 0 ;
|
||||
sh:severity sh:Violation ; sh:message "BR-003 (BLOCKING): a Business Concept MUST NOT have a formula." ] .
|
||||
prs:PublishedShape a sh:NodeShape ;
|
||||
sh:targetClass pr:DefinedObject ;
|
||||
rdfs:comment "Extra demands that only bite once an object claims to be PUBLISHED. Keeping these off the base shape is what lets a domain be documented progressively." ;
|
||||
|
||||
prsh:BusinessObjectShape a sh:NodeShape ;
|
||||
sh:targetClass pr:BusinessObject ;
|
||||
sh:property [ sh:path pr:composedOf ; sh:minCount 3 ; sh:maxCount 10 ; sh:class pr:BusinessConcept ;
|
||||
sh:severity sh:Warning ; sh:message "BR-004 (MAJOR): a Business Object MUST group 3 to 10 Business Concepts." ] ;
|
||||
sh:property [ sh:path pr:belongsToSubDomain ; sh:minCount 1 ; sh:maxCount 1 ; sh:class pr:SubDomain ;
|
||||
sh:severity sh:Warning ; sh:message "NR-012/OW (MAJOR): a Business Object belongs to exactly one Sub-Domain." ] ;
|
||||
sh:property [ sh:path pr:hasSteward ; sh:minCount 1 ; sh:class pr:DataSteward ;
|
||||
sh:severity sh:Warning ; sh:message "OW-005 (MAJOR): a Business Object MUST have a Data Steward." ] .
|
||||
sh:sparql [
|
||||
sh:severity sh:Violation ;
|
||||
sh:message "OW-007 -- an object flagged TO_ARBITRATE cannot be PUBLISHED. Resolve at the Cross-Domain Council first." ;
|
||||
sh:select """
|
||||
PREFIX pr: <https://ontology.pernod-ricard.com/metamodel/>
|
||||
SELECT $this WHERE {
|
||||
$this pr:status "PUBLISHED" ;
|
||||
pr:arbitrationStatus "TO_ARBITRATE" .
|
||||
}""" ] ;
|
||||
|
||||
prsh:DataObjectShape a sh:NodeShape ;
|
||||
sh:targetClass pr:DataObject ;
|
||||
sh:property [ sh:path pr:implements ; sh:minCount 1 ; sh:maxCount 1 ; sh:class pr:BusinessObject ;
|
||||
sh:severity sh:Violation ; sh:message "BR-006 (BLOCKING): a Data Object MUST implement exactly one Business Object." ] ;
|
||||
sh:property [ sh:path pr:hasElement ; sh:minCount 1 ; sh:class pr:DataElement ;
|
||||
sh:severity sh:Violation ; sh:message "BR-006 (BLOCKING): a Data Object MUST contain at least one Data Element." ] ;
|
||||
sh:property [ sh:path pr:hasSteward ; sh:minCount 1 ; sh:class pr:DataSteward ;
|
||||
sh:severity sh:Warning ; sh:message "OW-005 (MAJOR): a Data Object MUST have a Data Steward." ] .
|
||||
sh:sparql [
|
||||
sh:severity sh:Violation ;
|
||||
sh:message "OW-003 -- a PUBLISHED object must have an owning Data Domain." ;
|
||||
sh:select """
|
||||
PREFIX pr: <https://ontology.pernod-ricard.com/metamodel/>
|
||||
SELECT $this WHERE {
|
||||
$this pr:status "PUBLISHED" .
|
||||
FILTER NOT EXISTS { $this pr:owningDomain ?d }
|
||||
}""" ] .
|
||||
|
||||
prsh:DataElementShape a sh:NodeShape ;
|
||||
sh:targetClass pr:DataElement ;
|
||||
sh:property [ sh:path pr:hasFormat ; sh:minCount 1 ;
|
||||
sh:severity sh:Warning ; sh:message "DQ-005 (MAJOR): a Data Element MUST have a hasFormat." ] ;
|
||||
sh:property [ sh:path pr:hasSourceSystem ; sh:minCount 1 ;
|
||||
sh:severity sh:Warning ; sh:message "DQ-005 (MAJOR): a Data Element MUST have a hasSourceSystem." ] ;
|
||||
sh:property [ sh:path pr:physicalName ; sh:minCount 1 ; sh:maxCount 1 ; sh:datatype xsd:string ;
|
||||
sh:severity sh:Violation ; sh:message "DQ-010 (BLOCKING): a mapped Data Element MUST have a physical_name." ] .
|
||||
|
||||
prsh:DataDomainShape a sh:NodeShape ;
|
||||
#################################################################
|
||||
# OBSERVED OBJECTS -- captured, never hand-edited
|
||||
#################################################################
|
||||
|
||||
prs:CapturedObjectShape a sh:NodeShape ;
|
||||
sh:targetClass pr:CapturedObject ;
|
||||
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 captured 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 "Captured objects are not attributed directly -- ownership flows from the Data Element mapped to them." ] .
|
||||
|
||||
|
||||
# inSchema is deliberately NOT required anywhere. Neo4j has no schema and
|
||||
# BigQuery calls the level a dataset; a minCount 1 here would break on the
|
||||
# first harvest outside Snowflake.
|
||||
prs:DataStructureShape a sh:NodeShape ;
|
||||
sh:targetClass pr:DataStructure ;
|
||||
sh:property [
|
||||
sh:path pr:inSchema ; sh:maxCount 1 ;
|
||||
sh:severity sh:Info ;
|
||||
sh:message "Optional on purpose: not every engine has a schema level." ] ;
|
||||
sh:property [
|
||||
sh:path pr:sourcedFrom ; sh:maxCount 1 ;
|
||||
sh:severity sh:Info ] .
|
||||
|
||||
|
||||
prs:FieldShape a sh:NodeShape ;
|
||||
sh:targetClass pr:Field ;
|
||||
sh:property [
|
||||
sh:path pr:fullyQualifiedName ; sh:datatype xsd:string ;
|
||||
sh:minCount 1 ; sh:maxCount 1 ;
|
||||
sh:pattern "^[A-Za-z0-9_$]+\\\\.[A-Za-z0-9_$]+\\\\.[A-Za-z0-9_$]+\\\\.[A-Za-z0-9_$]+$" ;
|
||||
sh:severity sh:Violation ;
|
||||
sh:message "A Field carries DATABASE.SCHEMA.RELATION.COLUMN. A bare column name is ambiguous as soon as it exists in two tables, so the FQN is the real join key against INFORMATION_SCHEMA." ] ;
|
||||
sh:property [
|
||||
sh:path pr:livesIn ; sh:minCount 1 ; sh:maxCount 1 ;
|
||||
sh:severity sh:Violation ;
|
||||
sh:message "Every Field lives in exactly one Physical Relation." ] .
|
||||
|
||||
|
||||
#################################################################
|
||||
# CATEGORIZATION TREE -- scope of the unified belongsTo
|
||||
#################################################################
|
||||
|
||||
prs:SubDomainShape a sh:NodeShape ;
|
||||
sh:targetClass pr:SubDomain ;
|
||||
sh:property [
|
||||
sh:path pr:belongsTo ; sh:class pr:DataDomain ;
|
||||
sh:minCount 1 ; sh:maxCount 1 ;
|
||||
sh:severity sh:Violation ;
|
||||
sh:message "A Sub-Domain belongs to exactly one Data Domain." ] ;
|
||||
sh:property [
|
||||
sh:path pr:hasSubDomainOwner ; sh:maxCount 1 ;
|
||||
sh:severity sh:Warning ;
|
||||
sh:message "A Sub-Domain should name its owner." ] .
|
||||
|
||||
|
||||
prs:DataDomainShape a sh:NodeShape ;
|
||||
sh:targetClass pr:DataDomain ;
|
||||
sh:property [ sh:path pr:hasDGL ; sh:minCount 1 ; sh:maxCount 1 ;
|
||||
sh:severity sh:Violation ; sh:message "OW-004 (BLOCKING): a Data Domain MUST have exactly one DGL." ] ;
|
||||
sh:property [ sh:path pr:hasActivationStatus ; sh:minCount 1 ; sh:maxCount 1 ;
|
||||
sh:severity sh:Violation ; sh:message "OW-004 (BLOCKING): a Data Domain MUST have exactly one Activation Status." ] .
|
||||
sh:property [
|
||||
sh:path pr:hasDomainOwner ; sh:maxCount 1 ;
|
||||
sh:severity sh:Warning ;
|
||||
sh:message "A Data Domain should name its owner." ] ;
|
||||
sh:property [
|
||||
sh:path pr:hasDGL ; sh:maxCount 1 ;
|
||||
sh:severity sh:Warning ;
|
||||
sh:message "A Data Domain should name its Data Governance Lead." ] ;
|
||||
sh:property [
|
||||
sh:path pr:hasActivationStatus ; sh:maxCount 1 ;
|
||||
sh:severity sh:Info ] .
|
||||
|
||||
# FIX v0.1.1 - NR-002 now restricts ?cls to LEAF classes so cross-layer
|
||||
# homonyms (Sub-Domain vs Concept, etc.) are not flagged as duplicates.
|
||||
prsh:NameUniquenessShape a sh:NodeShape ;
|
||||
sh:targetClass pr:MetaModelObject ;
|
||||
sh:sparql [ sh:severity sh:Violation ;
|
||||
sh:message "NR-002 (BLOCKING): canonical name MUST be unique within its (leaf) class and owning Data Domain." ;
|
||||
sh:select """
|
||||
PREFIX pr: <https://ontology.pernod-ricard.com/metamodel/0.9/>
|
||||
SELECT $this WHERE {
|
||||
$this a ?cls ; pr:hasName ?n ; pr:owningDomain ?d .
|
||||
?other a ?cls ; pr:hasName ?n ; pr:owningDomain ?d .
|
||||
FILTER ( $this != ?other )
|
||||
FILTER ( ?cls IN (
|
||||
pr:DataDomain, pr:SubDomain, pr:BusinessObject,
|
||||
pr:BusinessConcept, pr:Metric, pr:KPI,
|
||||
pr:DataAsset, pr:DataObject, pr:DataElement,
|
||||
pr:DataContract, pr:DataProduct, pr:Table, pr:Field ) )
|
||||
}
|
||||
""" ] .
|
||||
|
||||
prsh:ArbitrationGateShape a sh:NodeShape ;
|
||||
sh:targetClass pr:MetaModelObject ;
|
||||
sh:sparql [ sh:severity sh:Violation ;
|
||||
sh:message "OW-007 (BLOCKING): a TO_ARBITRATE object MUST NOT be PUBLISHED." ;
|
||||
#################################################################
|
||||
# BUSINESS OBJECT -- subject, perimeter, stewardship
|
||||
#################################################################
|
||||
|
||||
prs:BusinessObjectShape a sh:NodeShape ;
|
||||
sh:targetClass pr:BusinessObject ;
|
||||
|
||||
sh:property [
|
||||
sh:path pr:belongsTo ; sh:class pr:SubDomain ;
|
||||
sh:minCount 1 ; sh:maxCount 1 ;
|
||||
sh:severity sh:Violation ;
|
||||
sh:message "A Business Object belongs to exactly one Sub-Domain." ] ;
|
||||
|
||||
sh:property [
|
||||
sh:path pr:aboutConcept ; sh:class pr:BusinessConcept ;
|
||||
sh:minCount 1 ; sh:maxCount 1 ;
|
||||
sh:severity sh:Violation ;
|
||||
sh:message "BR-004 -- a Business Object is about exactly one Business Concept. Replaces the old 3..10 minimum, which was satisfied in v0.6 by attaching unrelated concepts. If the subject cannot be named, the object is not scoped." ] ;
|
||||
|
||||
sh:property [
|
||||
sh:path pr:usesConcept ; sh:class pr:BusinessConcept ;
|
||||
sh:severity sh:Info ;
|
||||
sh:message "Other notions needed to read this object. Zero is a legitimate answer." ] ;
|
||||
|
||||
sh:property [
|
||||
sh:path pr:monitoredBy ; sh:class pr:DataSteward ;
|
||||
sh:minCount 1 ;
|
||||
sh:severity sh:Warning ;
|
||||
sh:message "The Business Object is where a steward is declared -- Data Objects and Data Elements inherit from here." ] ;
|
||||
|
||||
sh:property [
|
||||
sh:path pr:businessDefinition ; 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." ] ;
|
||||
|
||||
sh:property [
|
||||
sh:path pr:sourcedFrom ; sh:maxCount 0 ;
|
||||
sh:severity sh:Violation ;
|
||||
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." ] .
|
||||
|
||||
|
||||
#################################################################
|
||||
# BUSINESS CONCEPT AND METRIC
|
||||
#################################################################
|
||||
|
||||
prs:BusinessConceptShape a sh:NodeShape ;
|
||||
sh:targetClass pr:BusinessConcept ;
|
||||
rdfs:comment "Note what is NOT here: no requirement that a Concept be realized by any Data Element or measured by any Metric. The business layer is allowed to run ahead of the data. Coverage is a dashboard metric, not a rule." ;
|
||||
|
||||
sh:property [
|
||||
sh:path pr:businessDefinition ; sh:datatype xsd:string ;
|
||||
sh:minCount 1 ; sh:maxCount 1 ; sh:minLength 20 ;
|
||||
sh:severity sh:Violation ;
|
||||
sh:message "A Business Concept exists to carry meaning: it must hold exactly one business definition of at least 20 characters." ] ;
|
||||
|
||||
sh:property [
|
||||
sh:path pr:formula ; sh:maxCount 0 ;
|
||||
sh:severity sh:Violation ;
|
||||
sh:message "BR-003 -- the presence of a formula is what makes something a Metric. A Concept with a formula is a misclassified Metric." ] ;
|
||||
|
||||
sh:property [
|
||||
sh:path pr:sourcedFrom ; sh:maxCount 0 ;
|
||||
sh:severity sh:Violation ;
|
||||
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." ] .
|
||||
|
||||
|
||||
prs:MetricShape a sh:NodeShape ;
|
||||
sh:targetClass pr:Metric ;
|
||||
|
||||
sh:property [
|
||||
sh:path pr:formula ; sh:datatype xsd:string ;
|
||||
sh:minCount 1 ; sh:maxCount 1 ;
|
||||
sh:severity sh:Violation ;
|
||||
sh:message "BR-003 -- a Metric is a measurement with a calculation rule. No formula, no Metric." ] ;
|
||||
|
||||
sh:property [
|
||||
sh:path pr:unit ; sh:minCount 1 ;
|
||||
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:property [
|
||||
sh:path pr:measures ; sh:class pr:BusinessConcept ;
|
||||
sh:minCount 1 ;
|
||||
sh:severity sh:Violation ;
|
||||
sh:message "A Metric measures at least one Business Concept -- this is how a measure element reaches business meaning." ] ;
|
||||
|
||||
sh:property [
|
||||
sh:path pr:hasGranularity ; sh:maxCount 0 ;
|
||||
sh:severity sh:Warning ;
|
||||
sh:message "Grain moved off the Metric in v1.1. A Metric is a calculation rule and has no rows; grain belongs to the Data Object, stated with hasGrainElement and checkable against the real primary key." ] ;
|
||||
|
||||
sh:property [
|
||||
sh:path pr:physicalName ; sh:maxCount 0 ;
|
||||
sh:severity sh:Violation ;
|
||||
sh:message "A Metric must not carry a physical name -- that is a layer leak. Under BR-013 the physical name belongs on the Data Element." ] .
|
||||
|
||||
|
||||
# The source of truth of a Metric is asserted by exception, then checked.
|
||||
# "Where can I get this metric" is a first-order question and the derivation
|
||||
# chain will have gaps before harvesting, so the assertion stands on its own --
|
||||
# but it must agree with the elements that implement the metric, or the two
|
||||
# will drift the day an element moves.
|
||||
prs:MetricSourceShape a sh:NodeShape ;
|
||||
sh:targetClass pr:Metric ;
|
||||
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:severity sh:Violation ;
|
||||
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/0.9/>
|
||||
PREFIX pr: <https://ontology.pernod-ricard.com/metamodel/>
|
||||
SELECT $this WHERE {
|
||||
$this pr:hasStatus "PUBLISHED" .
|
||||
$this pr:arbitrationStatus "TO_ARBITRATE" . }
|
||||
""" ] .
|
||||
$this pr:sourcedFrom ?sys ; pr:computedBy ?de .
|
||||
FILTER NOT EXISTS {
|
||||
$this pr:computedBy ?any .
|
||||
?any pr:sourcedFrom ?sys .
|
||||
}
|
||||
}""" ] ;
|
||||
sh:sparql [
|
||||
sh:severity sh:Warning ;
|
||||
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)
|
||||
}""" ] .
|
||||
|
||||
|
||||
# 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:status "PUBLISHED" .
|
||||
FILTER NOT EXISTS { $this pr:computedBy ?de }
|
||||
}""" ] .
|
||||
|
||||
|
||||
#################################################################
|
||||
# DATA OBJECT -- the pivot, and grain
|
||||
#################################################################
|
||||
|
||||
prs:DataObjectShape a sh:NodeShape ;
|
||||
sh:targetClass pr:DataObject ;
|
||||
|
||||
sh:property [
|
||||
sh:path pr:represents ; sh:class pr:BusinessObject ;
|
||||
sh:minCount 1 ; sh:maxCount 1 ;
|
||||
sh:severity sh:Violation ;
|
||||
sh:message "BR-006 -- a Data Object represents exactly one Business Object. This is the pivot: the single junction between governance and technology." ] ;
|
||||
|
||||
sh:property [
|
||||
sh:path pr:hasElement ; sh:class pr:DataElement ;
|
||||
sh:minCount 1 ;
|
||||
sh:severity sh:Violation ;
|
||||
sh:message "A Data Object holds at least one Data Element." ] ;
|
||||
|
||||
sh:property [
|
||||
sh:path pr:hasGrainElement ; sh:class pr:DataElement ;
|
||||
sh:severity sh:Warning ;
|
||||
sh:message "State the grain: the elements whose combination makes a row unique. Checkable against the harvested primary key." ] ;
|
||||
|
||||
sh:property [
|
||||
sh:path pr:monitoredBy ; sh:maxCount 0 ;
|
||||
sh:severity sh:Warning ;
|
||||
sh:message "Stewardship is declared on the Business Object and inherited through represents. Declaring it here allows two answers to the same question." ] ;
|
||||
|
||||
sh:property [
|
||||
sh:path pr:sourcedFrom ; sh:class pr:System ; sh:maxCount 1 ;
|
||||
sh:severity sh:Warning ;
|
||||
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:property [
|
||||
sh:path pr:storedIn ; sh:class pr:DataStructure ;
|
||||
sh:severity sh:Warning ;
|
||||
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:property [
|
||||
sh:path pr:materializedAs ; sh:maxCount 0 ;
|
||||
sh:severity sh:Warning ;
|
||||
sh:message "Deprecated in v1.4: storedIn now serves both granularities." ] .
|
||||
|
||||
|
||||
# A declared grain element must actually belong to the object it grains.
|
||||
prs:GrainConsistencyShape a sh:NodeShape ;
|
||||
sh:targetClass pr:DataObject ;
|
||||
sh:sparql [
|
||||
sh:severity sh:Violation ;
|
||||
sh:message "A grain element must be one of the object's own Data Elements." ;
|
||||
sh:select """
|
||||
PREFIX pr: <https://ontology.pernod-ricard.com/metamodel/>
|
||||
SELECT $this ?grain WHERE {
|
||||
$this pr:hasGrainElement ?grain .
|
||||
FILTER NOT EXISTS { $this pr:hasElement ?grain }
|
||||
}""" ] .
|
||||
|
||||
|
||||
#################################################################
|
||||
# DATA ELEMENT -- the XOR of meaning
|
||||
#################################################################
|
||||
|
||||
prs:DataElementShape a sh:NodeShape ;
|
||||
sh:targetClass pr:DataElement ;
|
||||
|
||||
sh:property [
|
||||
sh:path pr:physicalName ; sh:datatype xsd:string ;
|
||||
sh:maxCount 1 ;
|
||||
sh:severity sh:Warning ;
|
||||
sh:message "NR-009 -- the physical name is recorded verbatim and never altered." ] ;
|
||||
|
||||
sh:property [
|
||||
sh:path pr:fullyQualifiedName ; sh:maxCount 1 ;
|
||||
sh:severity sh:Info ] ;
|
||||
|
||||
sh:property [
|
||||
sh:path pr:unit ; sh:maxCount 1 ;
|
||||
sh:severity sh:Violation ;
|
||||
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: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 defined 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:maxCount 1 ;
|
||||
sh:severity sh:Warning ;
|
||||
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: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:exposesMetric ; sh:class pr:Metric ;
|
||||
sh:maxCount 1 ;
|
||||
sh:severity sh:Info ;
|
||||
sh:message "Which certified Metric this BI field implements. Comparing expressions across fields realizing one Metric is the harmonisation measure." ] .
|
||||
|
||||
|
||||
#################################################################
|
||||
# NAMING -- portable regex, no lookbehind
|
||||
#################################################################
|
||||
|
||||
prs:NamingShape a sh:NodeShape ;
|
||||
sh:targetClass pr:DefinedObject ;
|
||||
sh:property [
|
||||
sh:path pr:name ;
|
||||
sh:not [ sh:pattern "_NM$" ] ;
|
||||
sh:severity sh:Warning ;
|
||||
sh:message "NR-001 -- a canonical name must not end in a physical suffix. Written as sh:not[sh:pattern] rather than a lookbehind: sh:pattern is normatively XPath 2.0 regex, which has no lookbehind, so the v0.1.1 form only worked under pySHACL." ] ;
|
||||
sh:property [
|
||||
sh:path pr:name ;
|
||||
sh:not [ sh:pattern "^[a-z_]+$" ] ;
|
||||
sh:severity sh:Warning ;
|
||||
sh:message "NR-002 -- a canonical name is written for humans, not as a snake_case identifier." ] .
|
||||
|
||||
Reference in New Issue
Block a user