Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1c8ab77003 | |||
| f83b7d7491 | |||
| 76e95de471 |
@@ -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
+1807
-1139
File diff suppressed because it is too large
Load Diff
+429
-153
@@ -7,11 +7,135 @@
|
||||
@prefix pr: <https://ontology.pernod-ricard.com/metamodel/> .
|
||||
|
||||
#################################################################
|
||||
# PERNOD RICARD DATA META MODEL - T-Box v1.1
|
||||
# PERNOD RICARD DATA META MODEL - T-Box v1.6
|
||||
# Owner: Bastien Gourdon - Data Governance Office
|
||||
#
|
||||
# WHAT CHANGED vs v1.0
|
||||
# WHAT CHANGED vs v1.5
|
||||
# --------------------
|
||||
# A. NO LAYER CARRIES A PROVENANCE. All six layer roots are now structural:
|
||||
# they say what an object IS, never how it got here. Every concrete class
|
||||
# declares its own provenance. v1.5 had fixed this for the physical layer
|
||||
# only, which left the model saying that Ownership is Defined while
|
||||
# Physical is neither -- an asymmetry with no meaning behind it. The two
|
||||
# axes are either orthogonal or they are not.
|
||||
#
|
||||
# B. ABSTRACTNESS IS DECLARED, NOT GUESSED. isAbstract marks the classes that
|
||||
# exist only to give their specialisations a parent. A viewer was inferring
|
||||
# it from "has subclasses", which flagged Metric as abstract -- one of the
|
||||
# most instantiated classes in the model, whose only crime is having KPI
|
||||
# beneath it. Whether a class is instantiated is a modelling decision and
|
||||
# belongs in the model.
|
||||
#
|
||||
# C. ForeignKeyConstraint -> ForeignKey, SubDomainOwner labelled Data
|
||||
# Sub-Domain Owner to match Data Domain Owner, and the ten classes that
|
||||
# had no rdfs:comment now have one. Stale descriptions corrected: a Data
|
||||
# Object is stored in a Data Structure, it is no longer "materialized as a
|
||||
# Physical Relation".
|
||||
#
|
||||
# WHAT CHANGED vs v1.4 (kept for reference)
|
||||
# -----------------------------------------
|
||||
# CuratedObject -> DefinedObject, ObservedObject -> CapturedObject, in the
|
||||
# IRIs and not only in the labels. v1.4 renamed what screens showed and left
|
||||
# the terms themselves untouched, so the model said one thing and the
|
||||
# interface another -- exactly the split the short-label work was meant to
|
||||
# end. Every reference follows: subclass declarations, the disjointness
|
||||
# axiom, the shapes and their messages, and the prose in the comments.
|
||||
#
|
||||
# No instance changes. Nothing in the A-Box ever named these two classes
|
||||
# directly: an object reaches them through its layer, by entailment. The old
|
||||
# IRIs are kept deprecated per LC-002 so a domain file written against them
|
||||
# still classifies.
|
||||
#
|
||||
# WHAT CHANGED vs v1.3 (kept for reference)
|
||||
# -----------------------------------------
|
||||
# A. PhysicalRelation -> DataStructure. "Relation" meant a table in Codd's
|
||||
# sense, which reads as a property in an ontology and locks the class into
|
||||
# the tabular world. A Neo4j node label, a Mongo collection and a Kafka
|
||||
# topic are data structures; none of them is a relation. The class is also
|
||||
# the declared EXTENSION POINT for non-tabular storage -- see its comment
|
||||
# for the four lines that would insert Dataset and Graph beneath it.
|
||||
#
|
||||
# B. hasSource -> sourcedFrom, pointing at a System instead of holding a
|
||||
# string. "Which objects depend on MDM Product?" was unanswerable because
|
||||
# MDM Product was text. It also settles the naming collision the string
|
||||
# created with BIDataSource: one names a system, the other a BI surface,
|
||||
# and now they are different kinds of thing rather than two readings of
|
||||
# one property.
|
||||
#
|
||||
# C. storedIn serves two granularities, like represents before it:
|
||||
# DataObject storedIn DataStructure
|
||||
# DataElement storedIn Field
|
||||
# materializedAs is therefore redundant and deprecated. The seam between
|
||||
# what governance defines and what a harvester captures now counts ONE
|
||||
# relation instead of two.
|
||||
#
|
||||
# D. inSchema moves from DataStructure to nothing declared, and must stay
|
||||
# OPTIONAL in the shapes. Neo4j has no schema and BigQuery calls the level
|
||||
# a dataset; a minCount 1 would break on the first non-Snowflake harvest.
|
||||
#
|
||||
# WHAT CHANGED vs v1.2 (kept for reference)
|
||||
# -----------------------------------------
|
||||
# A. references -> qualifiedBy. "References" named the mechanism, a foreign
|
||||
# key; qualifiedBy names the reason: the Sell Out dataset is qualified by
|
||||
# Product, Customer, Currency and Date. It reads without explanation, and
|
||||
# it is what gives a fact its grain and its analysis context.
|
||||
#
|
||||
# B. primaryLocation, a SUB-PROPERTY of storedIn. One Data Element can sit in
|
||||
# several Fields, one authoritative and the others copies. Two parallel
|
||||
# properties would be free to disagree -- a field marked primary in one and
|
||||
# missing from the other. As a sub-property, entailment guarantees the
|
||||
# primary location is always among the locations. Primary/replica rather
|
||||
# than master/slave, which the industry has moved away from and which
|
||||
# reads badly to a business audience.
|
||||
#
|
||||
# C. hasShortLabel. rdfs:label carries the exhaustive name (Data Domain, Sell
|
||||
# Out Data Hub - Gold), hasShortLabel the one screens use (Domain, SODH
|
||||
# Gold). Same object was being called two things depending on where you
|
||||
# looked; now both names are declared and the viewers stop hard-coding a
|
||||
# display dictionary of their own.
|
||||
#
|
||||
# D. hasSource is now forbidden on a Business Object as well as on a Business
|
||||
# Concept. Same argument: a Business Object is a governance grouping, not
|
||||
# data. Its data lives in the Data Objects that represent it, and those
|
||||
# carry the system. In v0.6 the 15 objects carried a grab-bag -- physical
|
||||
# locations, migration notes, arbitration remarks -- under one property.
|
||||
#
|
||||
# WHAT CHANGED vs v1.1 (kept for reference)
|
||||
# -----------------------------------------
|
||||
# A. physicalizedIn -> storedIn. "Physicalized in" is jargon; the relation
|
||||
# says where the data sits, and "stored in" says that to anyone.
|
||||
#
|
||||
# B. hasSource MEANS ONE THING: the system that holds the authoritative data.
|
||||
# It was drifting into a third meaning on Business Concepts and Metrics,
|
||||
# where it named the DOCUMENT a record came from ("SODH back-doc v0.7").
|
||||
# A business concept has no data source -- it has an owning domain, which
|
||||
# is its authority, and a definition published in the glossary. The
|
||||
# property is now forbidden on concepts by SHACL.
|
||||
#
|
||||
# C. FIVE TERMS REMOVED OR DEPRECATED, none of them instantiated anywhere:
|
||||
# Table deleted -- deprecated since v1.0, replaced by
|
||||
# BaseTable, and never used. A deprecation that
|
||||
# never ends stops meaning anything.
|
||||
# constrainsRelation derivable: constrainsField -> livesIn gives the
|
||||
# relation. An asserted edge redundant with an
|
||||
# existing path is a future divergence.
|
||||
# MaterializedView a class standing in for an attribute; being
|
||||
# derived is what dependsOn already says.
|
||||
# approximateRowCount volumetry with no identified use. queryCount30d
|
||||
# and lastQueriedOn carry the rationalisation case.
|
||||
# hasVendor marginal.
|
||||
#
|
||||
# D. hasProductOwner drops from BLOCKING to a warning in the shapes. The SODH
|
||||
# product owner is genuinely not appointed yet, and a blocking rule that
|
||||
# cannot be satisfied truthfully forces invention -- the BR-004 lesson.
|
||||
#
|
||||
# Environment and deployedIn stay, unused. Without them a DEV table and its
|
||||
# PROD twin are indistinguishable and lineage pollutes silently on the first
|
||||
# multi-environment harvest. Unused because nothing is harvested yet, not
|
||||
# because they are useless.
|
||||
#
|
||||
# WHAT CHANGED vs v1.0 (kept for reference)
|
||||
# -----------------------------------------
|
||||
# A. RELATIONS SIMPLIFIED AND GIVEN HONEST NAMES
|
||||
# belongsToDomain + belongsToSubDomain -> belongsTo (one property,
|
||||
# scoped per class by SHACL; reusable if a domain family appears).
|
||||
@@ -69,7 +193,7 @@
|
||||
# one Product Owner: it is a consumable, and there must be no ambiguity
|
||||
# about who runs it.
|
||||
#
|
||||
# G. realizesMetric KEPT AS A TRANSITIONAL CONTROL
|
||||
# G. exposesMetric KEPT AS A TRANSITIONAL CONTROL
|
||||
# Declared, required on certified BI fields, and that is all. No rule is
|
||||
# written against the lineage chain: making the assertion conditional on
|
||||
# intermediate mappings existing would create confusion and discourage
|
||||
@@ -88,11 +212,11 @@
|
||||
# freeze the term namespace and version the ontology document.
|
||||
# MIGRATION: sed -i 's|/metamodel/0.9/|/metamodel/|g' instances/*.ttl
|
||||
#
|
||||
# 2. NEW ORTHOGONAL AXIS: CuratedObject vs ObservedObject.
|
||||
# 2. NEW ORTHOGONAL AXIS: DefinedObject vs CapturedObject.
|
||||
# LAYER says WHAT an object is. PROVENANCE says HOW IT GOT HERE.
|
||||
# - CuratedObject : asserted by governance, edited via pull request,
|
||||
# - DefinedObject : written by governance, edited via pull request,
|
||||
# subject to the full ownership shape.
|
||||
# - ObservedObject: harvested from a running system, rebuilt every night,
|
||||
# - CapturedObject: read from a running system, rebuilt every night,
|
||||
# NEVER hand-edited, NOT required to carry owningDomain.
|
||||
# Without this split, the SHACL ownership shape would demand an owning Data
|
||||
# Domain and a named owner on every one of the ~50 000 Snowflake columns.
|
||||
@@ -102,7 +226,7 @@
|
||||
# Transformation, plus column-level lineage (derivedFrom).
|
||||
#
|
||||
# 4. NEW CONSUMPTION LAYER (Tableau + Looker).
|
||||
# BIWorkspace / BIDataSource / BIField / BIReport, plus realizesMetric,
|
||||
# BIWorkspace / BIDataSource / BIField / BIReport, plus exposesMetric,
|
||||
# which closes the loop between a certified Metric and its actual
|
||||
# implementations in the BI estate.
|
||||
#
|
||||
@@ -117,13 +241,13 @@
|
||||
|
||||
<https://ontology.pernod-ricard.com/metamodel/>
|
||||
a owl:Ontology ;
|
||||
owl:versionIRI <https://ontology.pernod-ricard.com/metamodel/1.1> ;
|
||||
owl:priorVersion <https://ontology.pernod-ricard.com/metamodel/1.0> ;
|
||||
owl:versionIRI <https://ontology.pernod-ricard.com/metamodel/1.6> ;
|
||||
owl:priorVersion <https://ontology.pernod-ricard.com/metamodel/1.5> ;
|
||||
dcterms:title "Pernod Ricard Data Meta Model" ;
|
||||
dcterms:creator "Bastien Gourdon" ;
|
||||
dcterms:modified "2026-07-26"^^xsd:date ;
|
||||
owl:versionInfo "1.1.0-draft-2026-07-26" ;
|
||||
rdfs:comment """OWL T-Box. Six layers (Ownership, Business, Logical, Delivery, Physical, Consumption) crossed with a provenance axis (Curated vs Observed). Curated objects are asserted by governance and reviewed; Observed objects are harvested from Snowflake, Tableau and Looker and are rebuilt, never edited. Bridge annotation properties are declared for future alignment with domain ontologies.""" .
|
||||
dcterms:modified "2026-07-30"^^xsd:date ;
|
||||
owl:versionInfo "1.6.0-draft-2026-07-30" ;
|
||||
rdfs:comment """OWL T-Box. Six layers (Ownership, Business, Logical, Delivery, Physical, Consumption) crossed with a provenance axis (Defined vs Captured). Defined objects are written by governance and reviewed; Captured objects are read from Snowflake, Tableau and Looker and are rebuilt, never edited. Bridge annotation properties are declared for future alignment with domain ontologies.""" .
|
||||
|
||||
|
||||
#################################################################
|
||||
@@ -153,11 +277,15 @@ pr:denotesActivity a owl:AnnotationProperty ;
|
||||
# ---- Operational documentation --------------------------------
|
||||
pr:harvestSource a owl:AnnotationProperty ;
|
||||
rdfs:label "harvest source" ;
|
||||
rdfs:comment "For Observed terms: the system view or API that populates this term. Documents the contract between the meta model and the harvester." .
|
||||
rdfs:comment "For Captured terms: the system view or API that populates this term. Documents the contract between the meta model and the harvester." .
|
||||
|
||||
pr:curationMode a owl:AnnotationProperty ;
|
||||
rdfs:label "curation mode" ;
|
||||
rdfs:comment "ASSERTED (governance writes it), HARVESTED (a job writes it) or PROPOSED_CONFIRMED (a job proposes, a steward confirms)." .
|
||||
pr:isAbstract a owl:AnnotationProperty ;
|
||||
rdfs:label "is abstract" ;
|
||||
rdfs:comment "True on a class that exists only to give its specialisations a common parent and a common shape, and is never instantiated. Declared rather than inferred: a viewer that guessed it from 'has subclasses' flagged Metric as abstract, one of the most instantiated classes in the model, whose only crime is having KPI beneath it. Whether a class is instantiated is a modelling decision." .
|
||||
|
||||
pr:authoringMode a owl:AnnotationProperty ;
|
||||
rdfs:label "authoring mode" ;
|
||||
rdfs:comment "Who writes this, in practice: ASSERTED (a person does), HARVESTED (a job does) or PROPOSED_CONFIRMED (a job proposes and a person accepts). Renamed from curationMode along with the classes: 'curation' belonged to the same jargon family as 'curated'." .
|
||||
|
||||
|
||||
#################################################################
|
||||
@@ -166,79 +294,126 @@ pr:curationMode a owl:AnnotationProperty ;
|
||||
|
||||
pr:MetaModelObject a owl:Class ;
|
||||
rdfs:label "Meta Model Object" ;
|
||||
rdfs:comment "Anything the meta model describes. Actors are deliberately NOT Meta Model Objects." .
|
||||
rdfs:comment "Anything the meta model describes. Actors are deliberately NOT Meta Model Objects." ;
|
||||
pr:hasShortLabel "Meta model object" ;
|
||||
pr:isAbstract true .
|
||||
|
||||
pr:CuratedObject a owl:Class ; rdfs:subClassOf pr:MetaModelObject ;
|
||||
rdfs:label "Curated Object" ;
|
||||
pr:curationMode "ASSERTED" ;
|
||||
rdfs:comment "Asserted by governance, edited through pull request on the domain TTL file, subject to the full ownership and lifecycle shape (identifier, name, owner, owning Data Domain, status, version)." .
|
||||
pr:DefinedObject a owl:Class ; rdfs:subClassOf pr:MetaModelObject ;
|
||||
rdfs:label "Defined Object" ;
|
||||
pr:hasShortLabel "Defined" ;
|
||||
pr:authoringMode "ASSERTED" ;
|
||||
rdfs:comment "Somebody decided it. Written by governance, edited through a pull request on the domain TTL file, and subject to the full identity and lifecycle shape: identifier, name, owner, owning Data Domain, status, version." ;
|
||||
pr:isAbstract true .
|
||||
|
||||
pr:ObservedObject a owl:Class ; rdfs:subClassOf pr:MetaModelObject ;
|
||||
rdfs:label "Observed Object" ;
|
||||
pr:curationMode "HARVESTED" ;
|
||||
rdfs:comment "Harvested from a running system and rebuilt on every run. Never hand-edited. Carries sourceIdentifier, fullyQualifiedName and harvestedOn instead of curated ownership: attribution is INFERRED through the Data Element it is mapped to, not asserted on the node." .
|
||||
pr:CuratedObject a owl:Class ;
|
||||
rdfs:label "Curated Object (deprecated)" ;
|
||||
owl:deprecated true ; dcterms:isReplacedBy pr:DefinedObject ;
|
||||
rdfs:comment "v1.4 term. Renamed because 'curated' is governance jargon; 'defined' says the same thing to a business reader. No instance ever named this class directly." .
|
||||
|
||||
pr:OwnershipLayerObject a owl:Class ; rdfs:subClassOf pr:CuratedObject ;
|
||||
rdfs:label "Ownership & Categorization Layer Object" .
|
||||
pr:BusinessLayerObject a owl:Class ; rdfs:subClassOf pr:CuratedObject ;
|
||||
rdfs:label "Business Layer Object" .
|
||||
pr:LogicalLayerObject a owl:Class ; rdfs:subClassOf pr:CuratedObject ;
|
||||
pr:CapturedObject a owl:Class ; rdfs:subClassOf pr:MetaModelObject ;
|
||||
rdfs:label "Captured Object" ;
|
||||
pr:hasShortLabel "Captured" ;
|
||||
pr:authoringMode "HARVESTED" ;
|
||||
rdfs:comment "Read from a running system and rebuilt on every run. Never hand-edited. Carries sourceIdentifier, fullyQualifiedName and harvestedOn instead of declared ownership: attribution is INFERRED through the Data Element it is mapped to, never asserted on the node." ;
|
||||
pr:isAbstract true .
|
||||
|
||||
pr:ObservedObject a owl:Class ;
|
||||
rdfs:label "Observed Object (deprecated)" ;
|
||||
owl:deprecated true ; dcterms:isReplacedBy pr:CapturedObject ;
|
||||
rdfs:comment "v1.4 term. Renamed because 'observed' is passive and technical; 'captured' says where it came from. No instance ever named this class directly." .
|
||||
|
||||
pr:OwnershipLayerObject a owl:Class ; rdfs:subClassOf pr:MetaModelObject ;
|
||||
rdfs:label "Ownership & Categorization Layer Object" ;
|
||||
rdfs:comment "Who is accountable, and how the estate is categorised: domains, sub-domains, and the business objects they answer for. A layer root, so it says nothing about provenance." ;
|
||||
pr:isAbstract true ;
|
||||
pr:hasShortLabel "Ownership layer" .
|
||||
pr:BusinessLayerObject a owl:Class ; rdfs:subClassOf pr:MetaModelObject ;
|
||||
rdfs:label "Business Layer Object" ;
|
||||
rdfs:comment "What the business means: the shared vocabulary and the measures built on it. A layer root, so it says nothing about provenance." ;
|
||||
pr:isAbstract true ;
|
||||
pr:hasShortLabel "Business layer" .
|
||||
pr:LogicalLayerObject a owl:Class ; rdfs:subClassOf pr:MetaModelObject ;
|
||||
rdfs:label "Logical Layer Object" ;
|
||||
rdfs:comment "How the data is structured." .
|
||||
pr:DeliveryLayerObject a owl:Class ; rdfs:subClassOf pr:CuratedObject ;
|
||||
rdfs:comment "How the data is structured." ;
|
||||
pr:isAbstract true ;
|
||||
pr:hasShortLabel "Logical layer" .
|
||||
pr:DeliveryLayerObject a owl:Class ; rdfs:subClassOf pr:MetaModelObject ;
|
||||
rdfs:label "Delivery Layer Object" ;
|
||||
rdfs:comment "What we promise, to whom, until when. A different question from how the data is structured, hence a layer of its own: version, owner, changelog, support channel, consumers to warn." .
|
||||
pr:PhysicalLayerObject a owl:Class ; rdfs:subClassOf pr:ObservedObject ;
|
||||
rdfs:label "Physical Layer Object" .
|
||||
pr:ConsumptionLayerObject a owl:Class ; rdfs:subClassOf pr:ObservedObject ;
|
||||
rdfs:comment "What we promise, to whom, until when. A different question from how the data is structured, hence a layer of its own: version, owner, changelog, support channel, consumers to warn." ;
|
||||
pr:isAbstract true ;
|
||||
pr:hasShortLabel "Delivery layer" .
|
||||
pr:PhysicalLayerObject a owl:Class ; rdfs:subClassOf pr:MetaModelObject ;
|
||||
rdfs:label "Physical Layer Object" ;
|
||||
rdfs:comment """What actually runs: the systems, containers, structures, columns, keys and code that hold the data.
|
||||
|
||||
Like every layer root, this class says nothing about provenance -- and this layer is the one that shows why that matters.
|
||||
|
||||
A System is DEFINED: somebody decides that MDM Product is a system worth tracking, and names it. Nobody harvests the list of systems that matter. Everything beneath a system is CAPTURED: databases, schemas, data structures, fields, keys and transformations are read from what is running.
|
||||
|
||||
Until v1.4 this class was itself declared a kind of Captured Object, which made every physical class captured by construction and put the five seeded Systems in breach of the harvest rules. v1.6 extended the correction to all six layers: a layer says what an object IS, never how it got here.""" ;
|
||||
pr:isAbstract true ;
|
||||
pr:hasShortLabel "Physical layer" .
|
||||
pr:ConsumptionLayerObject a owl:Class ; rdfs:subClassOf pr:MetaModelObject ;
|
||||
rdfs:label "Consumption Layer Object" ;
|
||||
rdfs:comment "The BI estate. Separate from Physical because BI assets have their own lifecycle and their own owner: a dashboard belongs to its author, not to the Data Domain that owns the underlying data." .
|
||||
rdfs:comment "The BI estate. Separate from Physical because BI assets have their own lifecycle and their own owner: a dashboard belongs to its author, not to the Data Domain that owns the underlying data." ;
|
||||
pr:hasShortLabel "Consumption" ;
|
||||
pr:isAbstract true .
|
||||
|
||||
|
||||
#################################################################
|
||||
# LAYER 1 - OWNERSHIP & CATEGORIZATION (curated)
|
||||
# LAYER 1 - OWNERSHIP & CATEGORIZATION (defined)
|
||||
#################################################################
|
||||
|
||||
pr:DataDomain a owl:Class ; rdfs:subClassOf pr:OwnershipLayerObject ;
|
||||
rdfs:label "Data Domain" .
|
||||
pr:DataDomain a owl:Class ; rdfs:subClassOf pr:OwnershipLayerObject , pr:DefinedObject ;
|
||||
rdfs:label "Data Domain" ;
|
||||
pr:hasShortLabel "Domain" ;
|
||||
rdfs:comment "A durable area of the business that owns the meaning of its data: Sales Performance, Product & Material, Financial Performance. It is the unit of accountability -- one owner, one governance lead -- and the top of the categorisation tree. Domains are stable; the projects and systems inside them are not." .
|
||||
|
||||
pr:SubDomain a owl:Class ; rdfs:subClassOf pr:OwnershipLayerObject ;
|
||||
rdfs:label "Sub-Domain" .
|
||||
pr:SubDomain a owl:Class ; rdfs:subClassOf pr:OwnershipLayerObject , pr:DefinedObject ;
|
||||
rdfs:label "Sub-Domain" ;
|
||||
pr:hasShortLabel "Sub-domain" ;
|
||||
rdfs:comment "A coherent slice of a Data Domain, grouping the business objects that belong together: Sell Out inside Sales Performance, Fiscal Calendar inside Financial Performance. It exists so that governance can be delegated and reviewed at a size a person can hold in their head." .
|
||||
|
||||
pr:BusinessObject a owl:Class ; rdfs:subClassOf pr:OwnershipLayerObject ;
|
||||
pr:BusinessObject a owl:Class ; rdfs:subClassOf pr:OwnershipLayerObject , pr:DefinedObject ;
|
||||
rdfs:label "Business Object" ;
|
||||
rdfs:comment "The single anchor of accountability, which is why it stays in the Ownership layer: it is the one object where a steward is declared, and Data Objects and Data Elements inherit from it. It carries NO business definition of its own -- its meaning comes from its aboutConcept. What it does carry is a perimeter: the subject it is about, and the notions needed to read it." .
|
||||
rdfs:comment "The single anchor of accountability, which is why it stays in the Ownership layer: it is the one object where a steward is declared, and Data Objects and Data Elements inherit from it. It carries NO business definition of its own -- its meaning comes from its aboutConcept. What it does carry is a perimeter: the subject it is about, and the notions needed to read it." ;
|
||||
pr:hasShortLabel "Business object" .
|
||||
|
||||
|
||||
#################################################################
|
||||
# LAYER 2 - BUSINESS (curated)
|
||||
# LAYER 2 - BUSINESS (defined)
|
||||
#################################################################
|
||||
|
||||
pr:BusinessConcept a owl:Class ; rdfs:subClassOf pr:BusinessLayerObject ;
|
||||
pr:BusinessConcept a owl:Class ; rdfs:subClassOf pr:BusinessLayerObject , pr:DefinedObject ;
|
||||
rdfs:label "Business Concept" ;
|
||||
rdfs:comment "A shared business notion WITHOUT a formula. The presence of a formula is the sole discriminator against Metric (rule BR-003)." .
|
||||
rdfs:comment "A shared business notion WITHOUT a formula. The presence of a formula is the sole discriminator against Metric (rule BR-003)." ;
|
||||
pr:hasShortLabel "Business concept" .
|
||||
|
||||
pr:Metric a owl:Class ; rdfs:subClassOf pr:BusinessLayerObject ;
|
||||
pr:Metric a owl:Class ; rdfs:subClassOf pr:BusinessLayerObject , pr:DefinedObject ;
|
||||
rdfs:label "Metric" ;
|
||||
rdfs:comment "A quantifiable measurement WITH a formula, a unit and a grain. Under BR-013 a Metric carries the harmonised CALCULATION RULE; variants that differ only by analysis context are Data Elements linked back via computedBy." .
|
||||
rdfs:comment "A quantifiable measurement WITH a formula, a unit and a grain. Under BR-013 a Metric carries the harmonised CALCULATION RULE; variants that differ only by analysis context are Data Elements linked back via computedBy." ;
|
||||
pr:hasShortLabel "Metric" .
|
||||
|
||||
pr:KPI a owl:Class ; rdfs:subClassOf pr:Metric ;
|
||||
rdfs:label "Key Performance Indicator" ;
|
||||
rdfs:comment "An elevated Metric. Use this class rather than the deprecated isKPI flag." .
|
||||
rdfs:comment "An elevated Metric. Use this class rather than the deprecated isKPI flag." ;
|
||||
pr:hasShortLabel "KPI" .
|
||||
|
||||
|
||||
#################################################################
|
||||
# LAYER 3 - LOGICAL (curated)
|
||||
# LAYER 3 - LOGICAL (defined)
|
||||
#################################################################
|
||||
|
||||
pr:DataObject a owl:Class ; rdfs:subClassOf pr:LogicalLayerObject ;
|
||||
pr:DataObject a owl:Class ; rdfs:subClassOf pr:LogicalLayerObject , pr:DefinedObject ;
|
||||
rdfs:label "Data Object" ;
|
||||
rdfs:comment "The pivot: represents exactly one Business Object, is composed of Data Elements, and is materialized as one or more Physical Relations. The single junction between governance and technology." .
|
||||
rdfs:comment "The pivot of the whole model: it represents exactly one Business Object, groups the Data Elements that make it up, and is stored in one or more Data Structures. The single junction between what governance defines and what actually runs -- everything upstream is meaning, everything downstream is machinery." ;
|
||||
pr:hasShortLabel "Data object" .
|
||||
|
||||
pr:DataElement a owl:Class ; rdfs:subClassOf pr:LogicalLayerObject ;
|
||||
pr:DataElement a owl:Class ; rdfs:subClassOf pr:LogicalLayerObject , pr:DefinedObject ;
|
||||
rdfs:label "Data Element" ;
|
||||
pr:curationMode "PROPOSED_CONFIRMED" ;
|
||||
rdfs:comment "The finest curated unit. Its mapping to a physical Field is the ONLY place where the curated graph and the observed graph meet: proposed automatically by name and type matching, confirmed by a Data Steward." .
|
||||
pr:authoringMode "PROPOSED_CONFIRMED" ;
|
||||
rdfs:comment "The finest defined unit. Its mapping to a physical Field is the ONLY place where the defined graph and the captured graph meet: proposed automatically by name and type matching, confirmed by a Data Steward." ;
|
||||
pr:hasShortLabel "Data element" .
|
||||
|
||||
pr:DataAsset a owl:Class ; rdfs:subClassOf pr:LogicalLayerObject ;
|
||||
rdfs:label "Data Asset (deprecated)" ;
|
||||
@@ -248,108 +423,152 @@ pr:DataAsset a owl:Class ; rdfs:subClassOf pr:LogicalLayerObject ;
|
||||
|
||||
|
||||
#################################################################
|
||||
# LAYER 4 - DELIVERY (curated)
|
||||
# LAYER 4 - DELIVERY (defined)
|
||||
#################################################################
|
||||
|
||||
pr:DataProduct a owl:Class ; rdfs:subClassOf pr:DeliveryLayerObject ;
|
||||
pr:DataProduct a owl:Class ; rdfs:subClassOf pr:DeliveryLayerObject , pr:DefinedObject ;
|
||||
rdfs:label "Data Product" ;
|
||||
rdfs:comment "The unit of delivery: what carries a version number, an owner, a changelog, a support channel and a list of consumers to warn. It has no single technical counterpart, and should not: physically it often lands on a Snowflake schema or a dbt project, but that is not what defines it. Operated by exactly one Sub-Domain -- it is a consumable, and there must be no ambiguity about who runs it. Candidate alignment: dcat:Dataset / DPROD." .
|
||||
rdfs:comment "The unit of delivery: what carries a version number, an owner, a changelog, a support channel and a list of consumers to warn. It has no single technical counterpart, and should not: physically it often lands on a Snowflake schema or a dbt project, but that is not what defines it. Operated by exactly one Sub-Domain -- it is a consumable, and there must be no ambiguity about who runs it. Candidate alignment: dcat:Dataset / DPROD." ;
|
||||
pr:hasShortLabel "Data product" .
|
||||
|
||||
pr:DataInterface a owl:Class ; rdfs:subClassOf pr:DeliveryLayerObject ;
|
||||
pr:DataInterface a owl:Class ; rdfs:subClassOf pr:DeliveryLayerObject , pr:DefinedObject ;
|
||||
rdfs:label "Data Interface" ;
|
||||
rdfs:comment "The channel through which a consumer actually gets the data: a Snowflake secure share or listing, a consumption schema with its grants, a file extract to a panel partner, an API or reverse-ETL feed. Not a Data Object (that is structure) and not a BI Data Source (that is one consumer among others). Verifiable: a grant, a share or an endpoint either exists or it does not. The output port in data-mesh terms." .
|
||||
rdfs:comment "The channel through which a consumer actually gets the data: a Snowflake secure share or listing, a consumption schema with its grants, a file extract to a panel partner, an API or reverse-ETL feed. Not a Data Object (that is structure) and not a BI Data Source (that is one consumer among others). Verifiable: a grant, a share or an endpoint either exists or it does not. The output port in data-mesh terms." ;
|
||||
pr:hasShortLabel "Data interface" .
|
||||
|
||||
pr:DataContract a owl:Class ; rdfs:subClassOf pr:DeliveryLayerObject ;
|
||||
pr:DataContract a owl:Class ; rdfs:subClassOf pr:DeliveryLayerObject , pr:DefinedObject ;
|
||||
rdfs:label "Data Contract" ;
|
||||
rdfs:comment "The service commitment attached to a Data Product: schema, freshness, quality thresholds, support, breaking-change notice period. Candidate alignment: ODCS (Bitol)." .
|
||||
rdfs:comment "The service commitment attached to a Data Product: schema, freshness, quality thresholds, support, breaking-change notice period. Candidate alignment: ODCS (Bitol)." ;
|
||||
pr:hasShortLabel "Data contract" .
|
||||
|
||||
|
||||
#################################################################
|
||||
# LAYER 5 - PHYSICAL (observed)
|
||||
# LAYER 5 - PHYSICAL (captured)
|
||||
#################################################################
|
||||
|
||||
pr:System a owl:Class ; rdfs:subClassOf pr:PhysicalLayerObject ;
|
||||
pr:System a owl:Class ; rdfs:subClassOf pr:PhysicalLayerObject , pr:DefinedObject ;
|
||||
rdfs:label "System" ;
|
||||
pr:harvestSource "Manually seeded once, then referenced. Replaces the free-text hasSourceSystem." ;
|
||||
rdfs:comment "A real system: Snowflake, MDM Product, MDM Customer, a panel provider feed, Tableau Server, Looker. Making this a node rather than a string is what makes 'what depends on MDM Product?' answerable." .
|
||||
pr:authoringMode "ASSERTED" ;
|
||||
rdfs:comment "A real system: Snowflake, MDM Product, MDM Customer, a panel provider feed, Tableau Server, Looker. Making this a node rather than a string is what makes 'what depends on MDM Product?' answerable." ;
|
||||
pr:hasShortLabel "System" .
|
||||
|
||||
pr:Database a owl:Class ; rdfs:subClassOf pr:PhysicalLayerObject ;
|
||||
pr:Database a owl:Class ; rdfs:subClassOf pr:PhysicalLayerObject , pr:CapturedObject ;
|
||||
rdfs:label "Database" ;
|
||||
pr:harvestSource "SNOWFLAKE.INFORMATION_SCHEMA.DATABASES" .
|
||||
pr:harvestSource "SNOWFLAKE.INFORMATION_SCHEMA.DATABASES" ;
|
||||
pr:hasShortLabel "Database" ;
|
||||
rdfs:comment "The top container of a storage engine: a Snowflake database, a Databricks catalog, a BigQuery project. Named differently by every vendor, which is why the class carries the generic name and the vendor word lives in the harvest annotation." .
|
||||
|
||||
pr:Schema a owl:Class ; rdfs:subClassOf pr:PhysicalLayerObject ;
|
||||
pr:Schema a owl:Class ; rdfs:subClassOf pr:PhysicalLayerObject , pr:CapturedObject ;
|
||||
rdfs:label "Schema" ;
|
||||
pr:harvestSource "SNOWFLAKE.INFORMATION_SCHEMA.SCHEMATA" .
|
||||
pr:harvestSource "SNOWFLAKE.INFORMATION_SCHEMA.SCHEMATA" ;
|
||||
pr:hasShortLabel "Schema" ;
|
||||
rdfs:comment "The namespace grouping structures inside a database. Optional on purpose: Neo4j has no schema and BigQuery calls the level a dataset, so nothing in the shapes may require it." .
|
||||
|
||||
pr:PhysicalRelation a owl:Class ; rdfs:subClassOf pr:PhysicalLayerObject ;
|
||||
rdfs:label "Physical Relation" ;
|
||||
pr:DataStructure a owl:Class ; rdfs:subClassOf pr:PhysicalLayerObject , pr:CapturedObject ;
|
||||
rdfs:label "Data Structure" ;
|
||||
pr:hasShortLabel "Table or view" ;
|
||||
pr:harvestSource "SNOWFLAKE.INFORMATION_SCHEMA.TABLES" ;
|
||||
rdfs:comment "Abstract parent of everything with columns. Specialised because impact traversal differs by kind: a View propagates impact to whatever it reads." .
|
||||
rdfs:comment """Anything that carries Fields and reads as a set of rows. Never instantiated directly: a harvest types the concrete kind, and a viewer shows that kind rather than this parent.
|
||||
|
||||
pr:BaseTable a owl:Class ; rdfs:subClassOf pr:PhysicalRelation ;
|
||||
rdfs:label "Base Table" .
|
||||
WHY THE PARENT EXISTS. Three properties need exactly this set as their scope: storedIn from a Data Object, hasField and its inverse livesIn, and dependsOn. Without it each would have to be tripled, once per concrete kind, and the checks would go with them.
|
||||
|
||||
pr:View a owl:Class ; rdfs:subClassOf pr:PhysicalRelation ;
|
||||
WHY THE NAME CHANGED. It was PhysicalRelation, from Codd, where a relation IS a table. In an ontology "relation" reads as a property, and the word locks the class into the tabular world. A Neo4j node label, a Mongo collection and a Kafka topic all carry fields; none is a relation.
|
||||
|
||||
EXTENSION POINT for non-tabular storage. Nothing at Pernod Ricard is non-tabular today, so no such class is declared -- four terms were deprecated in v1.2 precisely because nobody instantiated them. The day a graph enters scope, the insertion is additive and touches no instance, because subClassOf is transitive:
|
||||
|
||||
pr:Dataset rdfs:subClassOf pr:DataStructure .
|
||||
pr:Graph rdfs:subClassOf pr:DataStructure .
|
||||
pr:BaseTable rdfs:subClassOf pr:Dataset . # instead of DataStructure
|
||||
pr:inSchema rdfs:domain pr:Dataset . # instead of nothing
|
||||
|
||||
Existing BaseTable assertions stay DataStructure by entailment, every shape targeting DataStructure keeps working, and storedIn does not move. The extension point is paid once; the extension is paid when it arrives.""" ;
|
||||
pr:isAbstract true .
|
||||
|
||||
pr:PhysicalRelation a owl:Class ; rdfs:subClassOf pr:PhysicalLayerObject , pr:CapturedObject ;
|
||||
rdfs:label "Physical Relation (deprecated)" ;
|
||||
owl:deprecated true ; dcterms:isReplacedBy pr:DataStructure .
|
||||
|
||||
pr:BaseTable a owl:Class ; rdfs:subClassOf pr:DataStructure ;
|
||||
rdfs:label "Base Table" ;
|
||||
pr:hasShortLabel "Base table" ;
|
||||
rdfs:comment "A structure that physically holds its rows, as opposed to one that derives them. The common case in a warehouse Gold layer, and where impact analysis usually bottoms out." .
|
||||
|
||||
pr:View a owl:Class ; rdfs:subClassOf pr:DataStructure ;
|
||||
rdfs:label "View" ;
|
||||
rdfs:comment "Depends on other relations through dependsOn, so impact is transitive." .
|
||||
rdfs:comment "Depends on other relations through dependsOn, so impact is transitive." ;
|
||||
pr:hasShortLabel "View" .
|
||||
|
||||
pr:MaterializedView a owl:Class ; rdfs:subClassOf pr:PhysicalRelation ;
|
||||
rdfs:label "Materialized View" .
|
||||
pr:MaterializedView a owl:Class ; rdfs:subClassOf pr:DataStructure ;
|
||||
rdfs:label "Materialized View (deprecated)" ;
|
||||
owl:deprecated true ; dcterms:isReplacedBy pr:View ;
|
||||
rdfs:comment "A class standing in for an attribute: what distinguishes it is being derived, which dependsOn already states." .
|
||||
|
||||
pr:ExternalTable a owl:Class ; rdfs:subClassOf pr:PhysicalRelation ;
|
||||
pr:ExternalTable a owl:Class ; rdfs:subClassOf pr:DataStructure ;
|
||||
rdfs:label "External Table" ;
|
||||
rdfs:comment "Backed by files outside the warehouse. Marks the boundary where lineage leaves Snowflake." .
|
||||
rdfs:comment "Backed by files outside the warehouse. Marks the boundary where lineage leaves Snowflake." ;
|
||||
pr:hasShortLabel "External table" .
|
||||
|
||||
pr:Table a owl:Class ; rdfs:subClassOf pr:PhysicalRelation ;
|
||||
rdfs:label "Table (deprecated)" ;
|
||||
owl:deprecated true ;
|
||||
dcterms:isReplacedBy pr:BaseTable ;
|
||||
rdfs:comment "v0.9 term. Kept so that existing assertions still classify. Use BaseTable." .
|
||||
|
||||
pr:Field a owl:Class ; rdfs:subClassOf pr:PhysicalLayerObject ;
|
||||
pr:Field a owl:Class ; rdfs:subClassOf pr:PhysicalLayerObject , pr:CapturedObject ;
|
||||
rdfs:label "Field" ;
|
||||
pr:harvestSource "SNOWFLAKE.INFORMATION_SCHEMA.COLUMNS" ;
|
||||
rdfs:comment "A physical column. The anchor of impact analysis: everything upstream reaches a Field, everything downstream starts from one." .
|
||||
rdfs:comment "A physical column. The anchor of impact analysis: everything upstream reaches a Field, everything downstream starts from one." ;
|
||||
pr:hasShortLabel "Field" .
|
||||
|
||||
pr:KeyConstraint a owl:Class ; rdfs:subClassOf pr:PhysicalLayerObject ;
|
||||
pr:KeyConstraint a owl:Class ; rdfs:subClassOf pr:PhysicalLayerObject , pr:CapturedObject ;
|
||||
rdfs:label "Key Constraint" ;
|
||||
pr:harvestSource "SNOWFLAKE.INFORMATION_SCHEMA.TABLE_CONSTRAINTS + KEY_COLUMN_USAGE" ;
|
||||
rdfs:comment "Gives foreign keys a home. Rule OW-006 says an FK is not a Data Element - true, but it still needs somewhere to live, and it lets the asserted DataObject-references-DataObject edge be checked against reality." .
|
||||
rdfs:comment "Gives foreign keys a home. Rule OW-006 says an FK is not a Data Element - true, but it still needs somewhere to live, and it lets the asserted DataObject-references-DataObject edge be checked against reality." ;
|
||||
pr:isAbstract true ;
|
||||
pr:hasShortLabel "Key constraint" .
|
||||
|
||||
pr:PrimaryKey a owl:Class ; rdfs:subClassOf pr:KeyConstraint ;
|
||||
rdfs:label "Primary Key" .
|
||||
rdfs:label "Primary Key" ;
|
||||
rdfs:comment "The column or set of columns whose values make a row unique. Once harvested it makes a declared grain checkable: a hasGrainElement that does not match the real key is an automatic finding." ;
|
||||
pr:hasShortLabel "Primary key" .
|
||||
|
||||
pr:ForeignKeyConstraint a owl:Class ; rdfs:subClassOf pr:KeyConstraint ;
|
||||
rdfs:label "Foreign Key Constraint" .
|
||||
pr:ForeignKey a owl:Class ; rdfs:subClassOf pr:KeyConstraint ;
|
||||
rdfs:label "Foreign Key" ;
|
||||
pr:hasShortLabel "Foreign key" ;
|
||||
rdfs:comment "A column, or set of columns, whose values must exist in another structure. Rule OW-006 keeps it out of the Data Elements -- it carries no business notion of its own, it is joining plumbing -- but it still needs somewhere to live, and here it makes the asserted qualifiedBy between Data Objects checkable against what the warehouse really enforces." .
|
||||
|
||||
pr:Transformation a owl:Class ; rdfs:subClassOf pr:PhysicalLayerObject , prov:Activity ;
|
||||
pr:ForeignKeyConstraint a owl:Class ;
|
||||
rdfs:label "Foreign Key Constraint (deprecated)" ;
|
||||
owl:deprecated true ; dcterms:isReplacedBy pr:ForeignKey .
|
||||
|
||||
pr:Transformation a owl:Class ; rdfs:subClassOf pr:PhysicalLayerObject , pr:CapturedObject , prov:Activity ;
|
||||
rdfs:label "Transformation" ;
|
||||
pr:harvestSource "dbt manifest.json, Snowflake TASKS, Azure Data Factory pipelines" ;
|
||||
rdfs:comment "The code that turns inputs into outputs: a dbt model, a Snowflake task, an ADF pipeline. Subclass of prov:Activity so that lineage is expressed in a standard vocabulary rather than a private one." .
|
||||
rdfs:comment "The code that turns inputs into outputs: a dbt model, a Snowflake task, an ADF pipeline. Subclass of prov:Activity so that lineage is expressed in a standard vocabulary rather than a private one." ;
|
||||
pr:hasShortLabel "Transformation" .
|
||||
|
||||
|
||||
#################################################################
|
||||
# LAYER 6 - CONSUMPTION (observed)
|
||||
# LAYER 6 - CONSUMPTION (captured)
|
||||
#################################################################
|
||||
|
||||
pr:BIWorkspace a owl:Class ; rdfs:subClassOf pr:ConsumptionLayerObject ;
|
||||
pr:BIWorkspace a owl:Class ; rdfs:subClassOf pr:ConsumptionLayerObject , pr:CapturedObject ;
|
||||
rdfs:label "BI Workspace" ;
|
||||
pr:harvestSource "Tableau Metadata API (Project/Site) ; Looker API (Model)" .
|
||||
pr:harvestSource "Tableau Metadata API (Project/Site) ; Looker API (Model)" ;
|
||||
pr:hasShortLabel "BI workspace" ;
|
||||
rdfs:comment "The container a BI tool organises its content in: a Tableau project or site, a Looker model. Useful mainly for attribution -- it is where you look to find who publishes what." .
|
||||
|
||||
pr:BIDataSource a owl:Class ; rdfs:subClassOf pr:ConsumptionLayerObject ;
|
||||
pr:BIDataSource a owl:Class ; rdfs:subClassOf pr:ConsumptionLayerObject , pr:CapturedObject ;
|
||||
rdfs:label "BI Data Source" ;
|
||||
pr:harvestSource "Tableau Published Datasource ; Looker Explore" ;
|
||||
rdfs:comment "The semantic surface a report is built on. This is where BI meets the warehouse." .
|
||||
rdfs:comment "The semantic surface a report is built on. This is where BI meets the warehouse." ;
|
||||
pr:hasShortLabel "BI data source" .
|
||||
|
||||
pr:BIField a owl:Class ; rdfs:subClassOf pr:ConsumptionLayerObject ;
|
||||
pr:BIField a owl:Class ; rdfs:subClassOf pr:ConsumptionLayerObject , pr:CapturedObject ;
|
||||
rdfs:label "BI Field" ;
|
||||
pr:harvestSource "Tableau calculated fields ; LookML measures and dimensions" ;
|
||||
rdfs:comment "A field as a business user sees it. realizesMetric is what turns a certified Metric from a definition on paper into something measurable against the real BI estate." .
|
||||
rdfs:comment "A field as a business user sees it. exposesMetric is what turns a certified Metric from a definition on paper into something measurable against the real BI estate." ;
|
||||
pr:hasShortLabel "BI field" .
|
||||
|
||||
pr:BIReport a owl:Class ; rdfs:subClassOf pr:ConsumptionLayerObject ;
|
||||
pr:BIReport a owl:Class ; rdfs:subClassOf pr:ConsumptionLayerObject , pr:CapturedObject ;
|
||||
rdfs:label "BI Report" ;
|
||||
pr:harvestSource "Tableau Workbook/Sheet ; Looker Look/Dashboard" ;
|
||||
rdfs:comment "What a Data Governance Lead actually cares about in an impact analysis: not that a column changed, but which reports break and who to warn." .
|
||||
rdfs:comment "What a Data Governance Lead actually cares about in an impact analysis: not that a column changed, but which reports break and who to warn." ;
|
||||
pr:hasShortLabel "BI report" .
|
||||
|
||||
|
||||
#################################################################
|
||||
@@ -357,35 +576,43 @@ pr:BIReport a owl:Class ; rdfs:subClassOf pr:ConsumptionLayerObject ;
|
||||
#################################################################
|
||||
|
||||
pr:Actor a owl:Class ; rdfs:label "Actor" ;
|
||||
rdfs:comment "A person or a team. Deliberately NOT a MetaModelObject: people are not subject to ownership and lifecycle rules." .
|
||||
rdfs:comment "A person or a team. Deliberately NOT a MetaModelObject: people are not subject to ownership and lifecycle rules." ;
|
||||
pr:hasShortLabel "Actor" ;
|
||||
pr:isAbstract true .
|
||||
|
||||
pr:DataDomainOwner a owl:Class ; rdfs:subClassOf pr:Actor ;
|
||||
rdfs:label "Data Domain Owner" ;
|
||||
rdfs:comment "Accountable for a Data Domain. Exactly one per domain." .
|
||||
rdfs:comment "Accountable for a Data Domain. Exactly one per domain." ;
|
||||
pr:hasShortLabel "Data domain owner" .
|
||||
|
||||
pr:SubDomainOwner a owl:Class ; rdfs:subClassOf pr:Actor ;
|
||||
rdfs:label "Sub-Domain Owner" ;
|
||||
rdfs:comment "Accountable for a Sub-Domain. Exactly one per sub-domain." .
|
||||
rdfs:label "Data Sub-Domain Owner" ;
|
||||
rdfs:comment "Accountable for a Sub-Domain, exactly one per sub-domain. Named to match Data Domain Owner: the two roles are the same responsibility at two levels of the tree, and calling one 'Data Domain Owner' and the other 'Sub-Domain Owner' suggested a difference that does not exist." ;
|
||||
pr:hasShortLabel "Data sub-domain owner" .
|
||||
|
||||
pr:DataGovernanceLead a owl:Class ; rdfs:subClassOf pr:Actor ;
|
||||
rdfs:label "Data Governance Lead" ;
|
||||
pr:hasAcronym "DGL" ;
|
||||
rdfs:comment "Runs governance for a Data Domain. The acronym belongs in hasAcronym, not in the label (rule NR-005)." .
|
||||
rdfs:comment "Runs governance for a Data Domain. The acronym belongs in hasAcronym, not in the label (rule NR-005)." ;
|
||||
pr:hasShortLabel "Data governance lead" .
|
||||
|
||||
pr:DataProductOwner a owl:Class ; rdfs:subClassOf pr:Actor ;
|
||||
rdfs:label "Data Product Owner" ;
|
||||
rdfs:comment "Runs a Data Product day to day: releases, changelog, support, consumer communication. Belongs to the Sub-Domain that operates the product." .
|
||||
rdfs:comment "Runs a Data Product day to day: releases, changelog, support, consumer communication. Belongs to the Sub-Domain that operates the product." ;
|
||||
pr:hasShortLabel "Product owner" .
|
||||
|
||||
pr:DataSteward a owl:Class ; rdfs:subClassOf pr:Actor ;
|
||||
rdfs:label "Data Steward" ;
|
||||
rdfs:comment "Monitors a Business Object. Declared there and nowhere else: Data Objects and Data Elements inherit their steward, so that no column can ever have two answers to 'who watches this'." .
|
||||
rdfs:comment "Monitors a Business Object. Declared there and nowhere else: Data Objects and Data Elements inherit their steward, so that no column can ever have two answers to 'who watches this'." ;
|
||||
pr:hasShortLabel "Steward" .
|
||||
|
||||
|
||||
#################################################################
|
||||
# CONTROLLED VOCABULARIES
|
||||
#################################################################
|
||||
|
||||
pr:ActivationStatus a owl:Class ; rdfs:label "Activation Status" .
|
||||
pr:ActivationStatus a owl:Class ; rdfs:label "Activation Status" ;
|
||||
rdfs:comment "How far a Data Domain has gone in adopting the governance framework: Not Activated, Light Activation, Full Activation. A statement of maturity, not of quality." .
|
||||
pr:NotActivated a pr:ActivationStatus ; rdfs:label "Not Activated" .
|
||||
pr:LightActivation a pr:ActivationStatus ; rdfs:label "Light Activation" .
|
||||
pr:FullActivation a pr:ActivationStatus ; rdfs:label "Full Activation" .
|
||||
@@ -396,7 +623,8 @@ pr:Development a pr:Environment ; rdfs:label "Development" .
|
||||
pr:UserAcceptance a pr:Environment ; rdfs:label "User Acceptance" .
|
||||
pr:Production a pr:Environment ; rdfs:label "Production" .
|
||||
|
||||
pr:SystemType a owl:Class ; rdfs:label "System Type" .
|
||||
pr:SystemType a owl:Class ; rdfs:label "System Type" ;
|
||||
rdfs:comment "What kind of system this is -- source application, master data system, data platform, BI tool, external provider. Coarse on purpose: it exists to group systems in a view, not to model them." .
|
||||
pr:SourceApplication a pr:SystemType ; rdfs:label "Source Application" .
|
||||
pr:MasterDataSystem a pr:SystemType ; rdfs:label "Master Data System" .
|
||||
pr:DataPlatform a pr:SystemType ; rdfs:label "Data Platform" .
|
||||
@@ -495,10 +723,14 @@ pr:hasElement a owl:ObjectProperty ;
|
||||
rdfs:label "has element" ;
|
||||
rdfs:domain pr:DataObject ; rdfs:range pr:DataElement .
|
||||
|
||||
pr:references a owl:ObjectProperty ;
|
||||
rdfs:label "references" ;
|
||||
pr:qualifiedBy a owl:ObjectProperty ;
|
||||
rdfs:label "qualified by" ;
|
||||
rdfs:domain pr:DataObject ; rdfs:range pr:DataObject ;
|
||||
rdfs:comment "Foreign-key reference between Data Objects. NOT ownership. Now checkable against the harvested ForeignKeyConstraint objects." .
|
||||
rdfs:comment "The Data Objects that give this one its analysis context: a fact dataset is qualified by Product, Customer, Currency and Date. Physically a foreign key, which is why it is checkable against the harvested ForeignKeyConstraint objects -- but the name says the reason rather than the mechanism. NOT ownership. It is also what carries the grain of a fact object, whose own elements are measures and whose keys are not Data Elements (OW-006)." .
|
||||
|
||||
pr:references a owl:ObjectProperty ;
|
||||
rdfs:label "references (deprecated)" ;
|
||||
owl:deprecated true ; dcterms:isReplacedBy pr:qualifiedBy .
|
||||
|
||||
#################################################################
|
||||
# OBJECT PROPERTIES - Delivery
|
||||
@@ -516,7 +748,7 @@ pr:exposes a owl:ObjectProperty ;
|
||||
|
||||
pr:servedBy a owl:ObjectProperty ;
|
||||
rdfs:label "served by" ;
|
||||
rdfs:domain pr:DataInterface ; rdfs:range pr:PhysicalRelation ;
|
||||
rdfs:domain pr:DataInterface ; rdfs:range pr:DataStructure ;
|
||||
rdfs:comment "What physically backs the interface: the shared relations, the extract source." .
|
||||
|
||||
pr:governedBy a owl:ObjectProperty , owl:FunctionalProperty ;
|
||||
@@ -539,22 +771,43 @@ pr:usesDataObject a owl:ObjectProperty ;
|
||||
|
||||
|
||||
#################################################################
|
||||
# OBJECT PROPERTIES - the curated / observed seam
|
||||
# OBJECT PROPERTIES - the defined / captured seam
|
||||
# These two are the ONLY edges crossing from asserted to harvested.
|
||||
# Everything else is either pure governance or pure machine.
|
||||
#################################################################
|
||||
|
||||
pr:materializedAs a owl:ObjectProperty ;
|
||||
rdfs:label "materialized as" ;
|
||||
rdfs:domain pr:DataObject ; rdfs:range pr:PhysicalRelation ;
|
||||
pr:curationMode "PROPOSED_CONFIRMED" ;
|
||||
rdfs:comment "Data Object to physical relation. Not functional: the same Data Object exists in DEV, UAT and PROD, and sometimes across several tables." .
|
||||
rdfs:label "materialized as (deprecated)" ;
|
||||
owl:deprecated true ; dcterms:isReplacedBy pr:storedIn ;
|
||||
rdfs:comment "Redundant once storedIn serves two granularities. One verb fewer, and the seam counts one relation instead of two." .
|
||||
|
||||
pr:storedIn a owl:ObjectProperty ;
|
||||
rdfs:label "stored in" ;
|
||||
pr:authoringMode "PROPOSED_CONFIRMED" ;
|
||||
rdfs:comment """Where a logical artefact actually sits. One relation at two granularities, like represents:
|
||||
DataObject storedIn DataStructure (a table, a view, later a node label)
|
||||
DataElement storedIn Field (a column, later a node property)
|
||||
|
||||
Range left open and scoped by SHACL per class, because the two uses have different targets.
|
||||
|
||||
THE SEAM. This is the one relation crossing from what governance defines to what a harvester captures, and the one thing a steward confirms: the reconciliation job proposes the mapping on fully qualified name and type, a person accepts it. It is also what makes end-to-end impact analysis possible.
|
||||
|
||||
Not functional: the same object exists in DEV, UAT and PROD, and a Data Element may sit in several Fields -- see primaryLocation for the authoritative one. Renamed from physicalizedIn, which was jargon.""" .
|
||||
|
||||
pr:primaryLocation a owl:ObjectProperty , owl:FunctionalProperty ;
|
||||
rdfs:subPropertyOf pr:storedIn ;
|
||||
rdfs:label "primary location" ;
|
||||
rdfs:domain pr:DataElement ; rdfs:range pr:Field ;
|
||||
pr:authoringMode "PROPOSED_CONFIRMED" ;
|
||||
rdfs:comment """The authoritative Field among the several a Data Element may sit in -- the others being copies, replicas or downstream materialisations.
|
||||
|
||||
A SUB-PROPERTY of storedIn on purpose, not a parallel relation. Entailment then guarantees the primary location is always among the locations, so the two can never disagree; two independent properties would allow a Field marked primary here and absent from storedIn there.
|
||||
|
||||
Primary and replica rather than master and slave: the industry has moved on, and it reads better to the business.""" .
|
||||
|
||||
pr:physicalizedIn a owl:ObjectProperty ;
|
||||
rdfs:label "physicalized in" ;
|
||||
rdfs:domain pr:DataElement ; rdfs:range pr:Field ;
|
||||
pr:curationMode "PROPOSED_CONFIRMED" ;
|
||||
rdfs:comment "Data Element to physical column, 1..n. Proposed by the reconciliation job on fullyQualifiedName and type, confirmed by a Data Steward. This single edge is what makes end-to-end impact analysis possible." .
|
||||
rdfs:label "physicalized in (deprecated)" ;
|
||||
owl:deprecated true ; dcterms:isReplacedBy pr:storedIn .
|
||||
|
||||
|
||||
#################################################################
|
||||
@@ -564,7 +817,11 @@ pr:physicalizedIn a owl:ObjectProperty ;
|
||||
pr:sourcedFrom a owl:ObjectProperty ;
|
||||
rdfs:label "sourced from" ;
|
||||
rdfs:range pr:System ;
|
||||
rdfs:comment "Replaces the free-text hasSourceSystem. Domain deliberately omitted: applies to Data Elements, Physical Relations and BI Data Sources alike." .
|
||||
rdfs:comment """The System holding the authoritative data for this object. Replaces the free-text hasSource, and answers a question that text could not: which objects depend on MDM Product.
|
||||
|
||||
Domain deliberately omitted -- it applies to Data Elements, Data Structures, Metrics and BI Data Sources alike, and the scope lives in the shapes.
|
||||
|
||||
It also settles a naming collision. As a string, "source" meant a system here and a BI surface in BIDataSource, two readings of one word. As a relation towards System, the two are different kinds of thing and the ambiguity disappears.""" .
|
||||
|
||||
pr:hasSystemType a owl:ObjectProperty , owl:FunctionalProperty ;
|
||||
rdfs:label "has system type" ;
|
||||
@@ -580,25 +837,27 @@ pr:inDatabase a owl:ObjectProperty , owl:FunctionalProperty ;
|
||||
|
||||
pr:inSchema a owl:ObjectProperty , owl:FunctionalProperty ;
|
||||
rdfs:label "in schema" ;
|
||||
rdfs:domain pr:PhysicalRelation ; rdfs:range pr:Schema .
|
||||
rdfs:range pr:Schema ;
|
||||
rdfs:comment "Domain deliberately not declared, and the shapes must keep this OPTIONAL: Neo4j has no schema and BigQuery calls the level a dataset. A minCount 1 would break on the first harvest outside Snowflake. It is also the only property that would distinguish a tabular Dataset from a Graph, which is why that intermediate level is not declared yet -- one client does not pay for a level of hierarchy." .
|
||||
|
||||
pr:hasField a owl:ObjectProperty ;
|
||||
rdfs:label "has field" ;
|
||||
rdfs:domain pr:PhysicalRelation ; rdfs:range pr:Field ;
|
||||
rdfs:domain pr:DataStructure ; rdfs:range pr:Field ;
|
||||
owl:inverseOf pr:livesIn ;
|
||||
rdfs:comment "Domain widened from Table to PhysicalRelation so that views and external tables carry columns too. Inverse now declared - in v0.9 hasField and livesIn were inverses in fact but not in the model, so they were free to diverge." .
|
||||
|
||||
pr:livesIn a owl:ObjectProperty , owl:FunctionalProperty ;
|
||||
rdfs:label "lives in" ;
|
||||
rdfs:domain pr:Field ; rdfs:range pr:PhysicalRelation .
|
||||
rdfs:domain pr:Field ; rdfs:range pr:DataStructure .
|
||||
|
||||
pr:deployedIn a owl:ObjectProperty , owl:FunctionalProperty ;
|
||||
rdfs:label "deployed in" ;
|
||||
rdfs:domain pr:ObservedObject ; rdfs:range pr:Environment .
|
||||
rdfs:domain pr:CapturedObject ; rdfs:range pr:Environment .
|
||||
|
||||
pr:constrainsRelation a owl:ObjectProperty , owl:FunctionalProperty ;
|
||||
rdfs:label "constrains relation" ;
|
||||
rdfs:domain pr:KeyConstraint ; rdfs:range pr:PhysicalRelation .
|
||||
rdfs:label "constrains relation (deprecated)" ;
|
||||
owl:deprecated true ;
|
||||
rdfs:comment "Derivable: constrainsField then livesIn gives the relation. An asserted edge redundant with an existing path is a divergence waiting to happen." .
|
||||
|
||||
pr:constrainsField a owl:ObjectProperty ;
|
||||
rdfs:label "constrains field" ;
|
||||
@@ -607,24 +866,24 @@ pr:constrainsField a owl:ObjectProperty ;
|
||||
|
||||
pr:fromField a owl:ObjectProperty ;
|
||||
rdfs:label "from field" ;
|
||||
rdfs:domain pr:ForeignKeyConstraint ; rdfs:range pr:Field .
|
||||
rdfs:domain pr:ForeignKey ; rdfs:range pr:Field .
|
||||
|
||||
pr:toField a owl:ObjectProperty ;
|
||||
rdfs:label "to field" ;
|
||||
rdfs:domain pr:ForeignKeyConstraint ; rdfs:range pr:Field .
|
||||
rdfs:domain pr:ForeignKey ; rdfs:range pr:Field .
|
||||
|
||||
pr:consumes a owl:ObjectProperty ;
|
||||
rdfs:subPropertyOf prov:used ;
|
||||
rdfs:label "consumes" ;
|
||||
rdfs:domain pr:Transformation ; rdfs:range pr:PhysicalRelation .
|
||||
rdfs:domain pr:Transformation ; rdfs:range pr:DataStructure .
|
||||
|
||||
pr:produces a owl:ObjectProperty ;
|
||||
rdfs:label "produces" ;
|
||||
rdfs:domain pr:Transformation ; rdfs:range pr:PhysicalRelation .
|
||||
rdfs:domain pr:Transformation ; rdfs:range pr:DataStructure .
|
||||
|
||||
pr:dependsOn a owl:ObjectProperty , owl:TransitiveProperty ;
|
||||
rdfs:label "depends on" ;
|
||||
rdfs:domain pr:PhysicalRelation ; rdfs:range pr:PhysicalRelation ;
|
||||
rdfs:domain pr:DataStructure ; rdfs:range pr:DataStructure ;
|
||||
pr:harvestSource "SNOWFLAKE.ACCOUNT_USAGE.OBJECT_DEPENDENCIES" ;
|
||||
rdfs:comment "Relation-level dependency, view to table. Transitive by nature; in practice compute the closure at query time in Snowflake rather than materialising it with a reasoner." .
|
||||
|
||||
@@ -646,7 +905,7 @@ pr:inWorkspace a owl:ObjectProperty , owl:FunctionalProperty ;
|
||||
|
||||
pr:biConsumes a owl:ObjectProperty ;
|
||||
rdfs:label "BI consumes" ;
|
||||
rdfs:domain pr:BIDataSource ; rdfs:range pr:PhysicalRelation ;
|
||||
rdfs:domain pr:BIDataSource ; rdfs:range pr:DataStructure ;
|
||||
pr:harvestSource "Tableau Metadata API (GraphQL) ; Looker Explore SQL" .
|
||||
|
||||
pr:hasBIField a owl:ObjectProperty ;
|
||||
@@ -664,11 +923,11 @@ pr:biDerivedFrom a owl:ObjectProperty ;
|
||||
rdfs:domain pr:BIField ; rdfs:range pr:Field ;
|
||||
rdfs:comment "Closes the last gap in the impact chain: from a dashboard field back to the warehouse column." .
|
||||
|
||||
pr:realizesMetric a owl:ObjectProperty , owl:FunctionalProperty ;
|
||||
rdfs:label "realizes metric" ;
|
||||
pr:exposesMetric a owl:ObjectProperty , owl:FunctionalProperty ;
|
||||
rdfs:label "exposes metric" ;
|
||||
rdfs:domain pr:BIField ; rdfs:range pr:Metric ;
|
||||
pr:curationMode "PROPOSED_CONFIRMED" ;
|
||||
rdfs:comment """The harmonisation measure: how many distinct BI Fields realize one certified Metric, and do their expressions agree. Turns 'we harmonised the definitions' from a claim into a count over the real BI estate.
|
||||
pr:authoringMode "PROPOSED_CONFIRMED" ;
|
||||
rdfs:comment """The harmonisation measure: how many distinct BI Fields expose one certified Metric, and do their expressions agree. Turns 'we harmonised the definitions' from a claim into a count over the real BI estate.
|
||||
|
||||
In principle this is derivable from the lineage chain Metric -> DataElement -> Field -> BIField, since lineage carries semantic inheritance. In practice that chain will have holes -- four hops, one of them PROPOSED_CONFIRMED, another harvested from an API that does not resolve custom SQL, extracts or cascaded calculated fields -- and the question it answers is too important to depend on them.
|
||||
|
||||
@@ -695,7 +954,7 @@ pr:ownedBy a owl:ObjectProperty ;
|
||||
|
||||
pr:owningDomain a owl:ObjectProperty , owl:FunctionalProperty ;
|
||||
rdfs:label "owning Data Domain" ; rdfs:range pr:DataDomain ;
|
||||
rdfs:comment """The single Data Domain with authority to define the object's rule. Absent on Observed Objects: a Snowflake column is attributed through the Data Element mapped to it, never asserted directly.
|
||||
rdfs:comment """The single Data Domain with authority to define the object's rule. Absent on Captured Objects: a Snowflake column is attributed through the Data Element mapped to it, never asserted directly.
|
||||
|
||||
Unlike the steward, this one is RE-DECLARABLE on a Data Element. It is inherited by default from the parent chain, but OW-002 attributes an element to the domain that defines its RULE, and a borrowed element -- Brand Code sitting in a Sell Out fact object -- has its rule elsewhere. A Data Element whose owningDomain differs from its parent's is a borrowed element: a signal to check against its represents, not an anomaly to fix.""" .
|
||||
|
||||
@@ -734,11 +993,25 @@ pr:hasActivationStatus a owl:ObjectProperty , owl:FunctionalProperty ;
|
||||
|
||||
pr:hasIdentifier a owl:DatatypeProperty , owl:FunctionalProperty ; rdfs:label "identifier" ; rdfs:range xsd:string .
|
||||
pr:hasName a owl:DatatypeProperty , owl:FunctionalProperty ; rdfs:label "canonical name" ; rdfs:range xsd:string .
|
||||
|
||||
pr:hasShortLabel a owl:DatatypeProperty , owl:FunctionalProperty ;
|
||||
rdfs:label "short label" ; rdfs:range xsd:string ;
|
||||
rdfs:comment """The name screens use, where the canonical name is the exhaustive one: Domain for Data Domain, SODH Gold for Sell Out Data Hub - Gold.
|
||||
|
||||
Declared rather than left to each interface. The same object was being called two different things depending on where you looked, because every viewer kept a display dictionary of its own. One name is authoritative, the other is short, and both live in the model.""" .
|
||||
pr:hasStatus a owl:DatatypeProperty ; rdfs:label "status" ; rdfs:range xsd:string .
|
||||
pr:hasVersion a owl:DatatypeProperty ; rdfs:label "version" ; rdfs:range xsd:string .
|
||||
pr:createdOn a owl:DatatypeProperty ; rdfs:label "created on" ; rdfs:range xsd:date .
|
||||
pr:lastReviewedOn a owl:DatatypeProperty ; rdfs:label "last reviewed on" ; rdfs:range xsd:date .
|
||||
pr:hasSource a owl:DatatypeProperty ; rdfs:label "back-doc source" ; rdfs:range xsd:string .
|
||||
pr:hasSource a owl:DatatypeProperty ; rdfs:label "source of truth (deprecated)" ; rdfs:range xsd:string ;
|
||||
owl:deprecated true ; dcterms:isReplacedBy pr:sourcedFrom ;
|
||||
rdfs:comment """The system holding the authoritative data for this object: SODH Gold, MDM Product, Group Finance.
|
||||
|
||||
ONE MEANING ONLY. It had drifted into naming the document a record came from ("SODH back-doc v0.7"), and into packing a system and a relation into one string ("SODH Gold / F_SELL_OUT"). The relation belongs in physicalName; the document belongs nowhere -- provenance of a record is not a property of the thing it describes.
|
||||
|
||||
MUST NOT appear on a Business Concept: a concept has no data source. Its authority is its owning domain and its definition is published in the glossary. Enforced by SHACL.
|
||||
|
||||
On a Metric it is asserted rather than derived, by exception, because "where can I get this metric" is a first-order question and the derivation chain (computedBy, then the data object holding the element) will have gaps before harvesting. A shape checks the assertion against the elements.""" .
|
||||
pr:arbitrationStatus a owl:DatatypeProperty ; rdfs:label "arbitration status" ; rdfs:range xsd:string ;
|
||||
rdfs:comment "TO_ARBITRATE blocks publication (OW-007). Use this on the object rather than tracking arbitration in a side spreadsheet." .
|
||||
|
||||
@@ -792,18 +1065,21 @@ pr:sourceIdentifier a owl:DatatypeProperty , owl:FunctionalProperty ;
|
||||
rdfs:comment "Native immutable id in the source system (Snowflake object id, Tableau LUID, Looker id). Makes reconciliation idempotent and survives renames, which names do not." .
|
||||
|
||||
pr:harvestedOn a owl:DatatypeProperty ; rdfs:label "harvested on" ; rdfs:range xsd:dateTime ;
|
||||
rdfs:comment "Staleness marker. An Observed Object not refreshed by the last run is a deletion candidate, not a fact." .
|
||||
rdfs:comment "Staleness marker. A Captured Object not refreshed by the last run is a deletion candidate, not a fact." .
|
||||
|
||||
pr:hasPhysicalDataType a owl:DatatypeProperty ; rdfs:label "physical data type" ; rdfs:range xsd:string .
|
||||
pr:isNullable a owl:DatatypeProperty ; rdfs:label "is nullable" ; rdfs:range xsd:boolean .
|
||||
pr:ordinalPosition a owl:DatatypeProperty ; rdfs:label "ordinal position" ; rdfs:range xsd:integer .
|
||||
pr:hasViewDefinition a owl:DatatypeProperty ; rdfs:label "view definition" ; rdfs:range xsd:string .
|
||||
pr:hasVendor a owl:DatatypeProperty ; rdfs:label "vendor" ; rdfs:range xsd:string .
|
||||
pr:hasVendor a owl:DatatypeProperty ; rdfs:label "vendor (deprecated)" ; rdfs:range xsd:string ;
|
||||
owl:deprecated true .
|
||||
pr:definedBy a owl:DatatypeProperty ; rdfs:label "defined by (code URI)" ; rdfs:range xsd:anyURI ;
|
||||
rdfs:comment "Git URI of the code behind a Transformation. Connects the graph to the repository." .
|
||||
|
||||
# ---- Usage evidence (the rationalisation argument)
|
||||
pr:approximateRowCount a owl:DatatypeProperty ; rdfs:label "approximate row count" ; rdfs:range xsd:integer .
|
||||
pr:approximateRowCount a owl:DatatypeProperty ; rdfs:label "approximate row count (deprecated)" ; rdfs:range xsd:integer ;
|
||||
owl:deprecated true ;
|
||||
rdfs:comment "Volumetry with no identified use. queryCount30d and lastQueriedOn carry the rationalisation argument; row count does not." .
|
||||
pr:lastQueriedOn a owl:DatatypeProperty ; rdfs:label "last queried on" ; rdfs:range xsd:dateTime ;
|
||||
pr:harvestSource "SNOWFLAKE.ACCOUNT_USAGE.ACCESS_HISTORY" ;
|
||||
rdfs:comment "Lets the model say 'documented but unused for eight months'. A rationalisation argument that costs nothing and is denominated in warehouse spend." .
|
||||
@@ -818,7 +1094,7 @@ pr:hasExpression a owl:DatatypeProperty ; rdfs:label "BI expression" ; rdfs:rang
|
||||
# DISJOINTNESS
|
||||
#################################################################
|
||||
|
||||
[] a owl:AllDisjointClasses ; owl:members ( pr:CuratedObject pr:ObservedObject ) .
|
||||
[] a owl:AllDisjointClasses ; owl:members ( pr:DefinedObject pr:CapturedObject ) .
|
||||
|
||||
[] a owl:AllDisjointClasses ; owl:members (
|
||||
pr:OwnershipLayerObject pr:BusinessLayerObject pr:LogicalLayerObject
|
||||
@@ -830,9 +1106,9 @@ pr:hasExpression a owl:DatatypeProperty ; rdfs:label "BI expression" ; rdfs:rang
|
||||
[] a owl:AllDisjointClasses ; owl:members ( pr:BusinessConcept pr:Metric ) .
|
||||
|
||||
[] a owl:AllDisjointClasses ; owl:members (
|
||||
pr:BaseTable pr:View pr:MaterializedView pr:ExternalTable ) .
|
||||
pr:BaseTable pr:View pr:ExternalTable ) .
|
||||
|
||||
[] a owl:AllDisjointClasses ; owl:members ( pr:PrimaryKey pr:ForeignKeyConstraint ) .
|
||||
[] a owl:AllDisjointClasses ; owl:members ( pr:PrimaryKey pr:ForeignKey ) .
|
||||
|
||||
[] a owl:AllDisjointClasses ; owl:members (
|
||||
pr:BIWorkspace pr:BIDataSource pr:BIField pr:BIReport ) .
|
||||
|
||||
@@ -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()
|
||||
File diff suppressed because it is too large
Load Diff
+1117
-753
File diff suppressed because it is too large
Load Diff
@@ -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." ] .
|
||||
+116
-39
@@ -13,8 +13,8 @@
|
||||
# ----------------------
|
||||
# 1. SPLIT BY PROVENANCE. v0.1.1 targeted pr:MetaModelObject and demanded an
|
||||
# identifier, a name, an owner, an owning domain, a status and a version on
|
||||
# everything. Applied to the harvested half of the graph that would fail on
|
||||
# every one of ~50 000 Snowflake columns. Curated and Observed objects now
|
||||
# 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
|
||||
@@ -40,32 +40,32 @@
|
||||
|
||||
<https://ontology.pernod-ricard.com/metamodel/shapes/>
|
||||
dcterms:title "PR Data Meta Model - SHACL Shapes" ;
|
||||
dcterms:modified "2026-07-26"^^xsd:date ;
|
||||
dcterms:modified "2026-07-30"^^xsd:date ;
|
||||
rdfs:comment "Executable form of the rulebook. Validated by scripts/run_shacl_validation.py." .
|
||||
|
||||
|
||||
#################################################################
|
||||
# IDENTITY AND LIFECYCLE -- curated objects only
|
||||
# IDENTITY AND LIFECYCLE -- defined objects only
|
||||
#################################################################
|
||||
|
||||
prs:CuratedObjectShape a sh:NodeShape ;
|
||||
sh:targetClass pr:CuratedObject ;
|
||||
rdfs:comment "Applies to everything governance asserts. Observed objects are deliberately exempt: see ObservedObjectShape." ;
|
||||
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:hasIdentifier ; sh:datatype xsd:string ;
|
||||
sh:path pr:identifier ; sh:datatype xsd:string ;
|
||||
sh:minCount 1 ; sh:maxCount 1 ;
|
||||
sh:severity sh:Violation ;
|
||||
sh:message "DQ-001 -- every curated object carries exactly one identifier. NOTE: uniqueness across the graph is NOT tested here; SHACL checks cardinality only. The reconciliation job tests uniqueness." ] ;
|
||||
sh: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:hasName ; sh:datatype xsd:string ;
|
||||
sh:path pr:name ; sh:datatype xsd:string ;
|
||||
sh:minCount 1 ; sh:maxCount 1 ; sh:minLength 3 ;
|
||||
sh:severity sh:Violation ;
|
||||
sh:message "Every curated object carries exactly one canonical name." ] ;
|
||||
sh:message "Every defined object carries exactly one canonical name." ] ;
|
||||
|
||||
sh:property [
|
||||
sh:path pr:hasStatus ; sh:minCount 1 ;
|
||||
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." ] ;
|
||||
@@ -76,13 +76,13 @@ prs:CuratedObjectShape a sh:NodeShape ;
|
||||
sh:message "OW-001 -- at most one owning Data Domain. On a Data Element this may be re-declared to differ from the parent chain: that marks a borrowed element." ] ;
|
||||
|
||||
sh:property [
|
||||
sh:path pr:hasVersion ; sh:maxCount 1 ;
|
||||
sh: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:CuratedObject ;
|
||||
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 [
|
||||
@@ -91,7 +91,7 @@ prs:PublishedShape a sh:NodeShape ;
|
||||
sh:select """
|
||||
PREFIX pr: <https://ontology.pernod-ricard.com/metamodel/>
|
||||
SELECT $this WHERE {
|
||||
$this pr:hasStatus "PUBLISHED" ;
|
||||
$this pr:status "PUBLISHED" ;
|
||||
pr:arbitrationStatus "TO_ARBITRATE" .
|
||||
}""" ] ;
|
||||
|
||||
@@ -101,17 +101,17 @@ prs:PublishedShape a sh:NodeShape ;
|
||||
sh:select """
|
||||
PREFIX pr: <https://ontology.pernod-ricard.com/metamodel/>
|
||||
SELECT $this WHERE {
|
||||
$this pr:hasStatus "PUBLISHED" .
|
||||
$this pr:status "PUBLISHED" .
|
||||
FILTER NOT EXISTS { $this pr:owningDomain ?d }
|
||||
}""" ] .
|
||||
|
||||
|
||||
#################################################################
|
||||
# OBSERVED OBJECTS -- harvested, never hand-edited
|
||||
# OBSERVED OBJECTS -- captured, never hand-edited
|
||||
#################################################################
|
||||
|
||||
prs:ObservedObjectShape a sh:NodeShape ;
|
||||
sh:targetClass pr:ObservedObject ;
|
||||
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 [
|
||||
@@ -124,12 +124,26 @@ prs:ObservedObjectShape a sh:NodeShape ;
|
||||
sh:path pr:harvestedOn ; sh:datatype xsd:dateTime ;
|
||||
sh:minCount 1 ; sh:maxCount 1 ;
|
||||
sh:severity sh:Violation ;
|
||||
sh:message "Every observed object records when it was harvested. Without it there is no way to tell a current fact from a stale one." ] ;
|
||||
sh: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 "Observed objects are not attributed directly -- ownership flows from the Data Element mapped to them." ] .
|
||||
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 ;
|
||||
@@ -209,9 +223,14 @@ prs:BusinessObjectShape a sh:NodeShape ;
|
||||
sh:message "The Business Object is where a steward is declared -- Data Objects and Data Elements inherit from here." ] ;
|
||||
|
||||
sh:property [
|
||||
sh:path pr:hasBusinessDefinition ; sh:maxCount 0 ;
|
||||
sh: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: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." ] .
|
||||
|
||||
|
||||
#################################################################
|
||||
@@ -223,30 +242,35 @@ prs:BusinessConceptShape a sh:NodeShape ;
|
||||
rdfs:comment "Note what is NOT here: no requirement that a Concept be realized by any Data Element or measured by any Metric. The business layer is allowed to run ahead of the data. Coverage is a dashboard metric, not a rule." ;
|
||||
|
||||
sh:property [
|
||||
sh:path pr:hasBusinessDefinition ; sh:datatype xsd:string ;
|
||||
sh: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:hasFormula ; sh:maxCount 0 ;
|
||||
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: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:hasFormula ; sh:datatype xsd:string ;
|
||||
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:hasUnit ; sh:minCount 1 ; sh:maxCount 1 ;
|
||||
sh:path pr:unit ; sh:minCount 1 ;
|
||||
sh:severity sh:Violation ;
|
||||
sh:message "A Metric states its unit." ] ;
|
||||
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 ;
|
||||
@@ -265,6 +289,39 @@ prs:MetricShape a sh:NodeShape ;
|
||||
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 ;
|
||||
@@ -274,7 +331,7 @@ prs:MotherMetricShape a sh:NodeShape ;
|
||||
sh:select """
|
||||
PREFIX pr: <https://ontology.pernod-ricard.com/metamodel/>
|
||||
SELECT $this WHERE {
|
||||
$this pr:hasStatus "PUBLISHED" .
|
||||
$this pr:status "PUBLISHED" .
|
||||
FILTER NOT EXISTS { $this pr:computedBy ?de }
|
||||
}""" ] .
|
||||
|
||||
@@ -306,7 +363,22 @@ prs:DataObjectShape a sh:NodeShape ;
|
||||
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: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.
|
||||
@@ -340,6 +412,11 @@ prs:DataElementShape a sh:NodeShape ;
|
||||
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 ;
|
||||
@@ -363,7 +440,7 @@ prs:DataElementMeaningShape a sh:NodeShape ;
|
||||
sh:minCount 1 ] ]
|
||||
) ;
|
||||
sh:severity sh:Violation ;
|
||||
sh:message "A Data Element must reach business meaning by exactly one route: represents a Business Concept (dimensional), or be computedBy a Metric (measure). Never both, never neither. A curated column with no business meaning is debt, not an acceptable state." .
|
||||
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." .
|
||||
|
||||
|
||||
#################################################################
|
||||
@@ -381,9 +458,9 @@ prs:DataProductShape a sh:NodeShape ;
|
||||
|
||||
sh:property [
|
||||
sh:path pr:hasProductOwner ; sh:class pr:DataProductOwner ;
|
||||
sh:minCount 1 ; sh:maxCount 1 ;
|
||||
sh:severity sh:Violation ;
|
||||
sh:message "A Data Product has exactly one Product Owner." ] ;
|
||||
sh: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 ;
|
||||
@@ -418,7 +495,7 @@ prs:BIFieldShape a sh:NodeShape ;
|
||||
sh:targetClass pr:BIField ;
|
||||
rdfs:comment "realizesMetric is a transitional control: asserted directly, with no rule written against the lineage chain. Making the assertion conditional on intermediate mappings existing would create confusion and discourage people from filling it in at all." ;
|
||||
sh:property [
|
||||
sh:path pr:realizesMetric ; sh:class pr:Metric ;
|
||||
sh: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." ] .
|
||||
@@ -429,14 +506,14 @@ prs:BIFieldShape a sh:NodeShape ;
|
||||
#################################################################
|
||||
|
||||
prs:NamingShape a sh:NodeShape ;
|
||||
sh:targetClass pr:CuratedObject ;
|
||||
sh:targetClass pr:DefinedObject ;
|
||||
sh:property [
|
||||
sh:path pr:hasName ;
|
||||
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:hasName ;
|
||||
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