Compare commits
4 Commits
v1.5
..
1719ec1934
| Author | SHA1 | Date | |
|---|---|---|---|
| 1719ec1934 | |||
| ea21b7b300 | |||
| fbc63e55d8 | |||
| 1c8ab77003 |
@@ -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,970 @@
|
||||
# Pernod Ricard Data MetaModel — T-Box Rulebook
|
||||
|
||||
**Version 1.2** — Draft for review — generated 2026-08-03
|
||||
|
||||
> Generated from `rules.yaml`. Do not edit this document: edit the source and regenerate (EV-007, EV-008).
|
||||
|
||||
## Scope
|
||||
|
||||
Governs the vocabulary of the model itself: the IRIs, labels and declarative axioms of every class, property and enumeration individual. Does not govern instances, which remain under the A-Box rulebook.
|
||||
|
||||
Written to be read and applied directly, by a person or by a language model, without further context. Every rule states what must hold, why, and what enforces it.
|
||||
|
||||
## Categories
|
||||
|
||||
| Category | Subject | Rules |
|
||||
|---|---|---|
|
||||
| **Identifier** | Form of the IRI | 17 |
|
||||
| **Label** | Derivation and display | 6 |
|
||||
| **Declaration** | Declarative completeness | 5 |
|
||||
| **Evolution** | Change and propagation | 15 |
|
||||
|
||||
## Severity
|
||||
|
||||
- **BLOCKING** — The commit does not pass. Enforced pre-commit and in continuous integration by the named executor.
|
||||
- **MAJOR** — Reviewed and answered for, but not machine-enforceable. Reserved for rules that require human judgement, per EV-011.
|
||||
- **GUIDELINE** — Recommended practice with no gate.
|
||||
|
||||
---
|
||||
|
||||
# Identifier — Form of the IRI
|
||||
|
||||
The IRI is identity. It is immutable in the RDF sense — changing one is a migration, never an edit — so it must be unambiguous, searchable and free of local convention.
|
||||
|
||||
## TN-001 — CamelCase IRI with no separator
|
||||
|
||||
**Statement.** The local name of an IRI is strict CamelCase with no separator of any kind: no hyphen, no underscore, no dot, no space. Initial capital for a class or an enumeration individual; initial lower case for a property, whether object, datatype or annotation. Consecutive capitals are admitted only where they carry an acronym reproduced from a target class short label under TN-011, and nowhere else.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Severity | **BLOCKING** |
|
||||
| Applies to | class, object_property, datatype_property, annotation_property, individual |
|
||||
| Enforced at | script |
|
||||
| Executor | `check_tbox_naming.py` |
|
||||
| Procedure | _pending_ |
|
||||
|
||||
**Why.** A hyphen forces escaping in certain Turtle and SPARQL contexts and makes decamelisation ambiguous: Sub-Domain has no single correct expansion. Typographic convention is a display concern and belongs to the short label, never to identity.
|
||||
|
||||
| From | To | Note |
|
||||
|---|---|---|
|
||||
| pr:data_domain | pr:DataDomain | no underscore |
|
||||
| pr:Sub-Domain | pr:DataSubDomain | no hyphen |
|
||||
| pr:BelongsTo | pr:belongsTo | a property starts lower case |
|
||||
|
||||
## TN-002 — No acronym in the IRI
|
||||
|
||||
**Statement.** The local name of an IRI contains no acronym, initialism or abbreviation. Every short form is expanded in full. One exception, and one only: a relation reproducing the short label of the class it targets carries that short label as it stands, acronym included, under TN-011. No other term may carry an acronym, and no exception list is maintained.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Severity | **BLOCKING** |
|
||||
| Applies to | class, object_property, datatype_property, annotation_property, individual |
|
||||
| Enforced at | script |
|
||||
| Executor | `check_tbox_naming.py` |
|
||||
| Procedure | _pending_ |
|
||||
| Related | A-Box NR-004 |
|
||||
|
||||
**Why.** An acronym in an identifier is a local convention pretending to be a name. It is unsearchable by anyone who does not already know it, and it collides across domains: PO is a Product Owner in one team and a Purchase Order in another. Length is not a counter-argument — an IRI is written by tooling and read in context, and the short form remains available through the short label and the acronym annotation. The single exception exists because a relation name is read on every edge of the graph and in every query, where the spoken form is what makes it legible; reproducing the target's short label verbatim also keeps the relation name derivable from the class rather than invented, which a truncated form would not.
|
||||
|
||||
| From | To | Note |
|
||||
|---|---|---|
|
||||
| pr:KPI | pr:KeyPerformanceIndicator | short form moves to pr:acronym |
|
||||
| pr:BIDataSource | pr:BusinessIntelligenceDataSource | |
|
||||
| pr:definitionUri | pr:definitionAddress | URI is itself an acronym |
|
||||
| a relation targeting a class whose short label is BI Field | pr:containsBIField | the one exception — see TN-011 |
|
||||
|
||||
## TN-003 — Display properties are annotations
|
||||
|
||||
**Statement.** pr:acronym and pr:shortLabel are declared owl:AnnotationProperty, never owl:DatatypeProperty.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Severity | **BLOCKING** |
|
||||
| Applies to | annotation_property |
|
||||
| Enforced at | script |
|
||||
| Executor | `check_tbox_naming.py` |
|
||||
| Procedure | _pending_ |
|
||||
|
||||
**Why.** A datatype property applies to an individual. Annotating a term of the vocabulary with one takes the ontology out of OWL DL into OWL Full, which reasoners reject and strict validators flag. An annotation property applies to anything — class, property or individual. Nothing is lost on the control side: SHACL validates triples and does not read OWL declarations, so every shape targeting these properties keeps working unchanged.
|
||||
|
||||
| From | To | Note |
|
||||
|---|---|---|
|
||||
| pr:shortLabel a owl:DatatypeProperty | pr:shortLabel a owl:AnnotationProperty | |
|
||||
| pr:acronym a owl:DatatypeProperty | pr:acronym a owl:AnnotationProperty | |
|
||||
|
||||
## TN-004 — No digit in the IRI
|
||||
|
||||
**Statement.** The local name of a vocabulary term contains no digit. A numeric parameter — a window, a threshold, a version — is a value, never an identity.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Severity | **BLOCKING** |
|
||||
| Applies to | class, object_property, datatype_property, annotation_property |
|
||||
| Enforced at | script |
|
||||
| Executor | `check_tbox_naming.py` |
|
||||
| Procedure | _pending_ |
|
||||
| Related | A-Box NR-014 (deliberate opposite) |
|
||||
|
||||
**Why.** An instance identifier does encode numbers, and that is its purpose: it carries a position in the domain tree. A vocabulary term encodes nothing but its own meaning. The opposition is deliberate and is stated in the ontology header so that no reader takes one for a violation of the other. A term named after a thirty-day window starts lying the day the window becomes ninety, and nothing detects it.
|
||||
|
||||
| From | To | Note |
|
||||
|---|---|---|
|
||||
| pr:queryCount30d | pr:queryCount | the window belongs to the harvesting specification |
|
||||
|
||||
## TN-005 — A class is a singular common noun
|
||||
|
||||
**Statement.** The local name of a class is a singular noun phrase. No plural, no verb. No type word such as Object, Entity or Item unless it names a genuine abstraction of the model. No suffix rule is imposed on abstract classes.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Severity | **BLOCKING** |
|
||||
| Applies to | class |
|
||||
| Enforced at | script, human |
|
||||
| Executor | `check_tbox_naming.py` |
|
||||
| Procedure | _pending_ |
|
||||
| Related | A-Box NR-008, NR-013 |
|
||||
|
||||
**Why.** Abstractness is not inferable from a name and must not be encoded in one. The correspondence fails in both directions: classes can be abstract without any suffix, and classes ending in Object can be among the most heavily instantiated in the model. Renaming a central business term to satisfy a naming rule would trade meaning for symmetry. Abstractness is declared instead, by TN-007.
|
||||
|
||||
| From | To | Note |
|
||||
|---|---|---|
|
||||
| pr:DataDomains | pr:DataDomain | singular |
|
||||
| pr:ActorObject | pr:Actor | no type word added for its own sake |
|
||||
|
||||
## TN-006 — Every layer root is abstract
|
||||
|
||||
**Statement.** A class that serves as the root of a layer carries pr:isAbstract true.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Severity | **BLOCKING** |
|
||||
| Applies to | class |
|
||||
| Enforced at | script |
|
||||
| Executor | `check_tbox_naming.py` |
|
||||
| Procedure | _pending_ |
|
||||
|
||||
**Why.** A non-abstract layer root allows an instance to be typed as an object of that layer without saying which kind of object it is. That statement carries no information and no downstream control can repair it.
|
||||
|
||||
| From | To | Note |
|
||||
|---|---|---|
|
||||
| pr:PhysicalLayerObject with no declaration | pr:isAbstract true | |
|
||||
|
||||
## TN-007 — Every class declares whether it is abstract
|
||||
|
||||
**Statement.** Every owl:Class carries pr:isAbstract with an explicit boolean value. Absence of the property is a violation, not a default.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Severity | **BLOCKING** |
|
||||
| Applies to | class |
|
||||
| Enforced at | script |
|
||||
| Executor | `check_tbox_naming.py` |
|
||||
| Procedure | _pending_ |
|
||||
|
||||
**Why.** Where declaration is optional, nothing distinguishes a class deliberately left concrete from one where the question was never asked. Two situations make the compulsion necessary. A class with subclasses may be perfectly instantiable, and has been wrongly marked abstract by inference. And a class used as a classification axis may have no declared subclass at all, its members being typed by multiple typing on instances — its abstractness is then structurally unverifiable and only the explicit declaration carries it.
|
||||
|
||||
| From | To | Note |
|
||||
|---|---|---|
|
||||
| a class with no declaration | pr:isAbstract true or false | silence is a violation |
|
||||
| pr:Metric inferred abstract because it has a subclass | pr:isAbstract false | concrete despite having a subclass |
|
||||
|
||||
## TN-008 — Abstractness is declared, never inferred
|
||||
|
||||
**Statement.** Abstractness is read from pr:isAbstract and from nothing else. No consumer of the model — viewer, export, script or documentation generator — may derive it from the presence of subclasses, from the absence of instances, or from any other structural signal.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Severity | **BLOCKING** |
|
||||
| Applies to | class, consumer |
|
||||
| Enforced at | human |
|
||||
| Executor | `consumer contract review` |
|
||||
| Procedure | _pending_ |
|
||||
|
||||
**Why.** A declaration rule holds in the source only if it also binds every consumer. Otherwise each one re-invents its own inference and the model acquires as many answers as it has readers. This is the same failure mode that justified a governed short label rather than a display dictionary per viewer.
|
||||
|
||||
| From | To | Note |
|
||||
|---|---|---|
|
||||
| isAbstract = term.subclasses.length > 0 | isAbstract = term.isAbstract === true | consumer contract |
|
||||
|
||||
## TN-009 — A relation begins with a verb
|
||||
|
||||
**Statement.** The local name of an object property begins with a verb, in lower case. Both the active form and the passive or participial form are admitted: a past participle is a verb in first position. No relation begins with a preposition or with a noun.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Severity | **BLOCKING** |
|
||||
| Applies to | object_property |
|
||||
| Enforced at | script |
|
||||
| Executor | `check_tbox_naming.py` |
|
||||
| Procedure | _pending_ |
|
||||
|
||||
**Why.** A relation reads as a verb and an attribute reads as a noun. The distinction is what lets a reader tell an edge from a field without opening the declaration. The explicit clause on participial forms is required: read literally, a verb-first rule would condemn a whole family of sound relations such as computedBy, storedIn and derivedFrom.
|
||||
|
||||
| From | To | Note |
|
||||
|---|---|---|
|
||||
| pr:inDatabase | pr:isInDatabase | a preposition is not a verb |
|
||||
| pr:primaryLocation | pr:primarilyStoredIn | a noun is not a verb |
|
||||
| pr:computedBy | pr:computedBy | participial form is conforming |
|
||||
|
||||
## TN-010 — The is prefix is either a copula or a predicate
|
||||
|
||||
**Statement.** On an owl:ObjectProperty, is acts as a COPULA — a state verb before a preposition or adjective, whose target is a node of the graph. On a boolean datatype or annotation property, is acts as a PREDICATE — it introduces an adjective or participle and the value is true or false. No other use is admitted.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Severity | **BLOCKING** |
|
||||
| Applies to | object_property, datatype_property, annotation_property |
|
||||
| Enforced at | script |
|
||||
| Executor | `check_tbox_naming.py` |
|
||||
| Procedure | _pending_ |
|
||||
|
||||
**Why.** Conformance of a name beginning with is is never checkable on the name alone. A control asserting that every such term is a boolean produces false positives on every copula; the reverse control lets real problems through. The correct check reads the declared type of the property first, then applies the matching clause.
|
||||
|
||||
| From | To | Note |
|
||||
|---|---|---|
|
||||
| pr:isInSchema | copula | object property |
|
||||
| pr:isNullable | predicate | datatype property of range xsd:boolean |
|
||||
|
||||
## TN-011 — A relation names its target by its short label
|
||||
|
||||
**Statement.** A relation need not mention its target class at all. When it does, it uses the target class's pr:shortLabel AS IT STANDS — not the full label, not a truncation of the short label, and not a form invented for the occasion. Where the short label carries an acronym, the acronym is reproduced with it: this is the single exception admitted by TN-002, and TN-001 admits the consecutive capitals it produces.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Severity | **BLOCKING** |
|
||||
| Applies to | object_property |
|
||||
| Enforced at | script |
|
||||
| Executor | `check_tbox_naming.py` |
|
||||
| Procedure | _pending_ |
|
||||
|
||||
**Why.** Relation names are what is read on the edges of the graph, constantly, in every viewer and every query. Full labels make them unreadable on an arrow. The short label is the form the organisation actually speaks, and reusing it makes a relation name predictable from the class it points at instead of arbitrary. Reproducing it whole is what preserves that property: a truncation would be a form the model invented, unpredictable from the class and unverifiable against it. A corollary: relations do not normally carry a short label of their own, since this rule already keeps their IRIs short.
|
||||
|
||||
| From | To | Note |
|
||||
|---|---|---|
|
||||
| pr:hasDomainOwner | pr:hasDomainOwner | target DataDomainOwner, short label Domain Owner — conforming |
|
||||
| pr:hasDGL | pr:hasGovernanceLead | target short label is Governance Lead |
|
||||
| pr:inBIDataSource | pr:isInBIDataSource | short label BI Data Source reproduced whole, acronym included |
|
||||
| pr:containsField | pr:containsBIField | a truncated short label is not admitted |
|
||||
|
||||
## TN-012 — An attribute is a noun
|
||||
|
||||
**Statement.** The local name of a datatype property is a noun phrase. It begins with no verb, and in particular with no has.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Severity | **BLOCKING** |
|
||||
| Applies to | datatype_property |
|
||||
| Enforced at | script |
|
||||
| Executor | `check_tbox_naming.py` |
|
||||
| Procedure | _pending_ |
|
||||
|
||||
**Why.** A has prefix carries no information — rdfs:domain already says who has the attribute — and it costs the reader the distinction TN-009 exists to make visible. Under TN-018 the derived label would read as a verb phrase on a field, so a relation and an attribute would become indistinguishable in every display.
|
||||
|
||||
| From | To | Note |
|
||||
|---|---|---|
|
||||
| pr:hasFormula | pr:formula | |
|
||||
| pr:hasName | pr:canonicalName | name alone is too generic to stand as an identity |
|
||||
|
||||
## TN-013 — A date attribute is a noun, not a participle
|
||||
|
||||
**Statement.** An attribute holding a date or timestamp is named as a noun.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Severity | **BLOCKING** |
|
||||
| Applies to | datatype_property |
|
||||
| Enforced at | script |
|
||||
| Executor | `check_tbox_naming.py` |
|
||||
| Procedure | _pending_ |
|
||||
|
||||
**Why.** The participial form is idiomatic English but creates a standing family of exceptions to TN-012 that must be defended at every review. The nominal form is consistent with the other attributes of the model and needs no defence.
|
||||
|
||||
| From | To | Note |
|
||||
|---|---|---|
|
||||
| pr:createdOn | pr:creationDate | |
|
||||
| pr:lastQueriedOn | pr:lastQueryDate | |
|
||||
|
||||
## TN-014 — An attribute never takes a relational suffix
|
||||
|
||||
**Statement.** A datatype property never takes a form ending in By, In, From or On. Those forms are reserved for object properties. The only exception is the predicative is of booleans, covered by TN-015.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Severity | **BLOCKING** |
|
||||
| Applies to | datatype_property |
|
||||
| Enforced at | script |
|
||||
| Executor | `check_tbox_naming.py` |
|
||||
| Procedure | _pending_ |
|
||||
|
||||
**Why.** An attribute named like a relation will be read as one. A reader encountering a term in the shape of computedBy or ownedBy expects a node at the other end, not a string. The tell is usually visible in the label: a parenthetical gloss added to explain what the value actually holds is evidence that the name itself is wrong.
|
||||
|
||||
| From | To | Note |
|
||||
|---|---|---|
|
||||
| pr:definedBy holding a string | pr:definitionAddress | address, not location — location is taken by the storage side |
|
||||
|
||||
## TN-015 — A boolean attribute begins with is
|
||||
|
||||
**Statement.** A property of range xsd:boolean begins with is, followed by an adjective or participle. This is the single explicit exception to TN-012.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Severity | **BLOCKING** |
|
||||
| Applies to | datatype_property, annotation_property |
|
||||
| Enforced at | script |
|
||||
| Executor | `check_tbox_naming.py` |
|
||||
| Procedure | _pending_ |
|
||||
|
||||
**Why.** A bare adjective does not read as a statement, and some of them collide with reserved words in the languages that consume the model.
|
||||
|
||||
| From | To | Note |
|
||||
|---|---|---|
|
||||
| pr:nullable | pr:isNullable | |
|
||||
| pr:abstract | pr:isAbstract | abstract is a reserved word in several target languages |
|
||||
|
||||
## TN-016 — A controlled value is an individual or a literal
|
||||
|
||||
**Statement.** A value becomes a TYPED INDIVIDUAL if it needs to be defined, owned, or extended by a domain without modifying the model. It stays a LITERAL if it is a closed technical state whose list is the property of governance and must precisely not be extended. The model chooses one form per kind of value and states the choice.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Severity | **BLOCKING** |
|
||||
| Applies to | class, individual, datatype_property |
|
||||
| Enforced at | shacl, human |
|
||||
| Executor | `pr_metamodel_shapes.ttl` |
|
||||
| Procedure | _pending_ |
|
||||
| Related | A-Box NR-016 |
|
||||
|
||||
**Why.** A literal cannot carry a definition, an owner or a link, and extending its list means editing the shapes and therefore the frozen vocabulary. A typed individual can be added by a domain without touching the model, appears as a node in every viewer, and can be reached by following an edge. The test is not how the value looks but who is allowed to add one.
|
||||
|
||||
| From | To | Note |
|
||||
|---|---|---|
|
||||
| an activation level defined in a playbook, not in the graph | literal constrained by sh:in | not domain-extensible |
|
||||
| a kind of system a domain may need to add | typed individual | extensible and worth defining |
|
||||
|
||||
## TN-017 — Controlled literals are UPPER_SNAKE_CASE
|
||||
|
||||
**Statement.** Controlled literal values are written UPPER_SNAKE_CASE. This is the only place in the model where that casing is used.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Severity | **BLOCKING** |
|
||||
| Applies to | datatype_property |
|
||||
| Enforced at | shacl |
|
||||
| Executor | `pr_metamodel_shapes.ttl` |
|
||||
| Procedure | _pending_ |
|
||||
|
||||
**Why.** The casing exists so that a value drawn from a closed list is distinguishable from free text at a glance, in the source and in any export.
|
||||
|
||||
| From | To | Note |
|
||||
|---|---|---|
|
||||
| "Full activation" | "FULL_ACTIVATION" | |
|
||||
| "Published" | "PUBLISHED" | |
|
||||
|
||||
---
|
||||
|
||||
# Label — Derivation and display
|
||||
|
||||
The label is derived, not authored. Anything a reader needs that the derivation cannot produce lives in a dedicated channel: the short label for the spoken form, the comment for meaning.
|
||||
|
||||
## TN-018 — The label is derived from the IRI
|
||||
|
||||
**Statement.** rdfs:label is obtained from the local name of the IRI by inserting a space at every case boundary. No word added, removed, reordered or substituted. The words of a class label keep their initial capital; the words of a property label are entirely lower case.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Severity | **BLOCKING** |
|
||||
| Applies to | class, object_property, datatype_property, annotation_property, individual |
|
||||
| Enforced at | script |
|
||||
| Executor | `check_tbox_naming.py` |
|
||||
| Procedure | _pending_ |
|
||||
| Related | A-Box NR-005 (deliberate opposite) |
|
||||
|
||||
**Why.** A label carrying meaning the IRI does not is the symptom of a badly named IRI or of an incomplete comment — never of a legitimate exception to the label. Where a label has been quietly enriched to explain a term, the explanation belongs in rdfs:comment and the short form, if the organisation speaks one, in pr:shortLabel.
|
||||
|
||||
| From | To | Note |
|
||||
|---|---|---|
|
||||
| pr:BusinessIntelligenceDataSource | Business Intelligence Data Source | a class label keeps its capitals |
|
||||
| pr:primarilyStoredIn | primarily stored in | a property label is lower case |
|
||||
| "Ownership and Categorization Layer Object" | "Ownership Layer Object" | the added meaning moves to rdfs:comment |
|
||||
|
||||
## TN-019 — No acronym in the label
|
||||
|
||||
**Statement.** The label introduces no acronym or abbreviation that the IRI does not already carry, including in a recapitalised form that the derivation would not produce. Where the IRI legitimately carries an acronym under TN-011, the derived label carries it too and nothing further is added.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Severity | **BLOCKING** |
|
||||
| Applies to | class, object_property, datatype_property, annotation_property, individual |
|
||||
| Enforced at | script |
|
||||
| Executor | `check_tbox_naming.py` |
|
||||
| Procedure | _pending_ |
|
||||
|
||||
**Why.** A direct corollary of TN-018, kept as a separate rule because it catches labels whose IRI is innocent: a term correctly named in full can still carry a hand-written label that reintroduces the short form. Stated as an introduction rather than a prohibition, it stays a pure corollary — whatever the IRI legitimately holds, the derivation reproduces, and nothing else.
|
||||
|
||||
| From | To | Note |
|
||||
|---|---|---|
|
||||
| "BI expression" | "expression" | the IRI was already conforming |
|
||||
|
||||
## TN-020 — No state in the label
|
||||
|
||||
**Statement.** The label mentions no status, version or deprecation. State lives in an axiom — owl:deprecated, pr:status — never in text. Deprecated and active terms are never mixed in a single view: a consumer filters on the axiom at query time and presents deprecated terms separately.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Severity | **BLOCKING** |
|
||||
| Applies to | class, object_property, datatype_property, annotation_property, individual, consumer |
|
||||
| Enforced at | script |
|
||||
| Executor | `check_tbox_naming.py` |
|
||||
| Procedure | _pending_ |
|
||||
|
||||
**Why.** A term whose label repeats its state carries that state twice. The day one is changed without the other, the two contradict each other and nothing detects it. With the rule in force, the question of whether a term is deprecated has exactly one possible answer.
|
||||
|
||||
| From | To | Note |
|
||||
|---|---|---|
|
||||
| "belongs to domain (deprecated)" | "belongs to domain" | owl:deprecated carries the state |
|
||||
|
||||
## TN-021 — No gloss or parenthesis in the label
|
||||
|
||||
**Statement.** No parenthesis, qualification or example appears in the label. They belong in rdfs:comment.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Severity | **BLOCKING** |
|
||||
| Applies to | class, object_property, datatype_property, annotation_property |
|
||||
| Enforced at | script |
|
||||
| Executor | `check_tbox_naming.py` |
|
||||
| Procedure | _pending_ |
|
||||
|
||||
**Why.** A parenthetical gloss is almost always the symptom of an IRI that does not say what it holds. The right response is usually to rename the term, not to annotate the label.
|
||||
|
||||
| From | To | Note |
|
||||
|---|---|---|
|
||||
| "defined by (code URI)" | "definition address" | rename the term rather than gloss the label |
|
||||
| "query count (30 days)" | "query count" | |
|
||||
|
||||
## TN-022 — The short label carries the spoken form
|
||||
|
||||
**Statement.** A term carries pr:shortLabel ONLY where the form derived under TN-018 is not what the organisation writes or says. Where the two coincide, no short label is declared: a duplicate of the label carries no information and is one more thing to keep in step. The short label follows the casing of the label it replaces — title case for a class — and admits what the label cannot: acronyms, abbreviations, dropped words.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Severity | **MAJOR** |
|
||||
| Applies to | class, individual |
|
||||
| Enforced at | human |
|
||||
| Executor | `review checklist` |
|
||||
| Procedure | _pending_ |
|
||||
|
||||
**Why.** The short label is what makes the derivation rule bearable. Without it, a mechanical label impoverishes every screen; with it, display becomes explicit and centrally governed instead of being improvised by each consumer. Restricting it to the cases where it actually differs is what keeps it meaningful: a short label present on every term degenerates into a second copy of the label, drifting from it one entry at a time. It is MAJOR rather than BLOCKING because judging whether a derived form matches what people actually say is human work, and EV-011 forbids declaring a rule blocking with no executor.
|
||||
|
||||
| From | To | Note |
|
||||
|---|---|---|
|
||||
| "Data Sub Domain Owner" | short label Sub Domain Owner | the word Data is dropped in the spoken form |
|
||||
| "Key Performance Indicator" | short label KPI, acronym KPI | the acronym channel is what makes TN-002 bearable |
|
||||
| a short label reading Metric on a class labelled Metric | no short label | a duplicate of the label carries nothing |
|
||||
|
||||
## TN-023 — The label is generated, never typed
|
||||
|
||||
**Statement.** rdfs:label is never written by hand. It is produced by the derivation function from the IRI and stored in the source. A hand-typed label is a violation even when its value happens to be correct.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Severity | **BLOCKING** |
|
||||
| Applies to | class, object_property, datatype_property, annotation_property, individual |
|
||||
| Enforced at | script |
|
||||
| Executor | `check_tbox_naming.py` |
|
||||
| Procedure | _pending_ |
|
||||
|
||||
**Why.** Generate and store, rather than store only or compute at read time. The label stays in the source so SPARQL and third-party tooling behave normally, while a script regenerates it at every commit and the gate fails if a stored label differs from the derived one. Redundancy becomes harmless because it is checked: rather than hoping two facts stay consistent, the model makes inconsistency detectable.
|
||||
|
||||
| From | To | Note |
|
||||
|---|---|---|
|
||||
| label absent | violation | |
|
||||
| label differing from the derivation | violation — drift | |
|
||||
|
||||
---
|
||||
|
||||
# Declaration — Declarative completeness
|
||||
|
||||
Nothing is left to be guessed. What a term is, what it applies to, where it sits and where it came from are all stated, never inferred from structure or from silence.
|
||||
|
||||
## TN-024 — Every active term carries a comment
|
||||
|
||||
**Statement.** rdfs:comment is mandatory on every active term. On a property the comment is structured: the definition, plus the scope constraint or the confusion to be avoided. On a class the form is free.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Severity | **BLOCKING** |
|
||||
| Applies to | class, object_property, datatype_property, annotation_property |
|
||||
| Enforced at | script, human |
|
||||
| Executor | `check_tbox_naming.py` |
|
||||
| Procedure | _pending_ |
|
||||
|
||||
**Why.** A comment that paraphrases the name prevents nothing. A good one prevents a specific error: it says where the property may and may not be declared, or which neighbouring term it must not be confused with. The structure is imposed on properties because they are where ambiguity costs most — they are asserted thousands of times by people who will not read the model. The script checks presence; only a reviewer checks value, which is why the rule names two tiers.
|
||||
|
||||
| From | To | Note |
|
||||
|---|---|---|
|
||||
| "the owner" | definition plus where it may be declared and what it excludes | |
|
||||
|
||||
## TN-025 — Every property declares its domain and range
|
||||
|
||||
**Statement.** rdfs:domain and rdfs:range are mandatory on every property, EXCEPT where the property is deliberately polymorphic. A polymorphic property states so in its comment and has its scope declared in SHACL. Silent absence is a violation; documented absence is not.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Severity | **BLOCKING** |
|
||||
| Applies to | object_property, datatype_property |
|
||||
| Enforced at | script, human |
|
||||
| Executor | `check_tbox_naming.py` |
|
||||
| Procedure | _pending_ |
|
||||
|
||||
**Why.** A property with no domain cannot be targeted by any control. But some properties are polymorphic by design, their scope controlled class by class in SHACL rather than by twin properties; giving those an rdfs:domain would trigger the RDFS retyping described in TN-028. The rule therefore separates the two cases rather than demanding a domain everywhere.
|
||||
|
||||
| From | To | Note |
|
||||
|---|---|---|
|
||||
| a polymorphic property with no domain and no comment | violation | silence is indistinguishable from omission |
|
||||
| a polymorphic property with no domain | conforming | scope declared in SHACL |
|
||||
|
||||
## TN-026 — Every term declares how it was authored
|
||||
|
||||
**Statement.** pr:authoringMode is mandatory on every term. pr:harvestSource is mandatory if and only if the mode is HARVESTED.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Severity | **BLOCKING** |
|
||||
| Applies to | class, object_property, datatype_property, annotation_property |
|
||||
| Enforced at | script |
|
||||
| Executor | `check_tbox_naming.py` |
|
||||
| Procedure | _pending_ |
|
||||
|
||||
**Why.** Provenance decides who may edit a term and what a divergence means. A term stating where its data comes from without stating that it is harvested, or declaring itself harvested without naming a source, is half-declared in a way no control can catch. Declared symmetrically, provenance also makes a harvester specifiable from the model itself rather than from a side document.
|
||||
|
||||
| From | To | Note |
|
||||
|---|---|---|
|
||||
| harvestSource present | authoringMode HARVESTED | |
|
||||
| authoringMode HARVESTED, harvestSource absent | harvestSource declared | |
|
||||
|
||||
## TN-027 — A concrete class has one layer and one provenance
|
||||
|
||||
**Statement.** Every concrete class descends from exactly one layer root and from exactly one provenance axis. Controlled-vocabulary classes are outside the layers by nature — a named and closed exception.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Severity | **BLOCKING** |
|
||||
| Applies to | class |
|
||||
| Enforced at | script |
|
||||
| Executor | `check_tbox_naming.py` |
|
||||
| Procedure | _pending_ |
|
||||
|
||||
**Why.** A class with no layer is invisible to every layer-scoped control and to every view organised by layer; a class with two is ambiguous in both. Where a family of classes genuinely sits outside the data layers — actors, governance objects — the answer is an explicit root of its own, not silence. Adding such a root by INSERTION above an existing abstraction, rather than by renaming it, keeps the ranges that point at that abstraction readable: a range must state the nature of its target, not its position in the model.
|
||||
|
||||
| From | To | Note |
|
||||
|---|---|---|
|
||||
| a class with no parent | attached to its layer root | |
|
||||
| renaming an abstraction into a layer root | inserting the layer root above it | preserves the meaning of every range that points at it |
|
||||
|
||||
## TN-028 — A sub-property inherits the domain of its parent
|
||||
|
||||
**Statement.** Declaring rdfs:subPropertyOf does not shield a property from the rdfs:domain of its parent. Every assertion of a sub-property is also an assertion of the parent, so the parent's domain applies and its RDFS retyping takes effect.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Severity | **BLOCKING** |
|
||||
| Applies to | object_property, datatype_property |
|
||||
| Enforced at | script, human |
|
||||
| Executor | `check_tbox_naming.py` |
|
||||
| Procedure | _pending_ |
|
||||
|
||||
**Why.** In RDFS a domain is not a rejecting constraint but a retyping inference. Attaching a sub-property to a parent whose domain does not fit will silently retype its subjects — an object of one layer quietly becomes an object of another, entering counts, controls and traversals it was never part of. Where two relations share a verb but not a domain, they must not be merged and must not be linked by subPropertyOf; the unification, if one is needed, belongs on a shared upper property with no domain of its own.
|
||||
|
||||
| From | To | Note |
|
||||
|---|---|---|
|
||||
| a dashboard field asserted through a property whose domain is a warehouse column | the dashboard field is inferred to BE a warehouse column | silent retyping |
|
||||
| linking the two by rdfs:subPropertyOf | no protection — the parent's domain still applies | |
|
||||
| a shared upper property with no domain | end-to-end traversal without merging IRIs | the correct unification |
|
||||
|
||||
---
|
||||
|
||||
# Evolution — Change and propagation
|
||||
|
||||
Every rule here is anchored on a real failure. They govern how the model changes without breaking what depends on it.
|
||||
|
||||
## EV-001 — A rename is never an edit in place
|
||||
|
||||
**Statement.** A rename creates a new term and keeps the old one with owl:deprecated true and dcterms:isReplacedBy. No IRI is modified in place. The deprecated stub carries no edge: no subClassOf, no label, no comment — only its type and the two axioms.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Severity | **BLOCKING** |
|
||||
| Applies to | procedure |
|
||||
| Enforced at | human |
|
||||
| Executor | `commit review` |
|
||||
| Procedure | _pending_ |
|
||||
| Related | A-Box LC-002 |
|
||||
|
||||
**Why.** Nothing propagates in RDF. An IRI is a string copied into every triple, so renaming means rewriting every triple where it appears as subject, predicate or object — declaration, subclass axioms, domain, range, instance typing, shape targets and paths, queries, viewers, documentation. Edges are moved, not duplicated. The stub exists so that a consumer holding an older export can still be told what replaced the term.
|
||||
|
||||
| From | To | Note |
|
||||
|---|---|---|
|
||||
| editing an IRI in place | new term plus deprecated stub | |
|
||||
|
||||
## EV-002 — A withdrawal requires proof of non-instantiation
|
||||
|
||||
**Statement.** A term is removed only after a count proving it was never instantiated, the count being attached to the commit. Without proof, deprecation only.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Severity | **BLOCKING** |
|
||||
| Applies to | procedure |
|
||||
| Enforced at | script |
|
||||
| Executor | `count query attached to the commit` |
|
||||
| Procedure | _pending_ |
|
||||
|
||||
**Why.** Removal is the one irreversible operation on a vocabulary. The proof is cheap — a single counting query — and it is what separates a safe withdrawal from a silent data loss.
|
||||
|
||||
| From | To | Note |
|
||||
|---|---|---|
|
||||
| deleting a term | SELECT (COUNT(?s) AS ?n) WHERE { ?s a pr:Term } | must return zero |
|
||||
|
||||
## EV-003 — A deletion removes every reference to the subject
|
||||
|
||||
**Statement.** A subject removed while its IRI is still cited elsewhere is recreated by RDFS inference. Every reference is removed in the same operation.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Severity | **BLOCKING** |
|
||||
| Applies to | procedure |
|
||||
| Enforced at | script |
|
||||
| Executor | `dangling-reference check in the migration script` |
|
||||
| Procedure | _pending_ |
|
||||
|
||||
**Why.** A graph has no foreign keys. Deleting the block that declares a subject leaves every triple that names it intact, and a reasoner will reconstruct a hollow node from them — present in traversals, absent from every control.
|
||||
|
||||
| From | To | Note |
|
||||
|---|---|---|
|
||||
| deleting a subject block | deleting the block and every triple naming it | |
|
||||
|
||||
## EV-004 — A change is validated before it is applied
|
||||
|
||||
**Statement.** Every modification of the model is agreed before any artifact is written. Dry run first, apply second.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Severity | **BLOCKING** |
|
||||
| Applies to | procedure |
|
||||
| Enforced at | human |
|
||||
| Executor | `commit review` |
|
||||
| Procedure | _pending_ |
|
||||
|
||||
**Why.** The damage from a bad change is rarely in the change itself but in everything that silently depended on the old state. A dry run that reports what it would touch is what makes that dependency visible while it can still be discussed.
|
||||
|
||||
| From | To | Note |
|
||||
|---|---|---|
|
||||
| migrate.py --apply as the first run | migrate.py, then --apply | dry run is the default |
|
||||
|
||||
## EV-005 — Graph edits go through an RDF parser
|
||||
|
||||
**Statement.** Any transformation of the graph is performed with an RDF parser. No regular expression applied line by line to Turtle.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Severity | **BLOCKING** |
|
||||
| Applies to | procedure, script |
|
||||
| Enforced at | human |
|
||||
| Executor | `script review` |
|
||||
| Procedure | _pending_ |
|
||||
|
||||
**Why.** Turtle is a structured syntax and a line is not a unit of meaning. A literal may contain the very characters a regex splits on, so a text-level edit that looks correct on every example can truncate a value on the one that matters. A parser knows the difference between a delimiter and a character inside a string; a regex does not, and every workaround for that is a partial reimplementation of the parser.
|
||||
|
||||
| From | To | Note |
|
||||
|---|---|---|
|
||||
| re.sub on the file text | remove and add on parsed triples | |
|
||||
| hand-written clause splitting | parser | a workaround is evidence the wrong tool is in use |
|
||||
|
||||
## EV-006 — Every script is idempotent
|
||||
|
||||
**Statement.** A script may be replayed without changing the result. Its guard tests the TARGET state, not the source state, and it counts its own result. Acceptance test: two consecutive runs, the second reporting zero modifications.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Severity | **BLOCKING** |
|
||||
| Applies to | script |
|
||||
| Enforced at | script |
|
||||
| Executor | `double run in continuous integration` |
|
||||
| Procedure | _pending_ |
|
||||
|
||||
**Why.** A chain of transformations is only safe if any step can be replayed. A guard that tests whether work remains to be done breaks as soon as another script has already changed the source; a guard that tests whether the target state exists survives it. The self-count is what turns the property into a verifiable one.
|
||||
|
||||
| From | To | Note |
|
||||
|---|---|---|
|
||||
| appending unconditionally | appending only if the target is absent | guard on the target state |
|
||||
|
||||
## EV-007 — The replayable source is the start of the chain
|
||||
|
||||
**Statement.** The replayable source is the starting file plus the ordered sequence of scripts, never an intermediate committed state. Hand-editing a file produced by a script is forbidden, including for a typo: the script is corrected and the chain replayed.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Severity | **BLOCKING** |
|
||||
| Applies to | procedure |
|
||||
| Enforced at | human |
|
||||
| Executor | `commit review` |
|
||||
| Procedure | _pending_ |
|
||||
|
||||
**Why.** A committed intermediate file is a convenience, not a source. As soon as one hand edit exists in it that no script performs, replaying the chain no longer reproduces the committed state and nobody can say which of the two is right. Keeping the chain authoritative is also what makes a failed migration recoverable: restart from the last true source with a corrected script, rather than repair a half-migrated file by hand.
|
||||
|
||||
| From | To | Note |
|
||||
|---|---|---|
|
||||
| fixing a typo in a generated file | fixing the script and replaying | |
|
||||
|
||||
## EV-008 — Every artifact has a producer script
|
||||
|
||||
**Statement.** No artifact contains hard-coded data. Every deliverable — viewer, documentation, workbook, diagram — is regenerated from a versioned source by a script.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Severity | **BLOCKING** |
|
||||
| Applies to | script, artifact |
|
||||
| Enforced at | human |
|
||||
| Executor | `artifact review` |
|
||||
| Procedure | _pending_ |
|
||||
|
||||
**Why.** An artifact that cannot be regenerated becomes stale the moment the model moves, and there is no way to tell whether it is stale or merely different. This rulebook applies the rule to itself: the source is one file, the document and the workbook are generated from it.
|
||||
|
||||
| From | To | Note |
|
||||
|---|---|---|
|
||||
| a hand-maintained workbook | a source file plus a generator | |
|
||||
|
||||
## EV-009 — A change propagates in a single merge
|
||||
|
||||
**Statement.** A change is propagated to the vocabulary, the instances, the shapes, the consumers and the documentation in one branch, merged as a block. The main line never sees a state where one has moved and another has not.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Severity | **BLOCKING** |
|
||||
| Applies to | procedure |
|
||||
| Enforced at | human |
|
||||
| Executor | `merge review` |
|
||||
| Procedure | _pending_ |
|
||||
|
||||
**Why.** Partial propagation is not a smaller version of a change, it is a different and invalid state. Its worst form is silent: a gate that validates the stale half and reports success. A single merge makes the intermediate state unreachable rather than merely discouraged.
|
||||
|
||||
| From | To | Note |
|
||||
|---|---|---|
|
||||
| committing the vocabulary and updating the shapes later | one branch | |
|
||||
|
||||
## EV-010 — Shapes declare the ontology version they target
|
||||
|
||||
**Statement.** The shapes graph declares dcterms:conformsTo equal to the owl:versionIRI of the ontology. Any divergence aborts validation immediately.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Severity | **BLOCKING** |
|
||||
| Applies to | shape, script |
|
||||
| Enforced at | script |
|
||||
| Executor | `run_shacl_validation.py` |
|
||||
| Procedure | _pending_ |
|
||||
|
||||
**Why.** Validating against the wrong shapes does not fail loudly. It produces a long list of violations that reads exactly like a regression of the model, and the time goes into interpreting them rather than noticing the mismatch. One declared triple and one check turn that into a single line naming the real problem.
|
||||
|
||||
| From | To | Note |
|
||||
|---|---|---|
|
||||
| hundreds of violations to interpret | ABORT — shapes target a different ontology version | |
|
||||
|
||||
## EV-011 — No blocking rule without an executor
|
||||
|
||||
**Statement.** A rule may be declared BLOCKING only if a named script or shape enforces it. A blocking rule with no control is a statement of intent.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Severity | **BLOCKING** |
|
||||
| Applies to | rule |
|
||||
| Enforced at | script |
|
||||
| Executor | `rulebook source self-check` |
|
||||
| Procedure | _pending_ |
|
||||
|
||||
**Why.** An unenforced blocking rule is worse than an absent one: it is cited as though it held, and the gap only surfaces when something it should have caught reaches production. Making the executor a mandatory field turns the omission into something a script can detect on the rulebook itself.
|
||||
|
||||
| From | To | Note |
|
||||
|---|---|---|
|
||||
| severity BLOCKING with no executor | violation | detected by the generators before they write anything |
|
||||
|
||||
## EV-012 — Every rule names the tier that enforces it
|
||||
|
||||
**Statement.** Controls exist at three tiers — SHACL for the shape of the graph, scripts for the file, the lexicon and naming, human review for semantics. Every rule names its tier. A rule with no assigned tier is a violation.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Severity | **BLOCKING** |
|
||||
| Applies to | rule |
|
||||
| Enforced at | script |
|
||||
| Executor | `rulebook source self-check` |
|
||||
| Procedure | _pending_ |
|
||||
|
||||
**Why.** SHACL validates a graph, not a file, and not meaning. Two IRIs standing for the same notion produce two individually valid graphs; a duplicated block of text produces identical triples and no complaint. Naming the tier forces the question of what actually catches each rule, and pushes the ones SHACL cannot see to a control that can: a normalised-label index reviewed by a person, a file-level hash, a naming script, a review checklist.
|
||||
|
||||
| From | To | Note |
|
||||
|---|---|---|
|
||||
| two IRIs for one notion | normalised label index plus human review | SHACL cannot see it |
|
||||
| a duplicated text block | file-level block hashing | identical triples |
|
||||
|
||||
## EV-013 — No sh:pattern outside the SHACL specification
|
||||
|
||||
**Statement.** sh:pattern uses XPath 2.0 regular expressions, which support neither lookbehind nor lookahead. A constraint is expressed positively — what the string must be — rather than negatively. Where that is impossible the rule leaves SHACL and moves to the script tier under EV-012.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Severity | **BLOCKING** |
|
||||
| Applies to | shape |
|
||||
| Enforced at | script |
|
||||
| Executor | `run_shacl_validation.py` |
|
||||
| Procedure | _pending_ |
|
||||
| Related | A-Box NR-001 |
|
||||
|
||||
**Why.** A pattern written in a richer regex dialect does not degrade gracefully: the validator fails to compile it, and the rule that depended on it silently stops being enforced while still being declared blocking.
|
||||
|
||||
| From | To | Note |
|
||||
|---|---|---|
|
||||
| a pattern using (?<! ... ) | a pattern describing the whole accepted string | express positively |
|
||||
|
||||
## EV-014 — The term namespace is never versioned
|
||||
|
||||
**Statement.** The namespace of terms is not versioned. The version lives in owl:versionIRI alone.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Severity | **BLOCKING** |
|
||||
| Applies to | ontology |
|
||||
| Enforced at | script |
|
||||
| Executor | `check_tbox_naming.py` |
|
||||
| Procedure | _pending_ |
|
||||
|
||||
**Why.** A versioned term namespace changes every IRI at every release, breaking every consumer for no benefit. It also fails silently in the other direction: a file bound to an outdated namespace targets IRIs that no longer exist, so every shape matches nothing and validation passes while testing nothing at all.
|
||||
|
||||
| From | To | Note |
|
||||
|---|---|---|
|
||||
| a prefix bound to a versioned namespace | a prefix bound to the stable namespace | the version lives in owl:versionIRI |
|
||||
|
||||
## EV-015 — Every procedure that writes produces an execution log
|
||||
|
||||
**Statement.** Any procedure that modifies the graph writes a versioned execution log, committed alongside the change. The log records the attempt number and timestamp, a checksum of every input consumed, the ontology version and the version the shapes declare they target, a count per step, the full validation report, and a difference against the previous attempt.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Severity | **BLOCKING** |
|
||||
| Applies to | procedure, script |
|
||||
| Enforced at | script |
|
||||
| Executor | `the procedure's own runner` |
|
||||
| Procedure | _pending_ |
|
||||
|
||||
**Why.** A counter printed to the terminal disappears with the terminal. When a validation run fails, the question is never only what failed but whether it is the same failure as the previous attempt: a violation that persists unchanged after a correction says the cause lies elsewhere, while one that moves every time says the change is being patched rather than fixed. Those are opposite diagnoses and neither is available without a record of the previous run. The input checksums serve the same purpose one level down, separating a wrong model from a stale file — the ambiguity that made a past incident expensive to explain. The log is also the natural carrier of the proof of non-instantiation required by EV-002, which otherwise has no defined format.
|
||||
|
||||
| From | To | Note |
|
||||
|---|---|---|
|
||||
| a counter printed to standard output | a versioned log committed with the change | |
|
||||
| a failed run with no record of the previous one | a difference against the previous attempt | what makes correct-or-abandon decidable |
|
||||
|
||||
---
|
||||
|
||||
# Annex A — Abstractness
|
||||
|
||||
Applies TN-006 and TN-007 to the model. 44 classes, of which **13 abstract** and **31 concrete**. Maintained with the model, not after it.
|
||||
|
||||
| Class | isAbstract | Note |
|
||||
|---|---|---|
|
||||
| `MetaModelObject` | **true** | root of the model |
|
||||
| `DefinedObject` | **true** | provenance axis |
|
||||
| `CapturedObject` | **true** | provenance axis |
|
||||
| `OwnershipLayerObject` | **true** | layer root |
|
||||
| `BusinessLayerObject` | **true** | layer root |
|
||||
| `LogicalLayerObject` | **true** | layer root |
|
||||
| `PhysicalLayerObject` | **true** | NEW — omission in v1.6 |
|
||||
| `DeliveryLayerObject` | **true** | layer root |
|
||||
| `ConsumptionLayerObject` | **true** | layer root |
|
||||
| `GovernanceLayerObject` | **true** | NEW — seventh layer root |
|
||||
| `DataStructure` | **true** | harvesting types the concrete sort |
|
||||
| `KeyConstraint` | **true** | only PrimaryKey and ForeignKey instantiate |
|
||||
| `Actor` | **true** | renaming would degrade the range of ownedBy and hasPublisher |
|
||||
| `DataDomain` | false | |
|
||||
| `DataSubDomain` | false | |
|
||||
| `BusinessObject` | false | ends in Object and is heavily instantiated - refutes any suffix rule |
|
||||
| `BusinessConcept` | false | |
|
||||
| `Metric` | false | concrete despite having KeyPerformanceIndicator beneath it |
|
||||
| `KeyPerformanceIndicator` | false | |
|
||||
| `DataObject` | false | |
|
||||
| `DataElement` | false | |
|
||||
| `Database` | false | |
|
||||
| `Schema` | false | |
|
||||
| `Field` | false | |
|
||||
| `System` | false | |
|
||||
| `Transformation` | false | |
|
||||
| `BaseTable` | false | |
|
||||
| `ExternalTable` | false | |
|
||||
| `View` | false | |
|
||||
| `PrimaryKey` | false | |
|
||||
| `ForeignKey` | false | |
|
||||
| `DataProduct` | false | |
|
||||
| `DataContract` | false | |
|
||||
| `DataInterface` | false | |
|
||||
| `BusinessIntelligenceWorkspace` | false | |
|
||||
| `BusinessIntelligenceDataSource` | false | |
|
||||
| `BusinessIntelligenceField` | false | |
|
||||
| `BusinessIntelligenceReport` | false | |
|
||||
| `DataDomainOwner` | false | |
|
||||
| `DataSubDomainOwner` | false | |
|
||||
| `DataProductOwner` | false | |
|
||||
| `DataSteward` | false | |
|
||||
| `DataGovernanceLead` | false | |
|
||||
| `SystemType` | false | must stay concrete — it types five individuals |
|
||||
|
||||
---
|
||||
|
||||
# Annex B — Display
|
||||
|
||||
Applies TN-011 and TN-022. The short label is what screens display and what relation names reuse; the acronym is a search key, never an identity.
|
||||
|
||||
| IRI | rdfs:label | shortLabel | acronym | Note |
|
||||
|---|---|---|---|---|
|
||||
| `MetaModelObject` | Meta Model Object | - | - | |
|
||||
| `DefinedObject` | Defined Object | - | - | |
|
||||
| `CapturedObject` | Captured Object | - | - | |
|
||||
| `OwnershipLayerObject` | Ownership Layer Object | Ownership Layer | - | the categorisation role moves into rdfs:comment |
|
||||
| `BusinessLayerObject` | Business Layer Object | Business Layer | - | |
|
||||
| `LogicalLayerObject` | Logical Layer Object | Logical Layer | - | |
|
||||
| `PhysicalLayerObject` | Physical Layer Object | Physical Layer | - | |
|
||||
| `DeliveryLayerObject` | Delivery Layer Object | Delivery Layer | - | |
|
||||
| `ConsumptionLayerObject` | Consumption Layer Object | Consumption Layer | - | |
|
||||
| `GovernanceLayerObject` | Governance Layer Object | Governance Layer | - | |
|
||||
| `DataStructure` | Data Structure | - | - | the former short label was a gloss and belongs in rdfs:comment |
|
||||
| `KeyConstraint` | Key Constraint | - | - | |
|
||||
| `Actor` | Actor | - | - | |
|
||||
| `DataDomain` | Data Domain | Domain | DD | DD is already the short form used inside instance identifiers |
|
||||
| `DataSubDomain` | Data Sub Domain | Sub Domain | SD | no hyphen, for consistency |
|
||||
| `BusinessObject` | Business Object | - | BO | |
|
||||
| `BusinessConcept` | Business Concept | Concept | - | drives the name of usesConcept |
|
||||
| `Metric` | Metric | - | - | |
|
||||
| `KeyPerformanceIndicator` | Key Performance Indicator | KPI | KPI | the acronym channel is what makes TN-002 bearable |
|
||||
| `DataObject` | Data Object | - | - | |
|
||||
| `DataElement` | Data Element | Element | - | drives the name of hasElement and hasGrainElement |
|
||||
| `Database` | Database | - | - | |
|
||||
| `Schema` | Schema | - | - | |
|
||||
| `Field` | Field | - | - | |
|
||||
| `System` | System | - | - | |
|
||||
| `SystemType` | System Type | - | - | |
|
||||
| `Transformation` | Transformation | - | - | |
|
||||
| `BaseTable` | Base Table | - | - | |
|
||||
| `ExternalTable` | External Table | - | - | |
|
||||
| `View` | View | - | - | |
|
||||
| `PrimaryKey` | Primary Key | - | - | |
|
||||
| `ForeignKey` | Foreign Key | - | - | |
|
||||
| `DataProduct` | Data Product | - | - | |
|
||||
| `DataContract` | Data Contract | - | - | |
|
||||
| `DataInterface` | Data Interface | - | - | |
|
||||
| `BusinessIntelligenceWorkspace` | Business Intelligence Workspace | BI Workspace | - | |
|
||||
| `BusinessIntelligenceDataSource` | Business Intelligence Data Source | BI Data Source | - | |
|
||||
| `BusinessIntelligenceField` | Business Intelligence Field | BI Field | - | reproduced whole in containsBIField under TN-011 |
|
||||
| `BusinessIntelligenceReport` | Business Intelligence Report | BI Report | - | |
|
||||
| `DataDomainOwner` | Data Domain Owner | Domain Owner | DDO | |
|
||||
| `DataSubDomainOwner` | Data Sub Domain Owner | Sub Domain Owner | SDO | |
|
||||
| `DataProductOwner` | Data Product Owner | Product Owner | PO | not DPO - the initialism is taken by Data Protection Officer, a real GDPR role |
|
||||
| `DataSteward` | Data Steward | Steward | - | |
|
||||
| `DataGovernanceLead` | Data Governance Lead | Governance Lead | DGL | |
|
||||
Binary file not shown.
@@ -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>
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,206 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Generate strict, queryable BPMN 2.0 files from processes.yaml (EV-008).
|
||||
|
||||
USAGE
|
||||
python3 generate_bpmn.py [output_dir]
|
||||
|
||||
One file per procedure, in bpmn/. Rule identifiers are carried in
|
||||
extensionElements under the project namespace, so a rule can be traced to every
|
||||
procedure that enforces it with a single XPath expression:
|
||||
|
||||
//bpmn:process[.//pr:rule='TN-002']/@id
|
||||
|
||||
Diagram interchange is emitted with a simple left-to-right layout so the files
|
||||
open directly in any BPMN editor. Layout is cosmetic and never authoritative.
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
from xml.sax.saxutils import escape
|
||||
|
||||
import yaml
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
SRC = os.path.join(HERE, "processes.yaml")
|
||||
OUTDIR = sys.argv[1] if len(sys.argv) > 1 else os.path.join(HERE, "bpmn")
|
||||
|
||||
BPMN = "http://www.omg.org/spec/BPMN/20100524/MODEL"
|
||||
BPMNDI = "http://www.omg.org/spec/BPMN/20100524/DI"
|
||||
DI = "http://www.omg.org/spec/DD/20100524/DI"
|
||||
DC = "http://www.omg.org/spec/DD/20100524/DC"
|
||||
|
||||
TAG = {
|
||||
"start": "startEvent",
|
||||
"end": "endEvent",
|
||||
"userTask": "userTask",
|
||||
"scriptTask": "scriptTask",
|
||||
"callActivity": "callActivity",
|
||||
"gateway": "exclusiveGateway",
|
||||
}
|
||||
W = {"start": 36, "end": 36, "gateway": 50}
|
||||
H = {"start": 36, "end": 36, "gateway": 50}
|
||||
DEFAULT_W, DEFAULT_H = 150, 70
|
||||
COL, ROW = 200, 130
|
||||
|
||||
|
||||
def selfcheck(doc, rule_ids):
|
||||
"""A procedure must be a connected graph, and every rule it cites must exist."""
|
||||
problems = []
|
||||
ids = {p["id"] for p in doc["processes"]}
|
||||
for p in doc["processes"]:
|
||||
nodes = {n["id"] for n in p["flow"]}
|
||||
for f in p["flows"]:
|
||||
for side in ("from", "to"):
|
||||
if f[side] not in nodes:
|
||||
problems.append("%s: flow references unknown node %s" % (p["id"], f[side]))
|
||||
targets = {f["to"] for f in p["flows"]}
|
||||
sources = {f["from"] for f in p["flows"]}
|
||||
for n in p["flow"]:
|
||||
if n["type"] == "start" and n["id"] in targets:
|
||||
problems.append("%s: start event %s has an incoming flow" % (p["id"], n["id"]))
|
||||
if n["type"] == "end" and n["id"] in sources:
|
||||
problems.append("%s: end event %s has an outgoing flow" % (p["id"], n["id"]))
|
||||
if n["type"] not in ("start", "end") and n["id"] not in targets:
|
||||
problems.append("%s: node %s is unreachable" % (p["id"], n["id"]))
|
||||
if n["type"] != "end" and n["id"] not in sources:
|
||||
problems.append("%s: node %s is a dead end" % (p["id"], n["id"]))
|
||||
if n["type"] == "callActivity" and n.get("calls") not in ids:
|
||||
problems.append("%s: %s calls unknown procedure %s"
|
||||
% (p["id"], n["id"], n.get("calls")))
|
||||
for r in n.get("rules") or []:
|
||||
if rule_ids and r not in rule_ids:
|
||||
problems.append("%s/%s: unknown rule %s" % (p["id"], n["id"], r))
|
||||
return problems
|
||||
|
||||
|
||||
def layout(proc):
|
||||
"""Assign a column by longest path from start, a row to disambiguate siblings."""
|
||||
succ = {}
|
||||
for f in proc["flows"]:
|
||||
succ.setdefault(f["from"], []).append(f["to"])
|
||||
starts = [n["id"] for n in proc["flow"] if n["type"] == "start"]
|
||||
depth = {}
|
||||
for s in starts:
|
||||
stack = [(s, 0)]
|
||||
seen = set()
|
||||
while stack:
|
||||
node, d = stack.pop()
|
||||
if (node, d) in seen:
|
||||
continue
|
||||
seen.add((node, d))
|
||||
if depth.get(node, -1) < d:
|
||||
depth[node] = d
|
||||
for nxt in succ.get(node, []):
|
||||
if d < 60:
|
||||
stack.append((nxt, d + 1))
|
||||
used = {}
|
||||
pos = {}
|
||||
for n in proc["flow"]:
|
||||
c = depth.get(n["id"], 0)
|
||||
r = used.get(c, 0)
|
||||
used[c] = r + 1
|
||||
pos[n["id"]] = (60 + c * COL, 80 + r * ROW)
|
||||
return pos
|
||||
|
||||
|
||||
def build(proc, ns):
|
||||
pid = proc["id"]
|
||||
o = []
|
||||
a = o.append
|
||||
a('<?xml version="1.0" encoding="UTF-8"?>')
|
||||
a('<bpmn:definitions xmlns:bpmn="%s" xmlns:bpmndi="%s" xmlns:di="%s" '
|
||||
'xmlns:dc="%s" xmlns:pr="%s" id="defs_%s" targetNamespace="%s" '
|
||||
'exporter="generate_bpmn.py">' % (BPMN, BPMNDI, DI, DC, ns, pid, ns))
|
||||
a(' <bpmn:process id="%s" name="%s" isExecutable="false">'
|
||||
% (pid, escape(proc["name"])))
|
||||
doc = "%s | Scope: %s | Trigger: %s" % (
|
||||
proc["name"], proc.get("scope", ""), proc.get("trigger", ""))
|
||||
a(' <bpmn:documentation>%s</bpmn:documentation>' % escape(doc))
|
||||
|
||||
incoming, outgoing = {}, {}
|
||||
for i, f in enumerate(proc["flows"], start=1):
|
||||
fid = "flow_%d" % i
|
||||
f["_id"] = fid
|
||||
outgoing.setdefault(f["from"], []).append(fid)
|
||||
incoming.setdefault(f["to"], []).append(fid)
|
||||
|
||||
for n in proc["flow"]:
|
||||
tag = TAG[n["type"]]
|
||||
attrs = 'id="%s" name="%s"' % (n["id"], escape(n["name"]))
|
||||
if n["type"] == "callActivity":
|
||||
attrs += ' calledElement="%s"' % n["calls"]
|
||||
a(' <bpmn:%s %s>' % (tag, attrs))
|
||||
rules = n.get("rules") or []
|
||||
if rules:
|
||||
a(' <bpmn:extensionElements>')
|
||||
a(' <pr:rules>')
|
||||
for r in rules:
|
||||
a(' <pr:rule>%s</pr:rule>' % r)
|
||||
a(' </pr:rules>')
|
||||
a(' </bpmn:extensionElements>')
|
||||
for fid in incoming.get(n["id"], []):
|
||||
a(' <bpmn:incoming>%s</bpmn:incoming>' % fid)
|
||||
for fid in outgoing.get(n["id"], []):
|
||||
a(' <bpmn:outgoing>%s</bpmn:outgoing>' % fid)
|
||||
a(' </bpmn:%s>' % tag)
|
||||
|
||||
for f in proc["flows"]:
|
||||
nm = ' name="%s"' % escape(f["condition"]) if f.get("condition") else ""
|
||||
a(' <bpmn:sequenceFlow id="%s"%s sourceRef="%s" targetRef="%s" />'
|
||||
% (f["_id"], nm, f["from"], f["to"]))
|
||||
a(' </bpmn:process>')
|
||||
|
||||
pos = layout(proc)
|
||||
a(' <bpmndi:BPMNDiagram id="diagram_%s">' % pid)
|
||||
a(' <bpmndi:BPMNPlane id="plane_%s" bpmnElement="%s">' % (pid, pid))
|
||||
for n in proc["flow"]:
|
||||
x, y = pos[n["id"]]
|
||||
w, h = W.get(n["type"], DEFAULT_W), H.get(n["type"], DEFAULT_H)
|
||||
a(' <bpmndi:BPMNShape id="shape_%s" bpmnElement="%s">' % (n["id"], n["id"]))
|
||||
a(' <dc:Bounds x="%d" y="%d" width="%d" height="%d" />' % (x, y, w, h))
|
||||
a(' </bpmndi:BPMNShape>')
|
||||
for f in proc["flows"]:
|
||||
sx, sy = pos[f["from"]]
|
||||
tx, ty = pos[f["to"]]
|
||||
sw = W.get(next(n["type"] for n in proc["flow"] if n["id"] == f["from"]), DEFAULT_W)
|
||||
sh = H.get(next(n["type"] for n in proc["flow"] if n["id"] == f["from"]), DEFAULT_H)
|
||||
th = H.get(next(n["type"] for n in proc["flow"] if n["id"] == f["to"]), DEFAULT_H)
|
||||
a(' <bpmndi:BPMNEdge id="edge_%s" bpmnElement="%s">' % (f["_id"], f["_id"]))
|
||||
a(' <di:waypoint x="%d" y="%d" />' % (sx + sw, sy + sh // 2))
|
||||
a(' <di:waypoint x="%d" y="%d" />' % (tx, ty + th // 2))
|
||||
a(' </bpmndi:BPMNEdge>')
|
||||
a(' </bpmndi:BPMNPlane>')
|
||||
a(' </bpmndi:BPMNDiagram>')
|
||||
a('</bpmn:definitions>')
|
||||
return "\n".join(o) + "\n"
|
||||
|
||||
|
||||
def main():
|
||||
doc = yaml.safe_load(open(SRC, encoding="utf-8"))
|
||||
rule_ids = set()
|
||||
rb = os.path.join(HERE, "rules.yaml")
|
||||
if os.path.exists(rb):
|
||||
rule_ids = {r["id"] for r in yaml.safe_load(open(rb, encoding="utf-8"))["rules"]}
|
||||
|
||||
problems = selfcheck(doc, rule_ids)
|
||||
if problems:
|
||||
print("SELF-CHECK FAILED")
|
||||
for p in problems:
|
||||
print(" " + p)
|
||||
sys.exit(1)
|
||||
|
||||
os.makedirs(OUTDIR, exist_ok=True)
|
||||
ns = doc["meta"]["namespace"]
|
||||
for proc in doc["processes"]:
|
||||
path = os.path.join(OUTDIR, "%s.bpmn" % proc["id"])
|
||||
open(path, "w", encoding="utf-8").write(build(proc, ns))
|
||||
n_rules = len({r for n in proc["flow"] for r in (n.get("rules") or [])})
|
||||
print(" %-4s %-46s %2d nodes, %2d flows, %2d rules"
|
||||
% (proc["id"], proc["name"], len(proc["flow"]),
|
||||
len(proc["flows"]), n_rules))
|
||||
print("self-check passed: %d procedures, graphs connected, every rule known"
|
||||
% len(doc["processes"]))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,157 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Generate the Markdown processbook, with Mermaid views, from processes.yaml
|
||||
(EV-008). The BPMN files remain the authoritative form; the views here are
|
||||
derived from the same source and cannot drift from it.
|
||||
|
||||
USAGE
|
||||
python3 generate_processbook_md.py [output.md]
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
|
||||
import yaml
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
SRC = os.path.join(HERE, "processes.yaml")
|
||||
OUT = sys.argv[1] if len(sys.argv) > 1 else os.path.join(HERE, "PR_TBox_Processbook.md")
|
||||
|
||||
SHAPE = {
|
||||
"start": ("([", "])"),
|
||||
"end": ("([", "])"),
|
||||
"userTask": ("[", "]"),
|
||||
"scriptTask": ("[", "]"),
|
||||
"callActivity": ("[[", "]]"),
|
||||
"gateway": ("{", "}"),
|
||||
}
|
||||
KIND = {
|
||||
"userTask": "human",
|
||||
"scriptTask": "script",
|
||||
"callActivity": "calls",
|
||||
"gateway": "decision",
|
||||
"start": "start",
|
||||
"end": "end",
|
||||
}
|
||||
|
||||
|
||||
def flow(t):
|
||||
return " ".join((t or "").split())
|
||||
|
||||
|
||||
def mermaid(proc):
|
||||
out = ["```mermaid", "flowchart TD"]
|
||||
for n in proc["flow"]:
|
||||
o, c = SHAPE[n["type"]]
|
||||
label = n["name"].replace('"', "'")
|
||||
if n["type"] == "callActivity":
|
||||
label = "%s: %s" % (n["calls"], label)
|
||||
out.append(' %s%s"%s"%s' % (n["id"], o, label, c))
|
||||
for f in proc["flows"]:
|
||||
if f.get("condition"):
|
||||
out.append(' %s -->|%s| %s' % (f["from"], f["condition"], f["to"]))
|
||||
else:
|
||||
out.append(" %s --> %s" % (f["from"], f["to"]))
|
||||
for n in proc["flow"]:
|
||||
if n["type"] == "userTask":
|
||||
out.append(" class %s human;" % n["id"])
|
||||
elif n["type"] == "gateway":
|
||||
out.append(" class %s decision;" % n["id"])
|
||||
out.append(" classDef human fill:#FFF4CE,stroke:#B08900;")
|
||||
out.append(" classDef decision fill:#E8F0F7,stroke:#3E6E96;")
|
||||
out.append("```")
|
||||
return out
|
||||
|
||||
|
||||
def main():
|
||||
doc = yaml.safe_load(open(SRC, encoding="utf-8"))
|
||||
m, out = doc["meta"], []
|
||||
w = out.append
|
||||
|
||||
w("# %s" % m["title"])
|
||||
w("")
|
||||
w("**Version %s** — %s — generated %s" % (m["version"], m["status"], m["date"]))
|
||||
w("")
|
||||
w("> Generated from `processes.yaml`. The BPMN files in `bpmn/` are the "
|
||||
"authoritative form; the views below are derived from the same source. "
|
||||
"Do not edit this document (EV-007, EV-008).")
|
||||
w("")
|
||||
w(flow(m["scope"]))
|
||||
w("")
|
||||
w("## Procedures")
|
||||
w("")
|
||||
w("| Id | Family | Procedure | Scope |")
|
||||
w("|---|---|---|---|")
|
||||
for p in doc["processes"]:
|
||||
w("| **%s** | %s | %s | %s |" % (p["id"], p["family"], p["name"], p.get("scope", "")))
|
||||
w("")
|
||||
w("## Reading a diagram")
|
||||
w("")
|
||||
w("- A **rounded** node is a start or end state.")
|
||||
w("- A **diamond** is a decision; every outgoing path is labelled.")
|
||||
w("- A **shaded** box is a human task: it cannot be automated, and the work "
|
||||
"stops there until someone acts.")
|
||||
w("- A **double-bordered** box calls another procedure by its identifier.")
|
||||
w("")
|
||||
|
||||
for p in doc["processes"]:
|
||||
w("---")
|
||||
w("")
|
||||
w("# %s — %s" % (p["id"], p["name"]))
|
||||
w("")
|
||||
w("| | |")
|
||||
w("|---|---|")
|
||||
w("| Family | %s |" % p["family"])
|
||||
w("| Scope | %s |" % p.get("scope", ""))
|
||||
w("| Trigger | %s |" % p.get("trigger", ""))
|
||||
w("| Inputs | %s |" % ", ".join(p.get("inputs") or []))
|
||||
w("| Outputs | %s |" % ", ".join(p.get("outputs") or []))
|
||||
rules = sorted({r for n in p["flow"] for r in (n.get("rules") or [])})
|
||||
w("| Rules enforced | %s |" % (", ".join(rules) or "-"))
|
||||
calls = sorted({n["calls"] for n in p["flow"] if n["type"] == "callActivity"})
|
||||
w("| Calls | %s |" % (", ".join(calls) or "-"))
|
||||
w("")
|
||||
if p.get("parameter"):
|
||||
w("**Parameter.** %s" % flow(p["parameter"]))
|
||||
w("")
|
||||
if p.get("note"):
|
||||
w("**Note.** %s" % flow(p["note"]))
|
||||
w("")
|
||||
out.extend(mermaid(p))
|
||||
w("")
|
||||
w("| Step | Type | Rules |")
|
||||
w("|---|---|---|")
|
||||
for n in p["flow"]:
|
||||
if n["type"] in ("start", "end"):
|
||||
continue
|
||||
name = n["name"]
|
||||
if n["type"] == "callActivity":
|
||||
name = "%s (%s)" % (name, n["calls"])
|
||||
w("| %s | %s | %s |"
|
||||
% (name, KIND[n["type"]], ", ".join(n.get("rules") or []) or "-"))
|
||||
w("")
|
||||
|
||||
w("---")
|
||||
w("")
|
||||
w("# Rule coverage")
|
||||
w("")
|
||||
w("Which procedures enforce each rule. Derived, not maintained: a rule cited "
|
||||
"in a diagram appears here automatically.")
|
||||
w("")
|
||||
cover = {}
|
||||
for p in doc["processes"]:
|
||||
for n in p["flow"]:
|
||||
for r in n.get("rules") or []:
|
||||
cover.setdefault(r, set()).add(p["id"])
|
||||
w("| Rule | Procedures |")
|
||||
w("|---|---|")
|
||||
for r in sorted(cover):
|
||||
w("| %s | %s |" % (r, ", ".join(sorted(cover[r]))))
|
||||
w("")
|
||||
|
||||
open(OUT, "w", encoding="utf-8").write("\n".join(out))
|
||||
print("written: %s (%d lines, %d procedures, %d rules covered)"
|
||||
% (OUT, len(out), len(doc["processes"]), len(cover)))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,154 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Generate the Markdown rulebook from rules.yaml (EV-008).
|
||||
|
||||
USAGE
|
||||
python3 generate_rulebook_md.py [output.md]
|
||||
|
||||
rules.yaml is the single source of truth. This script never edits it.
|
||||
The output must never be edited by hand (EV-007).
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
|
||||
import yaml
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
SRC = os.path.join(HERE, "rules.yaml")
|
||||
OUT = sys.argv[1] if len(sys.argv) > 1 else os.path.join(HERE, "PR_TBox_Rulebook.md")
|
||||
|
||||
|
||||
def selfcheck(doc):
|
||||
"""EV-011 and EV-012 applied to the rulebook itself."""
|
||||
problems = []
|
||||
for r in doc["rules"]:
|
||||
ctl = r.get("control") or {}
|
||||
if not ctl.get("tier"):
|
||||
problems.append("%s: no control tier (EV-012)" % r["id"])
|
||||
if r.get("severity") == "BLOCKING" and not ctl.get("executor"):
|
||||
problems.append("%s: BLOCKING without an executor (EV-011)" % r["id"])
|
||||
if r.get("category") not in doc["categories"]:
|
||||
problems.append("%s: unknown category" % r["id"])
|
||||
return problems
|
||||
|
||||
|
||||
def flow(text):
|
||||
"""Collapse a YAML folded scalar into a single paragraph."""
|
||||
return " ".join((text or "").split())
|
||||
|
||||
|
||||
def joined(v):
|
||||
return ", ".join(v) if isinstance(v, list) else (v or "-")
|
||||
|
||||
|
||||
def main():
|
||||
doc = yaml.safe_load(open(SRC, encoding="utf-8"))
|
||||
problems = selfcheck(doc)
|
||||
if problems:
|
||||
print("SELF-CHECK FAILED")
|
||||
for p in problems:
|
||||
print(" " + p)
|
||||
sys.exit(1)
|
||||
|
||||
m, out = doc["meta"], []
|
||||
w = out.append
|
||||
|
||||
w("# %s" % m["title"])
|
||||
w("")
|
||||
w("**Version %s** — %s — generated %s" % (m["version"], m["status"], m["date"]))
|
||||
w("")
|
||||
w("> Generated from `rules.yaml`. Do not edit this document: edit the source "
|
||||
"and regenerate (EV-007, EV-008).")
|
||||
w("")
|
||||
w("## Scope")
|
||||
w("")
|
||||
w(flow(m["scope"]))
|
||||
w("")
|
||||
w(flow(m["audience"]))
|
||||
w("")
|
||||
w("## Categories")
|
||||
w("")
|
||||
w("| Category | Subject | Rules |")
|
||||
w("|---|---|---|")
|
||||
for k, v in doc["categories"].items():
|
||||
n = sum(1 for r in doc["rules"] if r["category"] == k)
|
||||
w("| **%s** | %s | %d |" % (k, v["title"], n))
|
||||
w("")
|
||||
w("## Severity")
|
||||
w("")
|
||||
for k, v in doc["severity_model"].items():
|
||||
w("- **%s** — %s" % (k, flow(v)))
|
||||
w("")
|
||||
|
||||
for cat, info in doc["categories"].items():
|
||||
w("---")
|
||||
w("")
|
||||
w("# %s — %s" % (cat, info["title"]))
|
||||
w("")
|
||||
w(flow(info["intent"]))
|
||||
w("")
|
||||
for r in [x for x in doc["rules"] if x["category"] == cat]:
|
||||
ctl = r.get("control") or {}
|
||||
w("## %s — %s" % (r["id"], r["title"]))
|
||||
w("")
|
||||
w("**Statement.** %s" % flow(r["statement"]))
|
||||
w("")
|
||||
w("| | |")
|
||||
w("|---|---|")
|
||||
w("| Severity | **%s** |" % r["severity"])
|
||||
w("| Applies to | %s |" % joined(r.get("scope")))
|
||||
w("| Enforced at | %s |" % joined(ctl.get("tier")))
|
||||
w("| Executor | `%s` |" % (ctl.get("executor") or "-"))
|
||||
w("| Procedure | %s |" % (ctl.get("procedure") or "_pending_"))
|
||||
if r.get("filiation"):
|
||||
w("| Related | %s |" % r["filiation"])
|
||||
w("")
|
||||
if r.get("rationale"):
|
||||
w("**Why.** %s" % flow(r["rationale"]))
|
||||
w("")
|
||||
if r.get("examples"):
|
||||
w("| From | To | Note |")
|
||||
w("|---|---|---|")
|
||||
for e in r["examples"]:
|
||||
w("| %s | %s | %s |" % (e["from"], e["to"], e.get("note") or ""))
|
||||
w("")
|
||||
|
||||
ab = doc["abstractness"]
|
||||
w("---")
|
||||
w("")
|
||||
w("# Annex A — Abstractness")
|
||||
w("")
|
||||
w("Applies TN-006 and TN-007 to the model. %d classes, of which **%d abstract** "
|
||||
"and **%d concrete**. Maintained with the model, not after it."
|
||||
% (len(ab), sum(1 for a in ab if a["is_abstract"]),
|
||||
sum(1 for a in ab if not a["is_abstract"])))
|
||||
w("")
|
||||
w("| Class | isAbstract | Note |")
|
||||
w("|---|---|---|")
|
||||
for a in ab:
|
||||
w("| `%s` | %s | %s |"
|
||||
% (a["term"], "**true**" if a["is_abstract"] else "false", a.get("note") or ""))
|
||||
w("")
|
||||
w("---")
|
||||
w("")
|
||||
w("# Annex B — Display")
|
||||
w("")
|
||||
w("Applies TN-011 and TN-022. The short label is what screens display and what "
|
||||
"relation names reuse; the acronym is a search key, never an identity.")
|
||||
w("")
|
||||
w("| IRI | rdfs:label | shortLabel | acronym | Note |")
|
||||
w("|---|---|---|---|---|")
|
||||
for d in doc["display"]:
|
||||
w("| `%s` | %s | %s | %s | %s |"
|
||||
% (d["iri"], d["label"], d.get("short_label") or "-",
|
||||
d.get("acronym") or "-", d.get("note") or ""))
|
||||
w("")
|
||||
|
||||
open(OUT, "w", encoding="utf-8").write("\n".join(out))
|
||||
print("self-check passed: %d rules, every tier assigned, no blocking rule "
|
||||
"without an executor" % len(doc["rules"]))
|
||||
print("written: %s (%d lines)" % (OUT, len(out)))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,218 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Generate the Excel rulebook from rules.yaml (EV-008).
|
||||
|
||||
USAGE
|
||||
python3 generate_rulebook_xlsx.py [output.xlsx]
|
||||
|
||||
rules.yaml is the single source of truth. This script never edits it.
|
||||
The output must never be edited by hand (EV-007).
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
|
||||
import yaml
|
||||
from openpyxl import Workbook
|
||||
from openpyxl.styles import Alignment, Border, Font, PatternFill, Side
|
||||
from openpyxl.utils import get_column_letter
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
SRC = os.path.join(HERE, "rules.yaml")
|
||||
OUT = sys.argv[1] if len(sys.argv) > 1 else os.path.join(HERE, "PR_TBox_Rulebook.xlsx")
|
||||
|
||||
FONT = "Arial"
|
||||
INK = "1F2933"
|
||||
HEAD_FILL = PatternFill("solid", fgColor="1F2933")
|
||||
BAND = {
|
||||
"Identifier": "E8F0F7",
|
||||
"Label": "EAF3EC",
|
||||
"Declaration": "FBF0E4",
|
||||
"Evolution": "F2EAF5",
|
||||
}
|
||||
MAJOR_FILL = PatternFill("solid", fgColor="FFF4CE")
|
||||
THIN = Side(style="thin", color="C9CFD6")
|
||||
BORDER = Border(left=THIN, right=THIN, top=THIN, bottom=THIN)
|
||||
|
||||
|
||||
def selfcheck(doc):
|
||||
"""EV-011 and EV-012 applied to the rulebook itself."""
|
||||
problems = []
|
||||
for r in doc["rules"]:
|
||||
ctl = r.get("control") or {}
|
||||
if not ctl.get("tier"):
|
||||
problems.append("%s: no control tier (EV-012)" % r["id"])
|
||||
if r.get("severity") == "BLOCKING" and not ctl.get("executor"):
|
||||
problems.append("%s: BLOCKING without an executor (EV-011)" % r["id"])
|
||||
if r.get("category") not in doc["categories"]:
|
||||
problems.append("%s: unknown category" % r["id"])
|
||||
return problems
|
||||
|
||||
|
||||
def flow(t):
|
||||
return " ".join((t or "").split())
|
||||
|
||||
|
||||
def joined(v):
|
||||
return ", ".join(v) if isinstance(v, list) else (v if v else "")
|
||||
|
||||
|
||||
def header(ws, cols):
|
||||
for i, (title, width) in enumerate(cols, start=1):
|
||||
c = ws.cell(row=1, column=i, value=title)
|
||||
c.font = Font(name=FONT, size=10, bold=True, color="FFFFFF")
|
||||
c.fill = HEAD_FILL
|
||||
c.alignment = Alignment(vertical="center", horizontal="left", wrap_text=True)
|
||||
c.border = BORDER
|
||||
ws.column_dimensions[get_column_letter(i)].width = width
|
||||
ws.row_dimensions[1].height = 28
|
||||
ws.freeze_panes = ws.cell(row=2, column=1)
|
||||
ws.auto_filter.ref = "A1:%s1" % get_column_letter(len(cols))
|
||||
|
||||
|
||||
def put(ws, r, c, value, wrap=False, bold=False, fill=None, size=10):
|
||||
cell = ws.cell(row=r, column=c, value=value)
|
||||
cell.font = Font(name=FONT, size=size, bold=bold, color=INK)
|
||||
cell.alignment = Alignment(vertical="top", wrap_text=wrap)
|
||||
cell.border = BORDER
|
||||
if fill:
|
||||
cell.fill = fill
|
||||
return cell
|
||||
|
||||
|
||||
def sheet_cover(wb, doc):
|
||||
m = doc["meta"]
|
||||
ws = wb.create_sheet("Cover")
|
||||
ws.column_dimensions["A"].width = 24
|
||||
ws.column_dimensions["B"].width = 100
|
||||
put(ws, 1, 1, m["title"], bold=True, size=14)
|
||||
|
||||
rows = [
|
||||
("Version", "%s — %s" % (m["version"], m["status"])),
|
||||
("Generated", "%s from rules.yaml" % m["date"]),
|
||||
("Scope", flow(m["scope"])),
|
||||
("Audience", flow(m["audience"])),
|
||||
("Generation", "Generated from rules.yaml by generate_rulebook_xlsx.py. "
|
||||
"Do not edit this workbook: edit the source and regenerate "
|
||||
"(EV-007, EV-008)."),
|
||||
]
|
||||
r = 3
|
||||
for k, v in rows:
|
||||
put(ws, r, 1, k, bold=True)
|
||||
put(ws, r, 2, v, wrap=True)
|
||||
ws.row_dimensions[r].height = 15 * (len(v) // 95 + 1)
|
||||
r += 1
|
||||
|
||||
r += 1
|
||||
put(ws, r, 1, "Categories", bold=True, size=12)
|
||||
r += 1
|
||||
for k, v in doc["categories"].items():
|
||||
put(ws, r, 1, k, bold=True, fill=PatternFill("solid", fgColor=BAND[k]))
|
||||
put(ws, r, 2, "%s — %s" % (v["title"], flow(v["intent"])), wrap=True)
|
||||
ws.row_dimensions[r].height = 30
|
||||
r += 1
|
||||
|
||||
r += 1
|
||||
put(ws, r, 1, "Counts", bold=True, size=12)
|
||||
r += 1
|
||||
for label, formula in [
|
||||
("Rules", "=COUNTA(Rules!A2:A200)"),
|
||||
("of which blocking", '=COUNTIF(Rules!D2:D200,"BLOCKING")'),
|
||||
("Examples", "=COUNTA(Examples!A2:A400)"),
|
||||
("Classes", "=COUNTA(Abstractness!A2:A200)"),
|
||||
("of which abstract", "=COUNTIF(Abstractness!B2:B200,TRUE)"),
|
||||
("Display entries", "=COUNTA(Display!A2:A200)"),
|
||||
]:
|
||||
put(ws, r, 1, label)
|
||||
put(ws, r, 2, formula)
|
||||
r += 1
|
||||
|
||||
|
||||
def sheet_rules(wb, doc):
|
||||
ws = wb.create_sheet("Rules")
|
||||
header(ws, [("Rule", 10), ("Category", 14), ("Title", 40), ("Severity", 11),
|
||||
("Statement", 70), ("Applies to", 24), ("Enforced at", 14),
|
||||
("Executor", 26), ("Procedure", 16), ("Related", 16),
|
||||
("Why", 90)])
|
||||
r = 2
|
||||
for x in doc["rules"]:
|
||||
ctl = x.get("control") or {}
|
||||
band = PatternFill("solid", fgColor=BAND[x["category"]])
|
||||
put(ws, r, 1, x["id"], bold=True, fill=band)
|
||||
put(ws, r, 2, x["category"], fill=band)
|
||||
put(ws, r, 3, x["title"], wrap=True, bold=True)
|
||||
put(ws, r, 4, x["severity"],
|
||||
fill=MAJOR_FILL if x["severity"] != "BLOCKING" else None)
|
||||
put(ws, r, 5, flow(x["statement"]), wrap=True)
|
||||
put(ws, r, 6, joined(x.get("scope")), wrap=True)
|
||||
put(ws, r, 7, joined(ctl.get("tier")))
|
||||
put(ws, r, 8, ctl.get("executor") or "")
|
||||
put(ws, r, 9, ctl.get("procedure") or "pending")
|
||||
put(ws, r, 10, x.get("filiation") or "")
|
||||
put(ws, r, 11, flow(x.get("rationale")), wrap=True)
|
||||
ws.row_dimensions[r].height = 78
|
||||
r += 1
|
||||
|
||||
|
||||
def sheet_examples(wb, doc):
|
||||
ws = wb.create_sheet("Examples")
|
||||
header(ws, [("Rule", 10), ("Category", 14), ("From", 48), ("To", 48), ("Note", 62)])
|
||||
r = 2
|
||||
for x in doc["rules"]:
|
||||
for e in x.get("examples") or []:
|
||||
band = PatternFill("solid", fgColor=BAND[x["category"]])
|
||||
put(ws, r, 1, x["id"], bold=True, fill=band)
|
||||
put(ws, r, 2, x["category"], fill=band)
|
||||
put(ws, r, 3, str(e["from"]), wrap=True)
|
||||
put(ws, r, 4, str(e["to"]), wrap=True)
|
||||
put(ws, r, 5, e.get("note") or "", wrap=True)
|
||||
r += 1
|
||||
|
||||
|
||||
def sheet_abstractness(wb, doc):
|
||||
ws = wb.create_sheet("Abstractness")
|
||||
header(ws, [("Class", 36), ("isAbstract", 12), ("Note", 96)])
|
||||
r = 2
|
||||
for a in doc["abstractness"]:
|
||||
put(ws, r, 1, a["term"], bold=a["is_abstract"])
|
||||
put(ws, r, 2, a["is_abstract"])
|
||||
put(ws, r, 3, a.get("note") or "", wrap=True)
|
||||
r += 1
|
||||
|
||||
|
||||
def sheet_display(wb, doc):
|
||||
ws = wb.create_sheet("Display")
|
||||
header(ws, [("IRI", 36), ("rdfs:label", 36), ("shortLabel", 22),
|
||||
("acronym", 10), ("Note", 80)])
|
||||
r = 2
|
||||
for d in doc["display"]:
|
||||
put(ws, r, 1, d["iri"], bold=True)
|
||||
put(ws, r, 2, d["label"])
|
||||
put(ws, r, 3, d.get("short_label") or "")
|
||||
put(ws, r, 4, d.get("acronym") or "")
|
||||
put(ws, r, 5, d.get("note") or "", wrap=True)
|
||||
r += 1
|
||||
|
||||
|
||||
def main():
|
||||
doc = yaml.safe_load(open(SRC, encoding="utf-8"))
|
||||
problems = selfcheck(doc)
|
||||
if problems:
|
||||
print("SELF-CHECK FAILED")
|
||||
for p in problems:
|
||||
print(" " + p)
|
||||
sys.exit(1)
|
||||
|
||||
wb = Workbook()
|
||||
wb.remove(wb.active)
|
||||
sheet_cover(wb, doc)
|
||||
sheet_rules(wb, doc)
|
||||
sheet_examples(wb, doc)
|
||||
sheet_abstractness(wb, doc)
|
||||
sheet_display(wb, doc)
|
||||
wb.save(OUT)
|
||||
print("self-check passed: %d rules" % len(doc["rules"]))
|
||||
print("written: %s (%d sheets)" % (OUT, len(wb.sheetnames)))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,363 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Migrate the Pernod Ricard Data MetaModel from v1.6 to v2.0.
|
||||
|
||||
USAGE
|
||||
python3 migrate_tbox_v2_0.py # dry run, writes nothing
|
||||
python3 migrate_tbox_v2_0.py --apply # writes in place
|
||||
python3 migrate_tbox_v2_0.py --apply --log-dir logs/
|
||||
|
||||
--ontology path to the ontology TTL (default ../ontology/pr_metamodel.ttl)
|
||||
--instances path to an instance TTL (repeatable)
|
||||
--shapes path to a shapes TTL (repeatable)
|
||||
|
||||
DESIGN
|
||||
EV-004 dry run is the default; nothing is written without --apply
|
||||
EV-005 every edit goes through rdflib, never a regex on the text
|
||||
EV-006 guards test the target state, so a replay reports zero change
|
||||
EV-015 an execution log is written for every attempt
|
||||
|
||||
Run on GrosseBertha, inside the pinned venv:
|
||||
. venv/bin/activate && python3 migrate_tbox_v2_0.py
|
||||
"""
|
||||
import argparse
|
||||
import datetime
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from collections import OrderedDict
|
||||
|
||||
import yaml
|
||||
from rdflib import Graph, Literal, Namespace, RDF, RDFS, OWL, URIRef, XSD
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
SPEC = os.path.join(HERE, "renames.yaml")
|
||||
RULES = os.path.normpath(os.path.join(HERE, "..", "rules.yaml"))
|
||||
|
||||
DCTERMS = Namespace("http://purl.org/dc/terms/")
|
||||
|
||||
|
||||
# ----------------------------------------------------------------- utilities
|
||||
|
||||
class Report(object):
|
||||
"""Counts every step, so that idempotence is provable and not merely hoped."""
|
||||
|
||||
def __init__(self):
|
||||
self.steps = OrderedDict()
|
||||
self.notes = []
|
||||
|
||||
def add(self, step, n, detail=None):
|
||||
self.steps[step] = self.steps.get(step, 0) + n
|
||||
if detail:
|
||||
self.notes.append("%s: %s" % (step, detail))
|
||||
|
||||
@property
|
||||
def total(self):
|
||||
return sum(self.steps.values())
|
||||
|
||||
|
||||
def md5(path):
|
||||
h = hashlib.md5()
|
||||
with open(path, "rb") as f:
|
||||
for chunk in iter(lambda: f.read(65536), b""):
|
||||
h.update(chunk)
|
||||
return h.hexdigest()
|
||||
|
||||
|
||||
def local(uri, ns):
|
||||
s = str(uri)
|
||||
return s[len(ns):] if s.startswith(ns) else None
|
||||
|
||||
|
||||
def decamelise(name, is_class):
|
||||
"""TN-018. Consecutive capitals are kept together: they carry an acronym."""
|
||||
spaced = re.sub(r"(?<=[a-z0-9])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])", " ", name)
|
||||
return spaced if is_class else spaced[0].lower() + spaced[1:]
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ the work
|
||||
|
||||
def rename(graph, pr, old, new, report, step):
|
||||
"""EV-001. Rewrite every triple naming the identifier, in all three positions."""
|
||||
src, dst = pr[old], pr[new]
|
||||
if (dst, None, None) in graph and (src, None, None) not in graph:
|
||||
return 0 # EV-006: already done
|
||||
n = 0
|
||||
for s, p, o in list(graph):
|
||||
ns, np_, no = s, p, o
|
||||
if s == src:
|
||||
ns = dst
|
||||
if p == src:
|
||||
np_ = dst
|
||||
if o == src:
|
||||
no = dst
|
||||
if (ns, np_, no) != (s, p, o):
|
||||
graph.remove((s, p, o))
|
||||
graph.add((ns, np_, no))
|
||||
n += 1
|
||||
report.add(step, n)
|
||||
return n
|
||||
|
||||
|
||||
def merge(graph, pr, src_name, dst_name, report):
|
||||
"""The source disappears into an existing target, which keeps its declaration."""
|
||||
src, dst = pr[src_name], pr[dst_name]
|
||||
n = 0
|
||||
for s, p, o in list(graph.triples((None, src, None))):
|
||||
graph.remove((s, p, o))
|
||||
graph.add((s, dst, o))
|
||||
n += 1
|
||||
for s, p, o in list(graph.triples((src, None, None))):
|
||||
graph.remove((s, p, o)) # drop the source declaration
|
||||
n += 1
|
||||
report.add("merge", n)
|
||||
return n
|
||||
|
||||
|
||||
def drop_subject(graph, subject, report, step):
|
||||
"""EV-003. Remove the block AND every reference naming it, or inference rebuilds it."""
|
||||
n = 0
|
||||
for t in list(graph.triples((subject, None, None))):
|
||||
graph.remove(t)
|
||||
n += 1
|
||||
for t in list(graph.triples((None, None, subject))):
|
||||
graph.remove(t)
|
||||
n += 1
|
||||
for t in list(graph.triples((None, subject, None))):
|
||||
graph.remove(t)
|
||||
n += 1
|
||||
report.add(step, n)
|
||||
return n
|
||||
|
||||
|
||||
def count_instances(graphs, term):
|
||||
"""EV-002. The proof required before any permanent withdrawal."""
|
||||
n = 0
|
||||
for g in graphs:
|
||||
n += len(list(g.triples((None, RDF.type, term))))
|
||||
n += len(list(g.triples((None, term, None))))
|
||||
n += len(list(g.triples((None, None, term))))
|
||||
return n
|
||||
|
||||
|
||||
def migrate(onto, others, spec, rules, report):
|
||||
ns = spec["meta"]["namespace"]
|
||||
pr = Namespace(ns)
|
||||
all_graphs = [onto] + others
|
||||
|
||||
# 1 — deprecated terms are withdrawn outright (phase clause), proof first
|
||||
if spec["structural"].get("drop_deprecated"):
|
||||
for subj in list(onto.subjects(OWL.deprecated, Literal(True))):
|
||||
name = local(subj, ns) or str(subj)
|
||||
used = sum(count_instances([g], subj) for g in others)
|
||||
if used:
|
||||
report.add("deprecated_kept", 1, "%s still used %d times" % (name, used))
|
||||
continue
|
||||
for g in all_graphs:
|
||||
drop_subject(g, subj, report, "deprecated_dropped")
|
||||
|
||||
# 2 — renames, in dependency order
|
||||
for order in (1, 2, 3):
|
||||
for r in [x for x in spec["renames"] if x["order"] == order]:
|
||||
for g in all_graphs:
|
||||
rename(g, pr, r["from"], r["to"], report, "rename_order_%d" % order)
|
||||
|
||||
# 3 — merges
|
||||
for m in spec.get("merges") or []:
|
||||
for g in all_graphs:
|
||||
merge(g, pr, m["from"], m["into"], report)
|
||||
|
||||
# 4 — reclassify display properties (TN-003)
|
||||
for rc in spec["structural"]["reclassify"]:
|
||||
term = pr[rc["term"]]
|
||||
if (term, RDF.type, OWL.AnnotationProperty) not in onto:
|
||||
onto.remove((term, RDF.type, getattr(OWL, rc["from"])))
|
||||
onto.add((term, RDF.type, OWL.AnnotationProperty))
|
||||
report.add("reclassified", 1, rc["term"])
|
||||
|
||||
# 5 — create the governance layer root (TN-027)
|
||||
for c in spec["structural"].get("create_classes") or []:
|
||||
term = pr[c["term"]]
|
||||
if (term, RDF.type, OWL.Class) not in onto:
|
||||
onto.add((term, RDF.type, OWL.Class))
|
||||
onto.add((term, RDFS.subClassOf, pr[c["parent"]]))
|
||||
onto.add((term, RDFS.label, Literal(c["label"])))
|
||||
onto.add((term, RDFS.comment, Literal(" ".join(c["comment"].split()))))
|
||||
onto.add((term, pr.isAbstract, Literal(True)))
|
||||
report.add("classes_created", 1, c["term"])
|
||||
|
||||
# 6 — reparent (TN-027)
|
||||
for rp in spec["structural"].get("reparent") or []:
|
||||
term, parent = pr[rp["term"]], pr[rp["parent"]]
|
||||
if (term, RDFS.subClassOf, parent) not in onto:
|
||||
onto.add((term, RDFS.subClassOf, parent))
|
||||
report.add("reparented", 1, "%s -> %s" % (rp["term"], rp["parent"]))
|
||||
|
||||
# 7 — sub-properties (TN-028)
|
||||
for sp in spec["structural"].get("subproperties") or []:
|
||||
term, parent = pr[sp["term"]], pr[sp["parent"]]
|
||||
if (term, RDFS.subPropertyOf, parent) not in onto:
|
||||
onto.add((term, RDFS.subPropertyOf, parent))
|
||||
report.add("subproperties", 1, "%s -> %s" % (sp["term"], sp["parent"]))
|
||||
|
||||
# 8 — controlled values become literals (TN-016, TN-017)
|
||||
for conv in spec["structural"].get("to_literal") or []:
|
||||
prop = pr[conv["property"]]
|
||||
if (prop, RDF.type, OWL.DatatypeProperty) not in onto:
|
||||
onto.remove((prop, RDF.type, OWL.ObjectProperty))
|
||||
onto.add((prop, RDF.type, OWL.DatatypeProperty))
|
||||
onto.remove((prop, RDFS.range, None))
|
||||
onto.add((prop, RDFS.range, XSD.string))
|
||||
onto.add((prop, RDFS.domain, pr[conv["domain"]]))
|
||||
report.add("to_literal_property", 1, conv["property"])
|
||||
for g in all_graphs: # rewrite the asserted values
|
||||
for s, p, o in list(g.triples((None, prop, None))):
|
||||
name = local(o, ns)
|
||||
if name and name in conv["value_map"]:
|
||||
g.remove((s, p, o))
|
||||
g.add((s, p, Literal(conv["value_map"][name])))
|
||||
report.add("to_literal_values", 1)
|
||||
for ind in conv["drop_individuals"] + [conv["drop_class"]]:
|
||||
subj = pr[ind]
|
||||
if (subj, None, None) in onto:
|
||||
for g in all_graphs:
|
||||
drop_subject(g, subj, report, "to_literal_dropped")
|
||||
|
||||
# 9 — abstractness, from the rulebook annex (TN-006, TN-007)
|
||||
for a in rules["abstractness"]:
|
||||
term = pr[a["term"]]
|
||||
if (term, RDF.type, OWL.Class) not in onto:
|
||||
report.add("abstract_missing_class", 1, a["term"])
|
||||
continue
|
||||
want = Literal(bool(a["is_abstract"]))
|
||||
if (term, pr.isAbstract, want) not in onto:
|
||||
onto.remove((term, pr.isAbstract, None))
|
||||
onto.add((term, pr.isAbstract, want))
|
||||
report.add("isAbstract_declared", 1)
|
||||
|
||||
# 10 — display annotations, from the rulebook annex (TN-022)
|
||||
# The annex is authoritative AND exhaustive: a null value means any existing
|
||||
# annotation is removed. A short label identical to the label carries no
|
||||
# information and is one more thing to keep in step.
|
||||
for d in rules["display"]:
|
||||
term = pr[d["iri"]]
|
||||
for prop, value in ((pr.shortLabel, d.get("short_label")),
|
||||
(pr.acronym, d.get("acronym"))):
|
||||
current = list(onto.objects(term, prop))
|
||||
if value:
|
||||
if (term, prop, Literal(value)) not in onto:
|
||||
onto.remove((term, prop, None))
|
||||
onto.add((term, prop, Literal(value)))
|
||||
report.add("display_set", 1)
|
||||
elif current:
|
||||
onto.remove((term, prop, None))
|
||||
report.add("display_removed", len(current))
|
||||
|
||||
# 11 — regenerate every label by derivation (TN-018, TN-023)
|
||||
kinds = (OWL.Class, OWL.ObjectProperty, OWL.DatatypeProperty, OWL.AnnotationProperty)
|
||||
for kind in kinds:
|
||||
for term in set(onto.subjects(RDF.type, kind)):
|
||||
name = local(term, ns)
|
||||
if not name:
|
||||
continue
|
||||
want = Literal(decamelise(name, kind == OWL.Class))
|
||||
if (term, RDFS.label, want) not in onto:
|
||||
onto.remove((term, RDFS.label, None))
|
||||
onto.add((term, RDFS.label, want))
|
||||
report.add("labels_regenerated", 1)
|
||||
|
||||
# 12 — bump the ontology version (EV-014: the namespace itself never moves)
|
||||
target = URIRef(ns.rstrip("/") + "/" + spec["meta"]["to_version"])
|
||||
for onto_iri in set(onto.subjects(RDF.type, OWL.Ontology)):
|
||||
if (onto_iri, OWL.versionIRI, target) not in onto:
|
||||
onto.remove((onto_iri, OWL.versionIRI, None))
|
||||
onto.add((onto_iri, OWL.versionIRI, target))
|
||||
report.add("version_bumped", 1, str(target))
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------- main
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--apply", action="store_true", help="write the files (default: dry run)")
|
||||
ap.add_argument("--ontology", default=os.path.join(HERE, "..", "ontology", "pr_metamodel.ttl"))
|
||||
ap.add_argument("--instances", action="append", default=[])
|
||||
ap.add_argument("--shapes", action="append", default=[])
|
||||
ap.add_argument("--log-dir", default=os.path.join(HERE, "logs"))
|
||||
args = ap.parse_args()
|
||||
|
||||
spec = yaml.safe_load(open(SPEC, encoding="utf-8"))
|
||||
rules = yaml.safe_load(open(RULES, encoding="utf-8"))
|
||||
|
||||
paths = [args.ontology] + args.instances + args.shapes
|
||||
missing = [p for p in paths if not os.path.exists(p)]
|
||||
if missing:
|
||||
print("MISSING INPUT")
|
||||
for p in missing:
|
||||
print(" " + p)
|
||||
sys.exit(1)
|
||||
|
||||
inputs = OrderedDict((p, md5(p)) for p in paths)
|
||||
|
||||
onto = Graph()
|
||||
onto.parse(args.ontology, format="turtle")
|
||||
others, other_paths = [], args.instances + args.shapes
|
||||
for p in other_paths:
|
||||
g = Graph()
|
||||
g.parse(p, format="turtle")
|
||||
others.append(g)
|
||||
|
||||
before = [len(onto)] + [len(g) for g in others]
|
||||
report = Report()
|
||||
migrate(onto, others, spec, rules, report)
|
||||
after = [len(onto)] + [len(g) for g in others]
|
||||
|
||||
print("MIGRATION %s -> %s %s"
|
||||
% (spec["meta"]["from_version"], spec["meta"]["to_version"],
|
||||
"APPLY" if args.apply else "DRY RUN"))
|
||||
print()
|
||||
for step, n in report.steps.items():
|
||||
print(" %-28s %6d" % (step, n))
|
||||
print(" %-28s %6d" % ("total changes", report.total))
|
||||
print()
|
||||
for p, b, a in zip(paths, before, after):
|
||||
print(" %-46s %6d -> %6d triples" % (os.path.basename(p), b, a))
|
||||
if report.notes:
|
||||
print()
|
||||
for n in report.notes:
|
||||
print(" note: " + n)
|
||||
|
||||
log = {
|
||||
"attempt_timestamp": datetime.datetime.now().isoformat(timespec="seconds"),
|
||||
"mode": "apply" if args.apply else "dry-run",
|
||||
"from_version": spec["meta"]["from_version"],
|
||||
"to_version": spec["meta"]["to_version"],
|
||||
"input_checksums": inputs,
|
||||
"steps": report.steps,
|
||||
"total_changes": report.total,
|
||||
"triples_before": dict(zip(paths, before)),
|
||||
"triples_after": dict(zip(paths, after)),
|
||||
"notes": report.notes,
|
||||
}
|
||||
os.makedirs(args.log_dir, exist_ok=True)
|
||||
stamp = datetime.datetime.now().strftime("%Y%m%dT%H%M%S")
|
||||
log_path = os.path.join(args.log_dir, "migration_%s.json" % stamp)
|
||||
json.dump(log, open(log_path, "w", encoding="utf-8"), indent=2)
|
||||
print("\n log: %s" % log_path)
|
||||
|
||||
if not args.apply:
|
||||
print("\n DRY RUN — nothing written. Re-run with --apply when the counts "
|
||||
"above are what you expect.")
|
||||
return
|
||||
|
||||
onto.serialize(destination=args.ontology, format="turtle")
|
||||
for p, g in zip(other_paths, others):
|
||||
g.serialize(destination=p, format="turtle")
|
||||
print("\n written. Replay this script now: a second run must report zero "
|
||||
"changes (EV-006).")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,154 @@
|
||||
# T-BOX MIGRATION TO v2.0 — RENAME MAP AND STRUCTURAL OPERATIONS
|
||||
# =============================================================================
|
||||
# Data consumed by migrate_tbox_v2_0.py. Kept separate from the script so that
|
||||
# the transformation is reviewable without reading code (EV-004).
|
||||
#
|
||||
# Not listed here and derived from other sources at run time:
|
||||
# - isAbstract declarations -> rules.yaml, annex `abstractness`
|
||||
# - shortLabel and acronym -> rules.yaml, annex `display`
|
||||
# - deprecated terms to drop -> found in the graph by owl:deprecated true
|
||||
# Deriving them keeps a single source of truth for each fact (EV-008).
|
||||
# =============================================================================
|
||||
|
||||
meta:
|
||||
from_version: "1.6"
|
||||
to_version: "2.0"
|
||||
namespace: "https://ontology.pernod-ricard.com/metamodel/"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# RENAMES — 48. Applied everywhere the identifier appears: subject, predicate,
|
||||
# object. Nothing cascades in RDF (EV-001).
|
||||
#
|
||||
# `order` is a migration constraint, not a preference:
|
||||
# 1 ordinary terms
|
||||
# 2 terms named in SHACL shapes or SPARQL constraints
|
||||
# 3 the four attributes borne by MetaModelObject, therefore by every subject
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
renames:
|
||||
|
||||
# --- classes
|
||||
- {from: SubDomain, to: DataSubDomain, kind: class, order: 1}
|
||||
- {from: SubDomainOwner, to: DataSubDomainOwner, kind: class, order: 1}
|
||||
- {from: KPI, to: KeyPerformanceIndicator, kind: class, order: 1}
|
||||
- {from: BIWorkspace, to: BusinessIntelligenceWorkspace, kind: class, order: 1}
|
||||
- {from: BIDataSource, to: BusinessIntelligenceDataSource, kind: class, order: 1}
|
||||
- {from: BIField, to: BusinessIntelligenceField, kind: class, order: 1}
|
||||
- {from: BIReport, to: BusinessIntelligenceReport, kind: class, order: 1}
|
||||
|
||||
# --- individual
|
||||
- {from: BITool, to: BusinessIntelligenceTool, kind: individual, order: 1}
|
||||
|
||||
# --- relations
|
||||
- {from: hasDGL, to: hasGovernanceLead, kind: relation, order: 1}
|
||||
- {from: aboutConcept, to: isAbout, kind: relation, order: 1}
|
||||
- {from: primaryLocation, to: primarilyStoredIn, kind: relation, order: 1}
|
||||
- {from: inDatabase, to: isInDatabase, kind: relation, order: 1}
|
||||
- {from: inSchema, to: isInSchema, kind: relation, order: 1}
|
||||
- {from: inWorkspace, to: isInBIWorkspace, kind: relation, order: 1}
|
||||
- {from: inBIDataSource, to: isInBIDataSource, kind: relation, order: 1}
|
||||
- {from: hasBIField, to: containsBIField, kind: relation, order: 1}
|
||||
- {from: hasBIOwner, to: hasPublisher, kind: relation, order: 1}
|
||||
- {from: fromField, to: referencesSourceField, kind: relation, order: 1}
|
||||
- {from: toField, to: referencesTargetField, kind: relation, order: 1}
|
||||
- {from: biDerivedFrom, to: derivedFromSourceField, kind: relation, order: 1}
|
||||
- {from: owningDomain, to: ownedByDomain, kind: relation, order: 2}
|
||||
|
||||
# --- attributes
|
||||
- {from: hasBusinessDefinition, to: businessDefinition, kind: attribute, order: 1}
|
||||
- {from: hasTechnicalDefinition, to: technicalDefinition, kind: attribute, order: 1}
|
||||
- {from: hasSynonym, to: synonym, kind: attribute, order: 1}
|
||||
- {from: hasFormula, to: formula, kind: attribute, order: 1}
|
||||
- {from: hasUnit, to: unit, kind: attribute, order: 1}
|
||||
- {from: hasTimeAggregation, to: timeAggregation, kind: attribute, order: 1}
|
||||
- {from: hasFormat, to: logicalFormat, kind: attribute, order: 1}
|
||||
- {from: hasPhysicalDataType, to: physicalDataType, kind: attribute, order: 1}
|
||||
- {from: hasViewDefinition, to: viewDefinition, kind: attribute, order: 1}
|
||||
- {from: hasExpression, to: expression, kind: attribute, order: 1}
|
||||
- {from: hasBusinessRule, to: businessRule, kind: attribute, order: 1}
|
||||
- {from: hasExampleValue, to: exampleValue, kind: attribute, order: 1}
|
||||
- {from: createdOn, to: creationDate, kind: attribute, order: 1}
|
||||
- {from: lastReviewedOn, to: lastReviewDate, kind: attribute, order: 1}
|
||||
- {from: harvestedOn, to: harvestDate, kind: attribute, order: 1}
|
||||
- {from: lastQueriedOn, to: lastQueryDate, kind: attribute, order: 1}
|
||||
- {from: definedBy, to: definitionAddress, kind: attribute, order: 1}
|
||||
- {from: queryCount30d, to: queryCount, kind: attribute, order: 1}
|
||||
- {from: hasShortLabel, to: shortLabel, kind: annotation, order: 2}
|
||||
- {from: hasAcronym, to: acronym, kind: annotation, order: 2}
|
||||
- {from: hasIdentifier, to: identifier, kind: attribute, order: 3}
|
||||
- {from: hasName, to: canonicalName, kind: attribute, order: 3}
|
||||
- {from: hasStatus, to: status, kind: attribute, order: 3}
|
||||
- {from: hasVersion, to: version, kind: attribute, order: 3}
|
||||
|
||||
# --- relations that change nature (TN-016): object property -> datatype property
|
||||
- {from: hasActivationStatus, to: activationStatus, kind: relation_to_attribute, order: 1}
|
||||
- {from: deployedIn, to: environment, kind: relation_to_attribute, order: 1}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# MERGES — the source identifier disappears into an existing target.
|
||||
# Unlike a rename, the target already exists and keeps its own declaration.
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
merges:
|
||||
- from: biConsumes
|
||||
into: consumes
|
||||
widen_domain: true
|
||||
reason: >
|
||||
Same verb, same target, identical meaning. The domain widens and scope is
|
||||
controlled per class by SHACL. Ranges are compatible, so no RDFS retyping
|
||||
is introduced (TN-028).
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# STRUCTURAL OPERATIONS
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
structural:
|
||||
|
||||
reclassify:
|
||||
- {term: shortLabel, from: DatatypeProperty, to: AnnotationProperty, rule: TN-003}
|
||||
- {term: acronym, from: DatatypeProperty, to: AnnotationProperty, rule: TN-003}
|
||||
|
||||
create_classes:
|
||||
- term: GovernanceLayerObject
|
||||
parent: MetaModelObject
|
||||
is_abstract: true
|
||||
label: Governance Layer Object
|
||||
comment: >
|
||||
Root of the governance layer. Holds the actors and governance objects that
|
||||
are not data objects and therefore sit outside the data layers. Created by
|
||||
insertion above Actor rather than by renaming it, so that every range
|
||||
pointing at Actor keeps stating the nature of its target rather than its
|
||||
position in the model.
|
||||
rule: TN-027
|
||||
|
||||
reparent:
|
||||
- {term: Actor, parent: GovernanceLayerObject, rule: TN-027}
|
||||
- {term: PhysicalLayerObject, parent: MetaModelObject, rule: TN-027}
|
||||
|
||||
subproperties:
|
||||
- {term: primarilyStoredIn, parent: storedIn, rule: TN-028}
|
||||
|
||||
# TN-016: closed governance states become literals. The classes and their
|
||||
# individuals are withdrawn, and the properties become datatype properties.
|
||||
to_literal:
|
||||
- property: activationStatus
|
||||
domain: DataDomain
|
||||
drop_class: ActivationStatus
|
||||
drop_individuals: [NotActivated, LightActivation, FullActivation]
|
||||
value_map:
|
||||
NotActivated: NOT_ACTIVATED
|
||||
LightActivation: LIGHT_ACTIVATION
|
||||
FullActivation: FULL_ACTIVATION
|
||||
- property: environment
|
||||
domain: CapturedObject
|
||||
drop_class: Environment
|
||||
drop_individuals: [Development, UserAcceptance, Production]
|
||||
value_map:
|
||||
Development: DEVELOPMENT
|
||||
UserAcceptance: USER_ACCEPTANCE
|
||||
Production: PRODUCTION
|
||||
|
||||
# Phase clause: before v2.0 is published the project is in design, so
|
||||
# deprecated terms are removed outright rather than kept as stubs. EV-002
|
||||
# still applies — the count must be zero.
|
||||
drop_deprecated: true
|
||||
@@ -0,0 +1,375 @@
|
||||
# PERNOD RICARD DATA METAMODEL — T-BOX PROCESSBOOK
|
||||
# =============================================================================
|
||||
# SINGLE SOURCE OF TRUTH for the procedures. The BPMN 2.0 files, the Mermaid
|
||||
# views and the Markdown document are GENERATED from this file (EV-008).
|
||||
#
|
||||
# python3 generate_bpmn.py -> bpmn/<id>.bpmn (strict, queryable)
|
||||
# python3 generate_processbook_md.py -> PR_TBox_Processbook.md (Mermaid views)
|
||||
#
|
||||
# NODE TYPES
|
||||
# start | end events
|
||||
# userTask a human decides or writes; cannot be automated
|
||||
# scriptTask fully automated
|
||||
# callActivity invokes another procedure by its id
|
||||
# gateway exclusive decision; every outgoing flow is guarded
|
||||
#
|
||||
# Every node may carry `rules`, the identifiers of the rulebook rules it
|
||||
# enforces. That list is what makes the BPMN queryable and what feeds the
|
||||
# control.procedure field back into the rulebook.
|
||||
# =============================================================================
|
||||
|
||||
meta:
|
||||
title: Pernod Ricard Data MetaModel — T-Box Processbook
|
||||
version: "0.1"
|
||||
status: Draft for review
|
||||
date: "2026-08-03"
|
||||
scope: >
|
||||
Procedures for changing the vocabulary of the model. Each procedure names
|
||||
the rules it enforces, the steps a person must perform, and the points at
|
||||
which the work stops rather than continues.
|
||||
namespace: "https://ontology.pernod-ricard.com/process/"
|
||||
|
||||
families:
|
||||
Create: Bringing a new element into the vocabulary.
|
||||
Verify: Establishing that what exists conforms.
|
||||
Update: Changing an element that already exists.
|
||||
Delete: Removing an element from the vocabulary.
|
||||
Release: Propagating a change and publishing a version.
|
||||
|
||||
processes:
|
||||
|
||||
# ---------------------------------------------------------------------- X1
|
||||
- id: X1
|
||||
family: Release
|
||||
name: Propagate a change and publish a version
|
||||
scope: Any validated modification of the vocabulary.
|
||||
trigger: A change to the vocabulary has been agreed and is ready to apply.
|
||||
inputs: [the agreed change, the target version, the list of affected artifacts]
|
||||
outputs: [a merged branch, a tagged version, an execution log]
|
||||
note: >
|
||||
The most frequently invoked procedure of the processbook: every other
|
||||
procedure ends by calling it. Two of its steps are gateways rather than
|
||||
checks, because an idempotence failure and a validation failure must stop
|
||||
the work rather than be noted in passing.
|
||||
flow:
|
||||
- {id: start, type: start, name: Change agreed}
|
||||
- {id: branch, type: scriptTask, name: Create the migration branch, rules: [EV-009]}
|
||||
- {id: apply, type: scriptTask, name: Apply the change through an RDF parser, rules: [EV-005, EV-006]}
|
||||
- {id: labels, type: scriptTask, name: Regenerate labels by derivation, rules: [TN-023]}
|
||||
- {id: abox, type: scriptTask, name: Propagate to the instances, rules: [EV-009]}
|
||||
- {id: shapes, type: scriptTask, name: Update the shapes and align conformsTo, rules: [EV-010]}
|
||||
- {id: artifacts, type: scriptTask, name: Regenerate every derived artifact, rules: [EV-008]}
|
||||
- {id: replay, type: scriptTask, name: Replay the chain a second time, rules: [EV-006, EV-007]}
|
||||
- {id: idem, type: gateway, name: "Second run reports zero change?"}
|
||||
- {id: verify_voc, type: callActivity, calls: R1, name: Verify the vocabulary}
|
||||
- {id: verify_abox, type: callActivity, calls: R2, name: Validate the instances}
|
||||
- {id: log, type: scriptTask, name: Write the execution log for this attempt, rules: [EV-015]}
|
||||
- {id: clean, type: gateway, name: "Any violation?"}
|
||||
- {id: decide, type: userTask, name: Correct or abandon, rules: [EV-004, EV-007]}
|
||||
- {id: drop, type: scriptTask, name: Destroy the branch and restart from the published version, rules: [EV-007]}
|
||||
- {id: review, type: userTask, name: Review the merge, rules: [EV-004, EV-009]}
|
||||
- {id: merge, type: scriptTask, name: Merge as a block, tag, push the tag separately, rules: [EV-009]}
|
||||
- {id: bump, type: scriptTask, name: Increment the ontology version, rules: [EV-014]}
|
||||
- {id: end, type: end, name: Version published}
|
||||
- {id: aborted, type: end, name: Change abandoned}
|
||||
flows:
|
||||
- {from: start, to: branch}
|
||||
- {from: branch, to: apply}
|
||||
- {from: apply, to: labels}
|
||||
- {from: labels, to: abox}
|
||||
- {from: abox, to: shapes}
|
||||
- {from: shapes, to: artifacts}
|
||||
- {from: artifacts, to: replay}
|
||||
- {from: replay, to: idem}
|
||||
- {from: idem, to: verify_voc, condition: "zero change"}
|
||||
- {from: idem, to: decide, condition: "the chain is not replayable"}
|
||||
- {from: verify_voc, to: verify_abox}
|
||||
- {from: verify_abox, to: log}
|
||||
- {from: log, to: clean}
|
||||
- {from: clean, to: review, condition: "none"}
|
||||
- {from: clean, to: decide, condition: "at least one"}
|
||||
- {from: decide, to: apply, condition: correct}
|
||||
- {from: decide, to: drop, condition: abandon}
|
||||
- {from: drop, to: aborted}
|
||||
- {from: review, to: merge}
|
||||
- {from: merge, to: bump}
|
||||
- {from: bump, to: end}
|
||||
|
||||
# ---------------------------------------------------------------------- C1
|
||||
- id: C1
|
||||
family: Create
|
||||
name: Create a term
|
||||
scope: class | relation | attribute | annotation
|
||||
parameter: >
|
||||
The nature of the term selects the naming rules applied at the naming step:
|
||||
a class takes TN-001, TN-002, TN-004, TN-005, TN-006, TN-007 and TN-027; a
|
||||
relation takes TN-001, TN-002, TN-009, TN-010, TN-011 and TN-028; an
|
||||
attribute takes TN-001, TN-002, TN-004, TN-012, TN-013, TN-014 and TN-015;
|
||||
an annotation takes TN-001, TN-002 and TN-003.
|
||||
trigger: A missing concept, edge or field has been identified.
|
||||
inputs: [the intended meaning, the nature, the target layer, the provenance, the parent or the domain and range]
|
||||
outputs: [a declared term, a published version]
|
||||
note: >
|
||||
The first step has no tool today. Checking that no existing term already
|
||||
covers the need is the semantic uniqueness control that SHACL cannot
|
||||
perform, and it stays a human task until R3 exists.
|
||||
flow:
|
||||
- {id: start, type: start, name: Need identified}
|
||||
- {id: unique, type: userTask, name: Check that no existing term covers the need}
|
||||
- {id: exists, type: gateway, name: "A term already covers it?"}
|
||||
- {id: reuse, type: end, name: Reuse the existing term}
|
||||
- {id: name, type: scriptTask, name: Name the term according to its nature, rules: [TN-001, TN-002, TN-003, TN-004, TN-005, TN-006, TN-009, TN-010, TN-011, TN-012, TN-013, TN-014, TN-015, TN-028]}
|
||||
- {id: label, type: scriptTask, name: Derive the label, rules: [TN-018, TN-019, TN-020, TN-021, TN-023]}
|
||||
- {id: short, type: userTask, name: Decide the short label and the acronym, rules: [TN-022]}
|
||||
- {id: comment, type: userTask, name: Write the comment, rules: [TN-024]}
|
||||
- {id: declare, type: scriptTask, name: Declare typing, provenance and attachment, rules: [TN-007, TN-025, TN-026, TN-027]}
|
||||
- {id: validate, type: userTask, name: Validate before applying, rules: [EV-004]}
|
||||
- {id: check, type: callActivity, calls: R1, name: Verify against the rulebook}
|
||||
- {id: release, type: callActivity, calls: X1, name: Propagate and publish}
|
||||
- {id: end, type: end, name: Term available}
|
||||
flows:
|
||||
- {from: start, to: unique}
|
||||
- {from: unique, to: exists}
|
||||
- {from: exists, to: reuse, condition: "yes"}
|
||||
- {from: exists, to: name, condition: "no"}
|
||||
- {from: name, to: label}
|
||||
- {from: label, to: short}
|
||||
- {from: short, to: comment}
|
||||
- {from: comment, to: declare}
|
||||
- {from: declare, to: validate}
|
||||
- {from: validate, to: check}
|
||||
- {from: check, to: release}
|
||||
- {from: release, to: end}
|
||||
|
||||
# ---------------------------------------------------------------------- C2
|
||||
- id: C2
|
||||
family: Create
|
||||
name: Create a controlled value
|
||||
scope: typed individual | literal
|
||||
parameter: >
|
||||
The form is not a style choice but the outcome of the first decision. The
|
||||
two branches have different consequences: the literal branch edits the
|
||||
shapes and therefore the vocabulary, so it falls under the version freeze;
|
||||
the individual branch touches nothing else.
|
||||
trigger: A new value is needed in a controlled set.
|
||||
inputs: [the value, the set it belongs to, whether a domain may add others]
|
||||
outputs: [a declared value, a published version]
|
||||
flow:
|
||||
- {id: start, type: start, name: New value needed}
|
||||
- {id: decide, type: userTask, name: "Must it be defined, owned or extended by a domain?", rules: [TN-016]}
|
||||
- {id: form, type: gateway, name: "Which form?"}
|
||||
- {id: indiv, type: scriptTask, name: Create the typed individual and derive its label, rules: [TN-001, TN-018, TN-022]}
|
||||
- {id: literal, type: scriptTask, name: Write the literal in upper snake case, rules: [TN-017]}
|
||||
- {id: shape, type: callActivity, calls: C3, name: Extend the closed list in the shapes}
|
||||
- {id: validate, type: userTask, name: Validate before applying, rules: [EV-004]}
|
||||
- {id: check, type: callActivity, calls: R1, name: Verify against the rulebook}
|
||||
- {id: release, type: callActivity, calls: X1, name: Propagate and publish}
|
||||
- {id: end, type: end, name: Value available}
|
||||
flows:
|
||||
- {from: start, to: decide}
|
||||
- {from: decide, to: form}
|
||||
- {from: form, to: indiv, condition: "extensible by a domain"}
|
||||
- {from: form, to: literal, condition: "closed governance state"}
|
||||
- {from: indiv, to: validate}
|
||||
- {from: literal, to: shape}
|
||||
- {from: shape, to: validate}
|
||||
- {from: validate, to: check}
|
||||
- {from: check, to: release}
|
||||
- {from: release, to: end}
|
||||
|
||||
# ---------------------------------------------------------------------- C3
|
||||
- id: C3
|
||||
family: Create
|
||||
name: Create a shape
|
||||
scope: Any SHACL constraint added to the validation set.
|
||||
trigger: A rule needs an executor, or a new class enters the validation perimeter.
|
||||
inputs: [the rule to enforce, the target class or property]
|
||||
outputs: [a shape, a rulebook entry naming it as executor]
|
||||
note: >
|
||||
The step that is forgotten is the last one before release: recording the
|
||||
shape as the executor of its rule. Without it a rule stays declared
|
||||
blocking with nothing enforcing it, which EV-011 forbids.
|
||||
flow:
|
||||
- {id: start, type: start, name: Constraint needed}
|
||||
- {id: write, type: userTask, name: Write the constraint}
|
||||
- {id: pattern, type: scriptTask, name: Check that every pattern is expressed positively, rules: [EV-013]}
|
||||
- {id: portable, type: gateway, name: "Expressible within the specification?"}
|
||||
- {id: demote, type: userTask, name: Move the rule to the script tier, rules: [EV-012]}
|
||||
- {id: conforms, type: scriptTask, name: Align the declared ontology version, rules: [EV-010]}
|
||||
- {id: test, type: callActivity, calls: R2, name: Test against real instances}
|
||||
- {id: record, type: userTask, name: Record the shape as the executor of its rule, rules: [EV-011, EV-012]}
|
||||
- {id: release, type: callActivity, calls: X1, name: Propagate and publish}
|
||||
- {id: end, type: end, name: Shape in force}
|
||||
flows:
|
||||
- {from: start, to: write}
|
||||
- {from: write, to: pattern}
|
||||
- {from: pattern, to: portable}
|
||||
- {from: portable, to: conforms, condition: "yes"}
|
||||
- {from: portable, to: demote, condition: "no"}
|
||||
- {from: demote, to: record}
|
||||
- {from: conforms, to: test}
|
||||
- {from: test, to: record}
|
||||
- {from: record, to: release}
|
||||
- {from: release, to: end}
|
||||
|
||||
# ---------------------------------------------------------------------- D1
|
||||
- id: D1
|
||||
family: Delete
|
||||
name: Deprecate a term
|
||||
scope: class | relation | attribute | annotation | individual
|
||||
trigger: A term is superseded or no longer needed.
|
||||
inputs: [the term, its replacement if any]
|
||||
outputs: [a deprecated stub, a published version]
|
||||
note: >
|
||||
The count at step two is informative, not a condition: a term may be
|
||||
deprecated whether or not it is instantiated. It becomes a condition only
|
||||
in D2.
|
||||
flow:
|
||||
- {id: start, type: start, name: Term superseded}
|
||||
- {id: replacement, type: userTask, name: Identify the replacement, or record that there is none}
|
||||
- {id: count, type: scriptTask, name: Count the instances for information, rules: [EV-015]}
|
||||
- {id: mark, type: scriptTask, name: Mark deprecated and declare the replacement, rules: [EV-001]}
|
||||
- {id: strip, type: scriptTask, name: Strip the stub of every edge, rules: [EV-001]}
|
||||
- {id: label, type: scriptTask, name: Remove any mention of state from the label, rules: [TN-020]}
|
||||
- {id: release, type: callActivity, calls: X1, name: Propagate and publish}
|
||||
- {id: end, type: end, name: Term deprecated}
|
||||
flows:
|
||||
- {from: start, to: replacement}
|
||||
- {from: replacement, to: count}
|
||||
- {from: count, to: mark}
|
||||
- {from: mark, to: strip}
|
||||
- {from: strip, to: label}
|
||||
- {from: label, to: release}
|
||||
- {from: release, to: end}
|
||||
|
||||
# ---------------------------------------------------------------------- D2
|
||||
- id: D2
|
||||
family: Delete
|
||||
name: Withdraw a term permanently
|
||||
scope: class | relation | attribute | annotation | individual
|
||||
trigger: A deprecated term is to be removed from the vocabulary.
|
||||
inputs: [the deprecated term]
|
||||
outputs: [a vocabulary without the term, a proof of non-instantiation, a published version]
|
||||
note: >
|
||||
The reference cleaning step is what prevents phantom nodes: a subject
|
||||
removed while its identifier is still cited elsewhere is reconstructed by
|
||||
inference, present in traversals and absent from every control. It is a step
|
||||
of the procedure, not a check at the end of a script.
|
||||
flow:
|
||||
- {id: start, type: start, name: Withdrawal requested}
|
||||
- {id: count, type: scriptTask, name: Run the counting query, rules: [EV-002, EV-015]}
|
||||
- {id: instantiated, type: gateway, name: "Any instance found?"}
|
||||
- {id: keep, type: end, name: Kept deprecated}
|
||||
- {id: proof, type: scriptTask, name: Attach the proof to the commit, rules: [EV-002, EV-015]}
|
||||
- {id: remove, type: scriptTask, name: Remove the subject block}
|
||||
- {id: refs, type: scriptTask, name: Remove every reference naming the subject, rules: [EV-003]}
|
||||
- {id: check_voc, type: callActivity, calls: R1, name: Verify the vocabulary}
|
||||
- {id: check_abox, type: callActivity, calls: R2, name: Validate the instances}
|
||||
- {id: release, type: callActivity, calls: X1, name: Propagate and publish}
|
||||
- {id: end, type: end, name: Term withdrawn}
|
||||
flows:
|
||||
- {from: start, to: count}
|
||||
- {from: count, to: instantiated}
|
||||
- {from: instantiated, to: keep, condition: "yes"}
|
||||
- {from: instantiated, to: proof, condition: "no"}
|
||||
- {from: proof, to: remove}
|
||||
- {from: remove, to: refs}
|
||||
- {from: refs, to: check_voc}
|
||||
- {from: check_voc, to: check_abox}
|
||||
- {from: check_abox, to: release}
|
||||
- {from: release, to: end}
|
||||
|
||||
# ---------------------------------------------------------------------- R1
|
||||
- id: R1
|
||||
family: Verify
|
||||
name: Verify the vocabulary against the rulebook
|
||||
scope: The whole vocabulary, or the subset touched by a change.
|
||||
trigger: A term has been created, changed or withdrawn, or a release is prepared.
|
||||
inputs: [the vocabulary, the rulebook source]
|
||||
outputs: [a conformance report]
|
||||
note: >
|
||||
Two tiers run in sequence, not in parallel: the script settles everything
|
||||
mechanisable, and a person answers only for the rules no script can judge.
|
||||
Reversing the order wastes review time on findings the script would have
|
||||
caught.
|
||||
flow:
|
||||
- {id: start, type: start, name: Verification requested}
|
||||
- {id: script, type: scriptTask, name: "Run the naming and declaration checks", rules: [TN-001, TN-002, TN-003, TN-004, TN-005, TN-006, TN-007, TN-009, TN-010, TN-011, TN-012, TN-013, TN-014, TN-015, TN-017, TN-018, TN-019, TN-020, TN-021, TN-023, TN-025, TN-026, TN-027, TN-028, EV-014]}
|
||||
- {id: mechanised, type: gateway, name: "Any mechanised violation?"}
|
||||
- {id: report_fail, type: end, name: Report returned with violations}
|
||||
- {id: human, type: userTask, name: "Review the rules no script can judge", rules: [TN-008, TN-016, TN-022, TN-024]}
|
||||
- {id: judged, type: gateway, name: "Reviewer raises an issue?"}
|
||||
- {id: report_ok, type: end, name: Vocabulary conforms}
|
||||
flows:
|
||||
- {from: start, to: script}
|
||||
- {from: script, to: mechanised}
|
||||
- {from: mechanised, to: report_fail, condition: "at least one"}
|
||||
- {from: mechanised, to: human, condition: none}
|
||||
- {from: human, to: judged}
|
||||
- {from: judged, to: report_fail, condition: "yes"}
|
||||
- {from: judged, to: report_ok, condition: "no"}
|
||||
|
||||
# ---------------------------------------------------------------------- R2
|
||||
- id: R2
|
||||
family: Verify
|
||||
name: Validate instances against the shapes
|
||||
scope: Any instance graph in the validation perimeter.
|
||||
trigger: Instances have changed, shapes have changed, or a release is prepared.
|
||||
inputs: [the instance graph, the shapes, the ontology]
|
||||
outputs: [a validation report]
|
||||
note: >
|
||||
The version check comes first and aborts rather than warns. Validating
|
||||
against shapes that target another version of the ontology does not fail
|
||||
loudly: it returns a long list of violations that reads exactly like a
|
||||
regression of the model.
|
||||
flow:
|
||||
- {id: start, type: start, name: Validation requested}
|
||||
- {id: conforms, type: scriptTask, name: "Compare the declared target version with the ontology version", rules: [EV-010]}
|
||||
- {id: match, type: gateway, name: "Versions match?"}
|
||||
- {id: abort, type: end, name: Aborted on version mismatch}
|
||||
- {id: run, type: scriptTask, name: Run the shape validation}
|
||||
- {id: violations, type: gateway, name: "Any violation?"}
|
||||
- {id: fail, type: end, name: Report returned with violations}
|
||||
- {id: ok, type: end, name: Instances conform}
|
||||
flows:
|
||||
- {from: start, to: conforms}
|
||||
- {from: conforms, to: match}
|
||||
- {from: match, to: abort, condition: "no"}
|
||||
- {from: match, to: run, condition: "yes"}
|
||||
- {from: run, to: violations}
|
||||
- {from: violations, to: fail, condition: "at least one"}
|
||||
- {from: violations, to: ok, condition: none}
|
||||
|
||||
# ---------------------------------------------------------------------- R3
|
||||
- id: R3
|
||||
family: Verify
|
||||
name: Audit what the shapes cannot see
|
||||
scope: The whole vocabulary and its source file.
|
||||
trigger: Periodic audit, or before a version is published.
|
||||
inputs: [the vocabulary, its source file]
|
||||
outputs: [a shortlist of suspected duplicates, a list of duplicated blocks]
|
||||
note: >
|
||||
Shape validation reads a graph, not a file and not meaning. Two identifiers
|
||||
standing for the same notion produce two individually valid graphs; a
|
||||
duplicated block of text produces identical triples and no complaint. This
|
||||
procedure is the tier those rules fall to, and its output is a shortlist for
|
||||
a person rather than a verdict.
|
||||
flow:
|
||||
- {id: start, type: start, name: Audit requested}
|
||||
- {id: index, type: scriptTask, name: "Build the normalised label index", rules: [EV-012]}
|
||||
- {id: hash, type: scriptTask, name: "Hash every normalised subject block", rules: [EV-012]}
|
||||
- {id: shortlist, type: gateway, name: "Any candidate found?"}
|
||||
- {id: clean, type: end, name: Nothing to arbitrate}
|
||||
- {id: review, type: userTask, name: Arbitrate each candidate}
|
||||
- {id: act, type: gateway, name: "Duplication confirmed?"}
|
||||
- {id: merge, type: end, name: Referred to the merge procedure}
|
||||
- {id: dismissed, type: end, name: Candidates dismissed}
|
||||
flows:
|
||||
- {from: start, to: index}
|
||||
- {from: index, to: hash}
|
||||
- {from: hash, to: shortlist}
|
||||
- {from: shortlist, to: clean, condition: none}
|
||||
- {from: shortlist, to: review, condition: "at least one"}
|
||||
- {from: review, to: act}
|
||||
- {from: act, to: merge, condition: "yes"}
|
||||
- {from: act, to: dismissed, condition: "no"}
|
||||
@@ -0,0 +1,3 @@
|
||||
|
||||
PyYAML>=6.0,<7
|
||||
openpyxl>=3.1,<3.2
|
||||
File diff suppressed because it is too large
Load Diff
+974
-943
File diff suppressed because it is too large
Load Diff
+120
-56
@@ -7,11 +7,33 @@
|
||||
@prefix pr: <https://ontology.pernod-ricard.com/metamodel/> .
|
||||
|
||||
#################################################################
|
||||
# PERNOD RICARD DATA META MODEL - T-Box v1.5
|
||||
# PERNOD RICARD DATA META MODEL - T-Box v1.6
|
||||
# Owner: Bastien Gourdon - Data Governance Office
|
||||
#
|
||||
# WHAT CHANGED vs v1.4
|
||||
# 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
|
||||
@@ -219,12 +241,12 @@
|
||||
|
||||
<https://ontology.pernod-ricard.com/metamodel/>
|
||||
a owl:Ontology ;
|
||||
owl:versionIRI <https://ontology.pernod-ricard.com/metamodel/1.5> ;
|
||||
owl:priorVersion <https://ontology.pernod-ricard.com/metamodel/1.4> ;
|
||||
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-30"^^xsd:date ;
|
||||
owl:versionInfo "1.5.0-draft-2026-07-30" ;
|
||||
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.""" .
|
||||
|
||||
|
||||
@@ -257,6 +279,10 @@ pr:harvestSource a owl:AnnotationProperty ;
|
||||
rdfs:label "harvest source" ;
|
||||
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: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'." .
|
||||
@@ -269,13 +295,15 @@ pr:authoringMode 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." ;
|
||||
pr:hasShortLabel "Meta model object" .
|
||||
pr:hasShortLabel "Meta model object" ;
|
||||
pr:isAbstract true .
|
||||
|
||||
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." .
|
||||
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:CuratedObject a owl:Class ;
|
||||
rdfs:label "Curated Object (deprecated)" ;
|
||||
@@ -286,49 +314,67 @@ 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." .
|
||||
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:DefinedObject ;
|
||||
rdfs:label "Ownership & Categorization Layer Object" .
|
||||
pr:BusinessLayerObject a owl:Class ; rdfs:subClassOf pr:DefinedObject ;
|
||||
rdfs:label "Business Layer Object" .
|
||||
pr:LogicalLayerObject a owl:Class ; rdfs:subClassOf pr:DefinedObject ;
|
||||
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:DefinedObject ;
|
||||
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." .
|
||||
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 """The one layer whose members do NOT share a provenance, which is why this class -- unlike the other five layer roots -- says nothing about it.
|
||||
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. The two axes are meant to be orthogonal; that shortcut collapsed them.""" .
|
||||
pr:ConsumptionLayerObject a owl:Class ; rdfs:subClassOf pr:CapturedObject ;
|
||||
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." ;
|
||||
pr:hasShortLabel "Consumption" .
|
||||
pr:hasShortLabel "Consumption" ;
|
||||
pr:isAbstract true .
|
||||
|
||||
|
||||
#################################################################
|
||||
# LAYER 1 - OWNERSHIP & CATEGORIZATION (defined)
|
||||
#################################################################
|
||||
|
||||
pr:DataDomain a owl:Class ; rdfs:subClassOf pr:OwnershipLayerObject ;
|
||||
pr:DataDomain a owl:Class ; rdfs:subClassOf pr:OwnershipLayerObject , pr:DefinedObject ;
|
||||
rdfs:label "Data Domain" ;
|
||||
pr:hasShortLabel "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 ;
|
||||
pr:SubDomain a owl:Class ; rdfs:subClassOf pr:OwnershipLayerObject , pr:DefinedObject ;
|
||||
rdfs:label "Sub-Domain" ;
|
||||
pr:hasShortLabel "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." ;
|
||||
pr:hasShortLabel "Business object" .
|
||||
@@ -338,12 +384,12 @@ pr:BusinessObject a owl:Class ; rdfs:subClassOf pr:OwnershipLayerObject ;
|
||||
# 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)." ;
|
||||
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." ;
|
||||
pr:hasShortLabel "Metric" .
|
||||
@@ -358,12 +404,12 @@ pr:KPI a owl:Class ; rdfs:subClassOf pr:Metric ;
|
||||
# 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: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." ;
|
||||
@@ -380,17 +426,17 @@ pr:DataAsset a owl:Class ; rdfs:subClassOf pr:LogicalLayerObject ;
|
||||
# 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." ;
|
||||
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." ;
|
||||
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)." ;
|
||||
pr:hasShortLabel "Data contract" .
|
||||
@@ -409,12 +455,14 @@ pr:System a owl:Class ; rdfs:subClassOf pr:PhysicalLayerObject , pr:DefinedObjec
|
||||
pr:Database a owl:Class ; rdfs:subClassOf pr:PhysicalLayerObject , pr:CapturedObject ;
|
||||
rdfs:label "Database" ;
|
||||
pr:harvestSource "SNOWFLAKE.INFORMATION_SCHEMA.DATABASES" ;
|
||||
pr:hasShortLabel "Database" .
|
||||
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:CapturedObject ;
|
||||
rdfs:label "Schema" ;
|
||||
pr:harvestSource "SNOWFLAKE.INFORMATION_SCHEMA.SCHEMATA" ;
|
||||
pr:hasShortLabel "Schema" .
|
||||
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:DataStructure a owl:Class ; rdfs:subClassOf pr:PhysicalLayerObject , pr:CapturedObject ;
|
||||
rdfs:label "Data Structure" ;
|
||||
@@ -433,7 +481,8 @@ EXTENSION POINT for non-tabular storage. Nothing at Pernod Ricard is non-tabular
|
||||
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.""" .
|
||||
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)" ;
|
||||
@@ -441,7 +490,8 @@ pr:PhysicalRelation a owl:Class ; rdfs:subClassOf pr:PhysicalLayerObject , pr:Ca
|
||||
|
||||
pr:BaseTable a owl:Class ; rdfs:subClassOf pr:DataStructure ;
|
||||
rdfs:label "Base Table" ;
|
||||
pr:hasShortLabel "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" ;
|
||||
@@ -467,13 +517,23 @@ pr:Field a owl:Class ; rdfs:subClassOf pr:PhysicalLayerObject , pr:CapturedObjec
|
||||
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: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" ;
|
||||
@@ -486,24 +546,25 @@ pr:Transformation a owl:Class ; rdfs:subClassOf pr:PhysicalLayerObject , pr:Capt
|
||||
# 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:hasShortLabel "BI workspace" .
|
||||
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." ;
|
||||
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. 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." ;
|
||||
@@ -516,17 +577,18 @@ 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." ;
|
||||
pr:hasShortLabel "Actor" .
|
||||
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." ;
|
||||
pr:hasShortLabel "Domain owner" .
|
||||
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." ;
|
||||
pr:hasShortLabel "Sub-domain owner" .
|
||||
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" ;
|
||||
@@ -549,7 +611,8 @@ pr:DataSteward a owl:Class ; rdfs:subClassOf pr:Actor ;
|
||||
# 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" .
|
||||
@@ -560,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" .
|
||||
@@ -802,11 +866,11 @@ 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 ;
|
||||
@@ -1044,7 +1108,7 @@ pr:hasExpression a owl:DatatypeProperty ; rdfs:label "BI expression" ; rdfs:rang
|
||||
[] a owl:AllDisjointClasses ; owl:members (
|
||||
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 ) .
|
||||
|
||||
@@ -36,7 +36,7 @@ DOMAIN_STYLE = OrderedDict([
|
||||
|
||||
MULTI = {"composedOf", "usesConcept", "hasElement", "hasMetric", "measures",
|
||||
"computedBy", "qualifiedBy", "hasGrainElement", "monitoredBy", "hasSynonym",
|
||||
"hasUnit", "hasSource", "packages", "exposes"}
|
||||
"unit", "hasSource", "packages", "exposes"}
|
||||
|
||||
SUBJECT_DOMAIN = "DD_06" # the hub owner: everything else is a dependency
|
||||
|
||||
@@ -183,25 +183,25 @@ def build(subjects):
|
||||
nodes[subj] = {
|
||||
"id": subj,
|
||||
"kind": t,
|
||||
# the model carries both names now: hasShortLabel for screens,
|
||||
# hasName for the record. The viewer stops keeping its own list.
|
||||
# the model carries both names now: shortLabel for screens,
|
||||
# name for the record. The viewer stops keeping its own list.
|
||||
"label": LABELS.get(t, t),
|
||||
"shortName": rec.get("hasShortLabel", ""),
|
||||
"ident": rec.get("hasIdentifier", ""),
|
||||
"name": rec.get("hasName", subj.replace("ex:", "")),
|
||||
"shortName": rec.get("shortLabel", ""),
|
||||
"ident": rec.get("identifier", ""),
|
||||
"name": rec.get("name", subj.replace("ex:", "")),
|
||||
"domain": domain_of(subj, rec, subjects),
|
||||
"arb": rec.get("arbitrationStatus", "") == "TO_ARBITRATE",
|
||||
"status": rec.get("hasStatus", ""),
|
||||
"definition": rec.get("hasBusinessDefinition", ""),
|
||||
"rule": rec.get("hasBusinessRule", ""),
|
||||
"formula": rec.get("hasFormula", ""),
|
||||
"units": as_list(rec.get("hasUnit")),
|
||||
"status": rec.get("status", ""),
|
||||
"definition": rec.get("businessDefinition", ""),
|
||||
"rule": rec.get("businessRule", ""),
|
||||
"formula": rec.get("formula", ""),
|
||||
"units": as_list(rec.get("unit")),
|
||||
"physical": rec.get("physicalName", ""),
|
||||
"format": rec.get("hasFormat", ""),
|
||||
"format": rec.get("format", ""),
|
||||
"source": ", ".join(as_list(rec.get("hasSource"))),
|
||||
"sourcedFrom": rec.get("sourcedFrom", ""),
|
||||
"fqn": "",
|
||||
"version": rec.get("hasVersion", ""),
|
||||
"version": rec.get("version", ""),
|
||||
"operatedBy": rec.get("operatedBy", ""),
|
||||
"governedBy": rec.get("governedBy", ""),
|
||||
"productOwner": rec.get("hasProductOwner", ""),
|
||||
|
||||
@@ -34,7 +34,8 @@ WANTED = {
|
||||
"owl:inverseOf": "inverseOf", "owl:deprecated": "deprecated",
|
||||
"dcterms:isReplacedBy": "isReplacedBy",
|
||||
"pr:harvestSource": "harvestSource", "pr:authoringMode": "authoringMode",
|
||||
"pr:hasAcronym": "acronym", "pr:hasShortLabel": "shortLabel",
|
||||
"pr:acronym": "acronym", "pr:shortLabel": "shortLabel",
|
||||
"pr:isAbstract": "isAbstract",
|
||||
}
|
||||
|
||||
LAYER_ROOT = OrderedDict([
|
||||
@@ -200,7 +201,6 @@ def build(subjects):
|
||||
return out
|
||||
|
||||
parents = {c: first(classes[c], "subClassOf") for c in classes}
|
||||
has_kids = set(parents.values())
|
||||
|
||||
nodes = {}
|
||||
for term, rec in classes.items():
|
||||
@@ -234,8 +234,10 @@ def build(subjects):
|
||||
"acronym": first(rec, "acronym"),
|
||||
"deprecated": first(rec, "deprecated") in ("true", "True"),
|
||||
"replacedBy": first(rec, "isReplacedBy"),
|
||||
"abstract": term in has_kids and term not in LAYER_ROOT
|
||||
and term not in ("pr:MetaModelObject",),
|
||||
# read, not guessed: inferring it from "has subclasses" flagged
|
||||
# Metric as abstract, and Metric is one of the most instantiated
|
||||
# classes in the model
|
||||
"abstract": first(rec, "isAbstract") in ("true", "True"),
|
||||
"members": [{"id": m, "label": first(subjects.get(m, {}), "label",
|
||||
m.split(":")[1])}
|
||||
for m in individuals.get(term, [])],
|
||||
@@ -495,13 +497,13 @@ footer{background:var(--navy);color:rgba(255,255,255,.5);padding:7px 20px;
|
||||
<header>
|
||||
<div>
|
||||
<h1>PR Data Meta Model · T-Box</h1>
|
||||
<div class="sub">Six layers, crossed with what governance asserts and what a harvester observes</div>
|
||||
<div class="sub">Six layers, crossed with what governance defines and what a harvester captures</div>
|
||||
</div>
|
||||
<div class="tabs" role="tablist">
|
||||
<button id="t-scope" class="on">Overview</button>
|
||||
<button id="t-model">Layers</button>
|
||||
<button id="t-graph">Graph</button>
|
||||
<button id="t-props">Relations</button>
|
||||
<button id="t-props">Properties</button>
|
||||
<button id="t-dep">Deprecated</button>
|
||||
</div>
|
||||
</header>
|
||||
@@ -549,18 +551,22 @@ var SCOPED_PAIRS = [
|
||||
["pr:owningDomain", "pr:DefinedObject", "pr:DataDomain", "owning domain"],
|
||||
["pr:ownedBy", "pr:DefinedObject", "pr:Actor", "owned by"]
|
||||
];
|
||||
var ORDER = ["pr:MetaModelObject", "pr:DefinedObject", "pr:CapturedObject",
|
||||
var ORDER = [
|
||||
"pr:MetaModelObject", "pr:DefinedObject", "pr:CapturedObject",
|
||||
"pr:OwnershipLayerObject", "pr:DataDomain", "pr:SubDomain", "pr:BusinessObject",
|
||||
"pr:BusinessLayerObject", "pr:BusinessConcept", "pr:Metric", "pr:KPI",
|
||||
"pr:LogicalLayerObject", "pr:DataObject", "pr:DataElement",
|
||||
"pr:DeliveryLayerObject", "pr:DataProduct", "pr:DataContract", "pr:DataInterface",
|
||||
"pr:PhysicalLayerObject", "pr:System", "pr:Database", "pr:Schema",
|
||||
"pr:DataStructure", "pr:BaseTable", "pr:View", "pr:ExternalTable", "pr:Field",
|
||||
"pr:KeyConstraint", "pr:PrimaryKey", "pr:ForeignKeyConstraint", "pr:Transformation",
|
||||
"pr:KeyConstraint", "pr:PrimaryKey", "pr:ForeignKey", "pr:Transformation",
|
||||
"pr:ConsumptionLayerObject", "pr:BIWorkspace", "pr:BIDataSource", "pr:BIField",
|
||||
"pr:BIReport"];
|
||||
var LANES = [["defined","Defined \u00b7 someone decided it","#1E4B9B"],
|
||||
["captured","Captured \u00b7 read from a running system","#8A6A28"],
|
||||
"pr:BIReport",
|
||||
"pr:Actor", "pr:DataDomainOwner", "pr:SubDomainOwner", "pr:DataGovernanceLead",
|
||||
"pr:DataSteward", "pr:DataProductOwner"
|
||||
];
|
||||
var LANES = [["defined","Defined","#1E4B9B"],
|
||||
["captured","Captured","#8A6A28"],
|
||||
["structural","Structural","#6B6B6B"]];
|
||||
function live(a){ return a.filter(function(x){ return !x.deprecated; }); }
|
||||
function layerTitle(id){
|
||||
@@ -726,7 +732,11 @@ function renderGraph(){
|
||||
stroke:"#D8D4C8", "stroke-width":1}));
|
||||
var t = el("text", {x:l.x+l.w/2, y:-16, "text-anchor":"middle",
|
||||
class:"lanelabel", fill:l.colour});
|
||||
t.textContent = l.label;
|
||||
/* clipped to the column: a label wider than its lane overlapped the
|
||||
neighbouring one, which read as though it belonged to both */
|
||||
var cap = Math.floor(l.w / 6.4);
|
||||
t.textContent = l.label.length > cap ? l.label.slice(0, cap-1) + "\u2026"
|
||||
: l.label;
|
||||
bL.appendChild(t);
|
||||
});
|
||||
M.layers.forEach(function(b){
|
||||
@@ -776,9 +786,9 @@ function renderGraph(){
|
||||
specialisations a shape, never instantiated */
|
||||
g.appendChild(el("rect",{width:NW, height:NH, rx:3, fill:"#FFFFFF",
|
||||
stroke:c, "stroke-dasharray": n.n.abstract ? "5 3" : "none"}));
|
||||
var lines = wrapText(n.n.shortLabel || n.n.label, 21);
|
||||
var lines = wrapText(n.n.shortLabel || n.n.label, 20);
|
||||
lines.forEach(function(t,i){
|
||||
var e = el("text",{x:NW/2, y:NH/2+3.5+(i-(lines.length-1)/2)*10, fill:"#1A1A1A"});
|
||||
var e = el("text",{x:NW/2, y:NH/2+3+(i-(lines.length-1)/2)*9.5, fill:"#1A1A1A"});
|
||||
e.textContent = t; g.appendChild(e);
|
||||
});
|
||||
nL.appendChild(g);
|
||||
@@ -789,13 +799,27 @@ function renderGraph(){
|
||||
document.getElementById("g-count").textContent =
|
||||
G.nodes.length+" classes \u00b7 "+G.edges.length+" edges \u00b7 "+cd+" seam";
|
||||
}
|
||||
function wrapText(s,max){
|
||||
var w=s.split(" "), out=[], cur="";
|
||||
w.forEach(function(x){
|
||||
if((cur+" "+x).trim().length>max){ if(cur) out.push(cur); cur=x; }
|
||||
else cur=(cur+" "+x).trim(); });
|
||||
/* A node box is NW wide and holds at most three lines. A word longer than a
|
||||
line is cut rather than allowed to run past the border, and anything beyond
|
||||
the third line is elided -- overflowing text overlapped the neighbouring
|
||||
column, which is worse than an ellipsis. */
|
||||
function wrapText(s, max){
|
||||
var out = [], cur = "";
|
||||
s.split(" ").forEach(function(w){
|
||||
while(w.length > max){
|
||||
if(cur){ out.push(cur); cur = ""; }
|
||||
out.push(w.slice(0, max - 1) + "\u2011");
|
||||
w = w.slice(max - 1);
|
||||
}
|
||||
if((cur + " " + w).trim().length > max){ if(cur) out.push(cur); cur = w; }
|
||||
else cur = (cur + " " + w).trim();
|
||||
});
|
||||
if(cur) out.push(cur);
|
||||
return out.slice(0,2);
|
||||
if(out.length > 3){
|
||||
out = out.slice(0, 3);
|
||||
out[2] = out[2].slice(0, Math.max(0, max - 1)) + "\u2026";
|
||||
}
|
||||
return out;
|
||||
}
|
||||
function applyHot(){
|
||||
var id = G.sel || G.hot;
|
||||
@@ -904,16 +928,21 @@ function renderModel(){
|
||||
M.layers.forEach(function(l){
|
||||
var g=live(M.nodes).filter(function(n){return n.layer===l.id;});
|
||||
if(!g.length) return;
|
||||
/* ORDER alone, never depth first: sorting by depth grouped all the
|
||||
parents together and pushed Base table, View and External table below
|
||||
Field and Key constraint, away from the Data Structure they specialise. */
|
||||
g.sort(function(a,b){
|
||||
return a.depth - b.depth
|
||||
|| ((ORDER.indexOf(a.id)+1)||99) - ((ORDER.indexOf(b.id)+1)||99)
|
||||
return ((ORDER.indexOf(a.id)+1)||99) - ((ORDER.indexOf(b.id)+1)||99)
|
||||
|| a.depth - b.depth
|
||||
|| a.label.localeCompare(b.label); });
|
||||
h.push('<h2 class="sec" style="color:'+provColour(g[0].prov)+'">'+esc(l.title)+' \u00b7 '+esc(l.tag)+'</h2>');
|
||||
h.push('<table><thead><tr><th>Class</th><th>Provenance</th><th class="hide-s">Type of</th>'
|
||||
+'<th class="hide-s">What it is for</th></tr></thead><tbody>');
|
||||
g.forEach(function(n){
|
||||
h.push('<tr data-id="'+n.id+'"><td><span class="dot" style="background:'+provColour(n.prov)
|
||||
+'"></span><b>'+esc(n.label)+'</b>'+(n.abstract?' '+pill("abstract"):'')+'</td>'
|
||||
h.push('<tr data-id="'+n.id+'"><td style="padding-left:'+(11+n.depth*16)+'px">'
|
||||
+'<span class="dot" style="background:'+provColour(n.prov)
|
||||
+'"></span><b>'+esc(n.shortLabel||n.label)+'</b>'
|
||||
+(n.abstract?' '+pill("abstract"):'')+'</td>'
|
||||
+'<td class="mono">'+n.prov+'</td><td class="hide-s mono">'
|
||||
+(byId[n.parent]?esc(clean(byId[n.parent].label)):"\u2014")+'</td>'
|
||||
+'<td class="hide-s">'+esc((n.comment||"").split(". ")[0])+'</td></tr>');
|
||||
@@ -925,7 +954,7 @@ function renderModel(){
|
||||
|
||||
/* ------------------------------------------------------------ RELATIONS */
|
||||
function renderProps(){
|
||||
var h=['<p class="lede">'+live(M.relations).length+' relations and '+live(M.attributes).length+' attributes.</p>'
|
||||
var h=['<p class="lede">'+live(M.relations).length+' relations, '+live(M.attributes).length+' attributes and '+M.bridges.length+' bridge properties.</p>'
|
||||
+'<p class="sublede">A property with no declared domain or range is deliberate: it serves several classes '
|
||||
+'with different targets, so the scope lives in the SHACL shapes instead, stated per class.</p>'];
|
||||
h.push('<div class="filters"><input id="q" type="search" placeholder="filter\u2026">'
|
||||
@@ -934,6 +963,14 @@ function renderProps(){
|
||||
+'</div><div id="tbl"></div>');
|
||||
return h.join("");
|
||||
}
|
||||
/* First sentence only: the table is for scanning, the card carries the full
|
||||
text. Splitting on ". " alone broke on abbreviations and on the multi-line
|
||||
comments, which start with a newline. */
|
||||
function firstSentence(t){
|
||||
t = (t||"").replace(/\s+/g," ").trim();
|
||||
var m = t.match(/^(.{0,150}?[.!?])(\s|$)/);
|
||||
return m ? m[1] : (t.length > 150 ? t.slice(0,150).replace(/\s\S*$/,"") + "\u2026" : t);
|
||||
}
|
||||
function propRows(){
|
||||
var all=M.relations.concat(M.attributes,M.bridges).filter(function(p){
|
||||
if(p.deprecated) return false;
|
||||
@@ -942,8 +979,9 @@ function propRows(){
|
||||
return true;
|
||||
});
|
||||
all.sort(function(a,b){ return a.kind.localeCompare(b.kind)||a.label.localeCompare(b.label); });
|
||||
var h=['<table><thead><tr><th>Property</th><th class="hide-s">From</th><th class="hide-s">To</th>'
|
||||
+'<th>Traits</th></tr></thead><tbody>'];
|
||||
var h=['<table><thead><tr><th>Property</th><th class="hide-s">From</th>'
|
||||
+'<th class="hide-s">To</th><th class="hide-s">Traits</th>'
|
||||
+'<th>What it is for</th></tr></thead><tbody>'];
|
||||
all.forEach(function(p){
|
||||
var t=[];
|
||||
if(p.functional) t.push("exactly one");
|
||||
@@ -954,7 +992,9 @@ function propRows(){
|
||||
+'<td class="hide-s">'+(byId[p.domain]?esc(clean(byId[p.domain].label)):'<span class="mono">any</span>')+'</td>'
|
||||
+'<td class="hide-s">'+(byId[p.range]?esc(clean(byId[p.range].label))
|
||||
:'<span class="mono">'+esc(p.range||"\u2014")+'</span>')+'</td>'
|
||||
+'<td class="mono">'+(t.join(" \u00b7 ")||"\u2014")+'</td></tr>');
|
||||
+'<td class="hide-s mono">'+(t.join(" \u00b7 ")||"\u2014")+'</td>'
|
||||
+'<td>'+(p.comment ? esc(firstSentence(p.comment))
|
||||
: '<span class="mono">no description</span>')+'</td></tr>');
|
||||
});
|
||||
h.push('</tbody></table><p class="sublede" style="margin-top:12px">'+all.length+' shown.</p>');
|
||||
return h.join("");
|
||||
|
||||
@@ -53,19 +53,19 @@ prs:DefinedObjectShape a sh:NodeShape ;
|
||||
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 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 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,7 +76,7 @@ prs:DefinedObjectShape 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." ] .
|
||||
|
||||
@@ -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,7 +101,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" .
|
||||
FILTER NOT EXISTS { $this pr:owningDomain ?d }
|
||||
}""" ] .
|
||||
|
||||
@@ -223,7 +223,7 @@ 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." ] ;
|
||||
|
||||
@@ -242,13 +242,13 @@ 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." ] ;
|
||||
|
||||
@@ -262,13 +262,13 @@ 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: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." ] ;
|
||||
|
||||
@@ -331,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 }
|
||||
}""" ] .
|
||||
|
||||
@@ -413,7 +413,7 @@ prs:DataElementShape a sh:NodeShape ;
|
||||
sh:severity sh:Info ] ;
|
||||
|
||||
sh:property [
|
||||
sh:path pr:hasUnit ; sh:maxCount 1 ;
|
||||
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." ] ;
|
||||
|
||||
@@ -508,12 +508,12 @@ prs:BIFieldShape a sh:NodeShape ;
|
||||
prs:NamingShape a sh:NodeShape ;
|
||||
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