tbox: v2.2 - polymorphisme declare sur 19 proprietes, 17 domaines poses, provenance portee par les classes concretes, GovernanceLayerObject rattachee, affectations de gouvernance completes (12 completees, 10 SDO, 1 PO, 16 stewards par objet metier), Panel Coverage gouverne. Validation SHACL sans violation bloquante
This commit is contained in:
@@ -0,0 +1,488 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Enforce the naming and declaration rules of the T-Box rulebook.
|
||||
|
||||
USAGE
|
||||
python3 check_tbox_naming.py [--ontology PATH] [--json PATH] [--quiet]
|
||||
|
||||
Named as the executor of 25 rules. EV-011 forbids declaring a rule blocking
|
||||
without one, so this script is what makes those declarations true.
|
||||
|
||||
THREE VERDICTS
|
||||
VIOLATION the rule is settled mechanically and is broken -> exit 1
|
||||
REVIEW the checker cannot conclude; a person must -> exit 0
|
||||
PASS conforming
|
||||
|
||||
The middle verdict is the point. Without it the choice would be between
|
||||
blocking on false positives and staying silent on real problems. What the
|
||||
checker cannot decide alone comes from naming_lexicon.yaml; a word absent from
|
||||
the lexicon produces a REVIEW, never a VIOLATION.
|
||||
"""
|
||||
import argparse
|
||||
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__))
|
||||
RULES = os.path.join(HERE, "rules.yaml")
|
||||
LEXICON = os.path.join(HERE, "naming_lexicon.yaml")
|
||||
DEFAULT_ONTOLOGY = os.path.normpath(os.path.join(HERE, "..", "ontology", "pr_metamodel.ttl"))
|
||||
|
||||
NS = "https://ontology.pernod-ricard.com/metamodel/"
|
||||
PR = Namespace(NS)
|
||||
|
||||
VIOLATION, REVIEW = "VIOLATION", "REVIEW"
|
||||
|
||||
KINDS = OrderedDict([
|
||||
(OWL.Class, "class"),
|
||||
(OWL.ObjectProperty, "relation"),
|
||||
(OWL.DatatypeProperty, "attribute"),
|
||||
(OWL.AnnotationProperty, "annotation"),
|
||||
])
|
||||
|
||||
|
||||
# ------------------------------------------------------------------- findings
|
||||
|
||||
class Findings(object):
|
||||
def __init__(self):
|
||||
self.items = []
|
||||
|
||||
def add(self, verdict, rule, term, message):
|
||||
self.items.append({"verdict": verdict, "rule": rule,
|
||||
"term": term, "message": message})
|
||||
|
||||
def violation(self, rule, term, message):
|
||||
self.add(VIOLATION, rule, term, message)
|
||||
|
||||
def review(self, rule, term, message):
|
||||
self.add(REVIEW, rule, term, message)
|
||||
|
||||
@property
|
||||
def violations(self):
|
||||
return [i for i in self.items if i["verdict"] == VIOLATION]
|
||||
|
||||
@property
|
||||
def reviews(self):
|
||||
return [i for i in self.items if i["verdict"] == REVIEW]
|
||||
|
||||
|
||||
# -------------------------------------------------------------------- helpers
|
||||
|
||||
def local(uri):
|
||||
s = str(uri)
|
||||
return s[len(NS):] if s.startswith(NS) else None
|
||||
|
||||
|
||||
def tokens(name):
|
||||
"""Split a CamelCase local name. A run of capitals is one token: an acronym."""
|
||||
return re.findall(r"[A-Z]+(?![a-z])|[A-Z][a-z0-9]*|^[a-z0-9]+|[a-z0-9]+", name)
|
||||
|
||||
|
||||
def decamelise(name, is_class):
|
||||
"""TN-018. A run of capitals is kept together: it carries 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:]
|
||||
|
||||
|
||||
def acronyms_in(name, lex):
|
||||
"""Runs of two or more capitals, plus any known short form appearing whole.
|
||||
|
||||
The negative lookahead matters: without it, BIField yields BIF rather than
|
||||
BI, because the run swallows the initial capital of the word that follows.
|
||||
"""
|
||||
found = set(re.findall(r"[A-Z]{2,}(?![a-z])", name))
|
||||
for short in lex["known_acronyms"]:
|
||||
if re.search(r"(?:^|(?<=[a-z]))%s(?![a-z])" % re.escape(short), name):
|
||||
found.add(short)
|
||||
return sorted(found)
|
||||
|
||||
|
||||
def strip_case(text):
|
||||
return re.sub(r"[^a-z0-9]", "", (text or "").lower())
|
||||
|
||||
|
||||
# --------------------------------------------------------------------- checks
|
||||
|
||||
def provenance_axis(g, term):
|
||||
"""The provenance axis a class descends from, or None."""
|
||||
seen, stack = set(), [term]
|
||||
while stack:
|
||||
node = stack.pop()
|
||||
for parent in g.objects(node, RDFS.subClassOf):
|
||||
if isinstance(parent, URIRef) and parent not in seen:
|
||||
seen.add(parent)
|
||||
stack.append(parent)
|
||||
if PR.CapturedObject in seen:
|
||||
return "CapturedObject"
|
||||
if PR.DefinedObject in seen:
|
||||
return "DefinedObject"
|
||||
return None
|
||||
|
||||
|
||||
def check_identifier(g, terms, lex, f):
|
||||
"""TN-001 to TN-015 — the form of the IRI."""
|
||||
short_labels = {t: str(v) for t in terms
|
||||
for v in g.objects(t, PR.shortLabel)}
|
||||
allowed_acronyms = set()
|
||||
for value in short_labels.values():
|
||||
allowed_acronyms |= set(re.findall(r"[A-Z]{2,}", value))
|
||||
|
||||
for term, kind in terms.items():
|
||||
name = local(term)
|
||||
if name is None:
|
||||
continue
|
||||
|
||||
# TN-001 — CamelCase, no separator, correct initial
|
||||
if re.search(r"[^A-Za-z0-9]", name):
|
||||
f.violation("TN-001", name, "the local name carries a separator")
|
||||
expect_upper = kind in ("class", "individual")
|
||||
if name and name[0].isupper() != expect_upper:
|
||||
f.violation("TN-001", name,
|
||||
"a %s starts with %s case" %
|
||||
(kind, "upper" if expect_upper else "lower"))
|
||||
|
||||
# TN-004 — no digit
|
||||
if any(c.isdigit() for c in name):
|
||||
f.violation("TN-004", name, "a digit belongs to a value, not an identity")
|
||||
|
||||
# TN-002 — no acronym, except one reproduced from a target short label
|
||||
for a in acronyms_in(name, lex):
|
||||
if kind == "relation" and a in allowed_acronyms:
|
||||
continue # TN-011 exception
|
||||
expansion = lex["known_acronyms"].get(a)
|
||||
f.violation("TN-002", name, "carries the acronym %s%s"
|
||||
% (a, " (%s)" % expansion if expansion else ""))
|
||||
|
||||
if kind == "class":
|
||||
check_class_name(name, lex, f)
|
||||
elif kind == "relation":
|
||||
check_relation_name(g, term, name, lex, f)
|
||||
elif kind == "attribute":
|
||||
check_attribute_name(g, term, name, lex, f)
|
||||
|
||||
|
||||
def check_class_name(name, lex, f):
|
||||
"""TN-005 — a singular common noun, no type word."""
|
||||
if name in lex["type_word_exceptions"]:
|
||||
return
|
||||
for w in lex["type_words"]:
|
||||
if re.search(r"(?:^|(?<=[a-z]))%s(?![a-z])" % w, name):
|
||||
f.violation("TN-005", name,
|
||||
"carries the type word %s and is not a recorded abstraction" % w)
|
||||
last = tokens(name)[-1] if tokens(name) else name
|
||||
if last.endswith("s") and last not in lex["not_plural"]:
|
||||
f.review("TN-005", name, "%s looks plural; a class name is singular" % last)
|
||||
|
||||
|
||||
def check_relation_name(g, term, name, lex, f):
|
||||
"""TN-009, TN-010, TN-011 — verb first, is disambiguated, target named whole."""
|
||||
toks = tokens(name)
|
||||
head = toks[0] if toks else name
|
||||
|
||||
# TN-009 — an adverb is transparent, the verb follows
|
||||
if head in lex["adverbs"]:
|
||||
toks = toks[1:]
|
||||
head = toks[0].lower() if toks else head
|
||||
head = head.lower()
|
||||
|
||||
if head in lex["copula"]:
|
||||
pass # TN-010, copula use
|
||||
elif head in lex["active_verbs"] or head in lex["participles"]:
|
||||
pass
|
||||
else:
|
||||
f.review("TN-009", name,
|
||||
"%s is not a verb known to the lexicon; add it there or rename" % head)
|
||||
|
||||
# TN-011 — if the target class is named, it is named by its short label whole
|
||||
for rng in g.objects(term, RDFS.range):
|
||||
target = local(rng)
|
||||
if not target:
|
||||
continue
|
||||
short = next((str(v) for v in g.objects(rng, PR.shortLabel)), None)
|
||||
reference = strip_case(short) if short else strip_case(target)
|
||||
tail = strip_case("".join(toks[1:]))
|
||||
if not tail or tail == reference:
|
||||
continue
|
||||
if reference.endswith(tail) or tail in reference:
|
||||
f.violation("TN-011", name,
|
||||
"names its target by a truncation; %s expects %s"
|
||||
% (target, short or target))
|
||||
|
||||
|
||||
def check_attribute_name(g, term, name, lex, f):
|
||||
"""TN-012 to TN-015 — a noun, no relational suffix, booleans predicative."""
|
||||
ranges = [str(r) for r in g.objects(term, RDFS.range)]
|
||||
is_boolean = str(XSD.boolean) in ranges
|
||||
is_date = any(r in lex["date_ranges"] for r in ranges)
|
||||
toks = tokens(name)
|
||||
head = (toks[0] if toks else name).lower()
|
||||
|
||||
if is_boolean:
|
||||
if head not in lex["copula"]:
|
||||
f.violation("TN-015", name, "a boolean attribute is predicative and begins with is")
|
||||
return
|
||||
|
||||
# TN-012 — a noun, and in particular no has
|
||||
if head == "has":
|
||||
f.violation("TN-012", name, "an attribute is a noun; the domain already says who has it")
|
||||
elif head in lex["active_verbs"] or head in lex["copula"]:
|
||||
f.violation("TN-012", name, "%s is a verb; an attribute is a noun" % head)
|
||||
elif head in lex["participles"]:
|
||||
f.review("TN-012", name, "%s is a participle; confirm this reads as a noun" % head)
|
||||
|
||||
# TN-014 — relational suffixes are reserved for relations
|
||||
last = toks[-1] if toks else name
|
||||
if last in lex["relational_suffixes"]:
|
||||
f.violation("TN-014", name,
|
||||
"ends in %s, a form reserved for object properties" % last)
|
||||
|
||||
# TN-013 — a date attribute is nominal
|
||||
if is_date and (last in lex["relational_suffixes"] or head in lex["participles"]):
|
||||
f.violation("TN-013", name, "a date attribute is a noun, not a participle")
|
||||
|
||||
|
||||
def check_declaration(g, terms, lex, f, abstractness):
|
||||
"""TN-003, TN-006, TN-007, TN-024, TN-025, TN-026, TN-027, TN-028."""
|
||||
layer_roots = {t for t in terms
|
||||
if terms[t] == "class"
|
||||
and (local(t) or "").endswith("LayerObject")}
|
||||
|
||||
for term, kind in terms.items():
|
||||
name = local(term)
|
||||
if name is None:
|
||||
continue
|
||||
|
||||
# TN-024 — every active term carries a comment
|
||||
if not list(g.objects(term, RDFS.comment)):
|
||||
f.violation("TN-024", name, "carries no rdfs:comment")
|
||||
|
||||
# TN-026 — only a concrete class declares how its instances are produced
|
||||
modes = [str(m) for m in g.objects(term, PR.authoringMode)]
|
||||
concrete = (kind == "class"
|
||||
and Literal(False) in set(g.objects(term, PR.isAbstract)))
|
||||
if modes and not concrete:
|
||||
f.violation("TN-026", name,
|
||||
"declares pr:authoringMode; only a concrete class does, "
|
||||
"since a vocabulary term is declared by definition")
|
||||
elif concrete:
|
||||
if not modes:
|
||||
f.violation("TN-026", name, "does not declare pr:authoringMode")
|
||||
else:
|
||||
if "HARVESTED" in modes and not list(g.objects(term, PR.harvestSource)):
|
||||
f.violation("TN-026", name, "is HARVESTED and names no harvestSource")
|
||||
axis = provenance_axis(g, term)
|
||||
if axis == "CapturedObject" and "HARVESTED" not in modes:
|
||||
f.violation("TN-026", name,
|
||||
"descends from CapturedObject and must declare HARVESTED")
|
||||
if axis == "DefinedObject" and "ASSERTED" not in modes:
|
||||
f.violation("TN-026", name,
|
||||
"descends from DefinedObject and must declare ASSERTED")
|
||||
|
||||
if kind == "class":
|
||||
values = list(g.objects(term, PR.isAbstract))
|
||||
# TN-007 — declared exactly once, explicitly
|
||||
if len(values) != 1:
|
||||
f.violation("TN-007", name,
|
||||
"declares pr:isAbstract %d times; exactly one is required"
|
||||
% len(values))
|
||||
# TN-006 — a layer root is abstract
|
||||
elif term in layer_roots and values[0].toPython() is not True:
|
||||
f.violation("TN-006", name, "is a layer root and must be abstract")
|
||||
# TN-027 — a concrete class has one layer and one provenance
|
||||
elif values[0].toPython() is False:
|
||||
check_attachment(g, term, name, layer_roots, f)
|
||||
|
||||
if kind in ("relation", "attribute"):
|
||||
check_property_typing(g, term, name, lex, f)
|
||||
|
||||
# TN-003 — the display properties are annotations
|
||||
for display in (PR.shortLabel, PR.acronym):
|
||||
name = local(display)
|
||||
types = set(g.objects(display, RDF.type))
|
||||
if not types:
|
||||
continue
|
||||
if OWL.AnnotationProperty not in types:
|
||||
f.violation("TN-003", name, "is declared %s and must be an annotation property"
|
||||
% ", ".join(sorted(local(t) or str(t) for t in types)))
|
||||
|
||||
|
||||
def check_attachment(g, term, name, layer_roots, f):
|
||||
"""TN-027 — exactly one layer root and one provenance axis above."""
|
||||
seen, stack = set(), [term]
|
||||
while stack:
|
||||
node = stack.pop()
|
||||
for parent in g.objects(node, RDFS.subClassOf):
|
||||
if isinstance(parent, URIRef) and parent not in seen:
|
||||
seen.add(parent)
|
||||
stack.append(parent)
|
||||
layers = seen & layer_roots
|
||||
axes = seen & {PR.DefinedObject, PR.CapturedObject}
|
||||
if len(layers) != 1:
|
||||
f.violation("TN-027", name, "descends from %d layer roots; exactly one is required"
|
||||
% len(layers))
|
||||
if len(axes) != 1:
|
||||
f.violation("TN-027", name, "descends from %d provenance axes; exactly one is required"
|
||||
% len(axes))
|
||||
|
||||
|
||||
def check_property_typing(g, term, name, lex, f):
|
||||
"""TN-025 and TN-028 — domain, range, and inherited domain."""
|
||||
domains = list(g.objects(term, RDFS.domain))
|
||||
ranges = list(g.objects(term, RDFS.range))
|
||||
|
||||
if not ranges and Literal(True) not in set(g.objects(term, PR.polymorphic)):
|
||||
f.violation("TN-025", name, "declares no rdfs:range")
|
||||
polymorphic = Literal(True) in set(g.objects(term, PR.polymorphic))
|
||||
if not domains:
|
||||
if polymorphic:
|
||||
f.review("TN-025", name,
|
||||
"declares itself polymorphic; confirm the scope is declared in SHACL")
|
||||
else:
|
||||
f.violation("TN-025", name,
|
||||
"omits its domain in silence; declare pr:polymorphic true "
|
||||
"or state the domain")
|
||||
|
||||
# TN-028 — a sub-property inherits the domain of its parent
|
||||
for parent in g.objects(term, RDFS.subPropertyOf):
|
||||
if not isinstance(parent, URIRef) or local(parent) is None:
|
||||
continue
|
||||
parent_domains = set(g.objects(parent, RDFS.domain))
|
||||
if not parent_domains or not domains:
|
||||
continue
|
||||
if set(domains) & parent_domains:
|
||||
continue
|
||||
compatible = False
|
||||
for d in domains:
|
||||
ancestors, stack = set(), [d]
|
||||
while stack:
|
||||
node = stack.pop()
|
||||
for up in g.objects(node, RDFS.subClassOf):
|
||||
if isinstance(up, URIRef) and up not in ancestors:
|
||||
ancestors.add(up)
|
||||
stack.append(up)
|
||||
if ancestors & parent_domains:
|
||||
compatible = True
|
||||
if not compatible:
|
||||
f.review("TN-028", name,
|
||||
"is a sub-property of %s, whose domain does not cover its own; "
|
||||
"asserting it will retype the subject"
|
||||
% local(parent))
|
||||
|
||||
|
||||
def check_label(g, terms, f):
|
||||
"""TN-018 to TN-023 — the label is derived, and carries nothing else."""
|
||||
for term, kind in terms.items():
|
||||
name = local(term)
|
||||
if name is None:
|
||||
continue
|
||||
labels = list(g.objects(term, RDFS.label))
|
||||
if not labels:
|
||||
f.violation("TN-023", name, "carries no rdfs:label")
|
||||
continue
|
||||
if len(labels) > 1:
|
||||
f.violation("TN-023", name, "carries %d labels; the derivation yields one"
|
||||
% len(labels))
|
||||
text = str(labels[0])
|
||||
expected = decamelise(name, kind in ("class", "individual"))
|
||||
if text != expected:
|
||||
f.violation("TN-018", name, "label is %r; the derivation yields %r"
|
||||
% (text, expected))
|
||||
if "(" in text or ")" in text:
|
||||
f.violation("TN-021", name, "the label carries a gloss; it belongs in the comment")
|
||||
if re.search(r"\b(deprecated|obsolete|draft|published|v\d)\b", text, re.I):
|
||||
f.violation("TN-020", name, "the label carries state; the axiom carries it")
|
||||
|
||||
|
||||
def check_namespace(g, f):
|
||||
"""EV-014 — the term namespace is never versioned."""
|
||||
for prefix, uri in g.namespaces():
|
||||
if str(uri).startswith("https://ontology.pernod-ricard.com/metamodel/") \
|
||||
and str(uri) != NS:
|
||||
f.violation("EV-014", str(uri),
|
||||
"the prefix %s binds a versioned namespace; the version lives "
|
||||
"in owl:versionIRI alone" % prefix)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------ main
|
||||
|
||||
def collect(g):
|
||||
terms = OrderedDict()
|
||||
for rdf_type, kind in KINDS.items():
|
||||
for term in sorted(g.subjects(RDF.type, rdf_type), key=str):
|
||||
if local(term) is None:
|
||||
continue
|
||||
if (term, OWL.deprecated, Literal(True)) in g:
|
||||
continue
|
||||
terms[term] = kind
|
||||
return terms
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--ontology", default=DEFAULT_ONTOLOGY)
|
||||
ap.add_argument("--json", default=None, help="write the findings as JSON")
|
||||
ap.add_argument("--quiet", action="store_true", help="print the summary only")
|
||||
args = ap.parse_args()
|
||||
|
||||
lex = yaml.safe_load(open(LEXICON, encoding="utf-8"))
|
||||
rules = yaml.safe_load(open(RULES, encoding="utf-8"))
|
||||
abstractness = {a["term"]: a["is_abstract"] for a in rules["abstractness"]}
|
||||
|
||||
g = Graph()
|
||||
g.parse(args.ontology, format="turtle")
|
||||
terms = collect(g)
|
||||
|
||||
f = Findings()
|
||||
check_identifier(g, terms, lex, f)
|
||||
check_declaration(g, terms, lex, f, abstractness)
|
||||
check_label(g, terms, f)
|
||||
check_namespace(g, f)
|
||||
|
||||
titles = {r["id"]: r["title"] for r in rules["rules"]}
|
||||
print("=" * 78)
|
||||
print("T-BOX NAMING CHECK")
|
||||
print(" ontology : %s" % args.ontology)
|
||||
print(" terms : %d active" % len(terms))
|
||||
print("=" * 78)
|
||||
|
||||
if not args.quiet:
|
||||
for verdict in (VIOLATION, REVIEW):
|
||||
items = [i for i in f.items if i["verdict"] == verdict]
|
||||
if not items:
|
||||
continue
|
||||
print("\n%s — %d" % (verdict, len(items)))
|
||||
by_rule = OrderedDict()
|
||||
for i in items:
|
||||
by_rule.setdefault(i["rule"], []).append(i)
|
||||
for rule, group in sorted(by_rule.items()):
|
||||
print("\n [%s] %s (%d)" % (rule, titles.get(rule, ""), len(group)))
|
||||
for i in group[:12]:
|
||||
print(" %-34s %s" % (i["term"], i["message"]))
|
||||
if len(group) > 12:
|
||||
print(" ... and %d more" % (len(group) - 12))
|
||||
|
||||
print("\n" + "=" * 78)
|
||||
print("RESULT : %d violation(s) | %d for review | %d term(s) checked"
|
||||
% (len(f.violations), len(f.reviews), len(terms)))
|
||||
print("=" * 78)
|
||||
|
||||
if args.json:
|
||||
directory = os.path.dirname(os.path.abspath(args.json))
|
||||
if directory:
|
||||
os.makedirs(directory, exist_ok=True)
|
||||
json.dump({"ontology": args.ontology, "terms": len(terms),
|
||||
"violations": len(f.violations), "reviews": len(f.reviews),
|
||||
"findings": f.items},
|
||||
open(args.json, "w", encoding="utf-8"), indent=2)
|
||||
print("json: %s" % args.json)
|
||||
|
||||
sys.exit(1 if f.violations else 0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,416 @@
|
||||
{
|
||||
"ontology": "ontology/pr_metamodel.ttl",
|
||||
"terms": 130,
|
||||
"violations": 49,
|
||||
"reviews": 19,
|
||||
"findings": [
|
||||
{
|
||||
"verdict": "VIOLATION",
|
||||
"rule": "TN-026",
|
||||
"term": "BaseTable",
|
||||
"message": "is HARVESTED and names no harvestSource"
|
||||
},
|
||||
{
|
||||
"verdict": "VIOLATION",
|
||||
"rule": "TN-026",
|
||||
"term": "ExternalTable",
|
||||
"message": "is HARVESTED and names no harvestSource"
|
||||
},
|
||||
{
|
||||
"verdict": "VIOLATION",
|
||||
"rule": "TN-026",
|
||||
"term": "ForeignKey",
|
||||
"message": "is HARVESTED and names no harvestSource"
|
||||
},
|
||||
{
|
||||
"verdict": "VIOLATION",
|
||||
"rule": "TN-026",
|
||||
"term": "PrimaryKey",
|
||||
"message": "is HARVESTED and names no harvestSource"
|
||||
},
|
||||
{
|
||||
"verdict": "VIOLATION",
|
||||
"rule": "TN-026",
|
||||
"term": "SystemType",
|
||||
"message": "does not declare pr:authoringMode"
|
||||
},
|
||||
{
|
||||
"verdict": "VIOLATION",
|
||||
"rule": "TN-027",
|
||||
"term": "SystemType",
|
||||
"message": "descends from 0 layer roots; exactly one is required"
|
||||
},
|
||||
{
|
||||
"verdict": "VIOLATION",
|
||||
"rule": "TN-027",
|
||||
"term": "SystemType",
|
||||
"message": "descends from 0 provenance axes; exactly one is required"
|
||||
},
|
||||
{
|
||||
"verdict": "VIOLATION",
|
||||
"rule": "TN-026",
|
||||
"term": "View",
|
||||
"message": "is HARVESTED and names no harvestSource"
|
||||
},
|
||||
{
|
||||
"verdict": "REVIEW",
|
||||
"rule": "TN-025",
|
||||
"term": "belongsTo",
|
||||
"message": "declares itself polymorphic; confirm the scope is declared in SHACL"
|
||||
},
|
||||
{
|
||||
"verdict": "VIOLATION",
|
||||
"rule": "TN-024",
|
||||
"term": "consumes",
|
||||
"message": "carries no rdfs:comment"
|
||||
},
|
||||
{
|
||||
"verdict": "VIOLATION",
|
||||
"rule": "TN-024",
|
||||
"term": "containsBIField",
|
||||
"message": "carries no rdfs:comment"
|
||||
},
|
||||
{
|
||||
"verdict": "VIOLATION",
|
||||
"rule": "TN-024",
|
||||
"term": "governedBy",
|
||||
"message": "carries no rdfs:comment"
|
||||
},
|
||||
{
|
||||
"verdict": "VIOLATION",
|
||||
"rule": "TN-024",
|
||||
"term": "hasDomainOwner",
|
||||
"message": "carries no rdfs:comment"
|
||||
},
|
||||
{
|
||||
"verdict": "VIOLATION",
|
||||
"rule": "TN-024",
|
||||
"term": "hasElement",
|
||||
"message": "carries no rdfs:comment"
|
||||
},
|
||||
{
|
||||
"verdict": "VIOLATION",
|
||||
"rule": "TN-024",
|
||||
"term": "hasGovernanceLead",
|
||||
"message": "carries no rdfs:comment"
|
||||
},
|
||||
{
|
||||
"verdict": "VIOLATION",
|
||||
"rule": "TN-024",
|
||||
"term": "hasMetric",
|
||||
"message": "carries no rdfs:comment"
|
||||
},
|
||||
{
|
||||
"verdict": "VIOLATION",
|
||||
"rule": "TN-024",
|
||||
"term": "hasSubDomainOwner",
|
||||
"message": "carries no rdfs:comment"
|
||||
},
|
||||
{
|
||||
"verdict": "VIOLATION",
|
||||
"rule": "TN-024",
|
||||
"term": "hasSystemType",
|
||||
"message": "carries no rdfs:comment"
|
||||
},
|
||||
{
|
||||
"verdict": "VIOLATION",
|
||||
"rule": "TN-024",
|
||||
"term": "hostedOn",
|
||||
"message": "carries no rdfs:comment"
|
||||
},
|
||||
{
|
||||
"verdict": "VIOLATION",
|
||||
"rule": "TN-024",
|
||||
"term": "isInBIDataSource",
|
||||
"message": "carries no rdfs:comment"
|
||||
},
|
||||
{
|
||||
"verdict": "VIOLATION",
|
||||
"rule": "TN-024",
|
||||
"term": "isInBIWorkspace",
|
||||
"message": "carries no rdfs:comment"
|
||||
},
|
||||
{
|
||||
"verdict": "VIOLATION",
|
||||
"rule": "TN-024",
|
||||
"term": "isInDatabase",
|
||||
"message": "carries no rdfs:comment"
|
||||
},
|
||||
{
|
||||
"verdict": "VIOLATION",
|
||||
"rule": "TN-024",
|
||||
"term": "livesIn",
|
||||
"message": "carries no rdfs:comment"
|
||||
},
|
||||
{
|
||||
"verdict": "VIOLATION",
|
||||
"rule": "TN-024",
|
||||
"term": "measures",
|
||||
"message": "carries no rdfs:comment"
|
||||
},
|
||||
{
|
||||
"verdict": "VIOLATION",
|
||||
"rule": "TN-024",
|
||||
"term": "ownedBy",
|
||||
"message": "carries no rdfs:comment"
|
||||
},
|
||||
{
|
||||
"verdict": "REVIEW",
|
||||
"rule": "TN-025",
|
||||
"term": "ownedBy",
|
||||
"message": "declares itself polymorphic; confirm the scope is declared in SHACL"
|
||||
},
|
||||
{
|
||||
"verdict": "REVIEW",
|
||||
"rule": "TN-025",
|
||||
"term": "ownedByDomain",
|
||||
"message": "declares itself polymorphic; confirm the scope is declared in SHACL"
|
||||
},
|
||||
{
|
||||
"verdict": "VIOLATION",
|
||||
"rule": "TN-024",
|
||||
"term": "produces",
|
||||
"message": "carries no rdfs:comment"
|
||||
},
|
||||
{
|
||||
"verdict": "VIOLATION",
|
||||
"rule": "TN-024",
|
||||
"term": "referencesSourceField",
|
||||
"message": "carries no rdfs:comment"
|
||||
},
|
||||
{
|
||||
"verdict": "VIOLATION",
|
||||
"rule": "TN-024",
|
||||
"term": "referencesTargetField",
|
||||
"message": "carries no rdfs:comment"
|
||||
},
|
||||
{
|
||||
"verdict": "REVIEW",
|
||||
"rule": "TN-025",
|
||||
"term": "represents",
|
||||
"message": "declares itself polymorphic; confirm the scope is declared in SHACL"
|
||||
},
|
||||
{
|
||||
"verdict": "REVIEW",
|
||||
"rule": "TN-025",
|
||||
"term": "sourcedFrom",
|
||||
"message": "declares itself polymorphic; confirm the scope is declared in SHACL"
|
||||
},
|
||||
{
|
||||
"verdict": "REVIEW",
|
||||
"rule": "TN-025",
|
||||
"term": "storedIn",
|
||||
"message": "declares itself polymorphic; confirm the scope is declared in SHACL"
|
||||
},
|
||||
{
|
||||
"verdict": "VIOLATION",
|
||||
"rule": "TN-024",
|
||||
"term": "usesBIDataSource",
|
||||
"message": "carries no rdfs:comment"
|
||||
},
|
||||
{
|
||||
"verdict": "VIOLATION",
|
||||
"rule": "TN-024",
|
||||
"term": "activationStatus",
|
||||
"message": "carries no rdfs:comment"
|
||||
},
|
||||
{
|
||||
"verdict": "REVIEW",
|
||||
"rule": "TN-025",
|
||||
"term": "arbitrationStatus",
|
||||
"message": "declares itself polymorphic; confirm the scope is declared in SHACL"
|
||||
},
|
||||
{
|
||||
"verdict": "VIOLATION",
|
||||
"rule": "TN-024",
|
||||
"term": "businessRule",
|
||||
"message": "carries no rdfs:comment"
|
||||
},
|
||||
{
|
||||
"verdict": "VIOLATION",
|
||||
"rule": "TN-024",
|
||||
"term": "canonicalName",
|
||||
"message": "carries no rdfs:comment"
|
||||
},
|
||||
{
|
||||
"verdict": "REVIEW",
|
||||
"rule": "TN-025",
|
||||
"term": "canonicalName",
|
||||
"message": "declares itself polymorphic; confirm the scope is declared in SHACL"
|
||||
},
|
||||
{
|
||||
"verdict": "VIOLATION",
|
||||
"rule": "TN-024",
|
||||
"term": "creationDate",
|
||||
"message": "carries no rdfs:comment"
|
||||
},
|
||||
{
|
||||
"verdict": "REVIEW",
|
||||
"rule": "TN-025",
|
||||
"term": "creationDate",
|
||||
"message": "declares itself polymorphic; confirm the scope is declared in SHACL"
|
||||
},
|
||||
{
|
||||
"verdict": "VIOLATION",
|
||||
"rule": "TN-024",
|
||||
"term": "environment",
|
||||
"message": "carries no rdfs:comment"
|
||||
},
|
||||
{
|
||||
"verdict": "VIOLATION",
|
||||
"rule": "TN-024",
|
||||
"term": "exampleValue",
|
||||
"message": "carries no rdfs:comment"
|
||||
},
|
||||
{
|
||||
"verdict": "REVIEW",
|
||||
"rule": "TN-025",
|
||||
"term": "exampleValue",
|
||||
"message": "declares itself polymorphic; confirm the scope is declared in SHACL"
|
||||
},
|
||||
{
|
||||
"verdict": "VIOLATION",
|
||||
"rule": "TN-024",
|
||||
"term": "formula",
|
||||
"message": "carries no rdfs:comment"
|
||||
},
|
||||
{
|
||||
"verdict": "REVIEW",
|
||||
"rule": "TN-025",
|
||||
"term": "fullyQualifiedName",
|
||||
"message": "declares itself polymorphic; confirm the scope is declared in SHACL"
|
||||
},
|
||||
{
|
||||
"verdict": "REVIEW",
|
||||
"rule": "TN-025",
|
||||
"term": "harvestDate",
|
||||
"message": "declares itself polymorphic; confirm the scope is declared in SHACL"
|
||||
},
|
||||
{
|
||||
"verdict": "VIOLATION",
|
||||
"rule": "TN-024",
|
||||
"term": "identifier",
|
||||
"message": "carries no rdfs:comment"
|
||||
},
|
||||
{
|
||||
"verdict": "REVIEW",
|
||||
"rule": "TN-025",
|
||||
"term": "identifier",
|
||||
"message": "declares itself polymorphic; confirm the scope is declared in SHACL"
|
||||
},
|
||||
{
|
||||
"verdict": "VIOLATION",
|
||||
"rule": "TN-024",
|
||||
"term": "isNullable",
|
||||
"message": "carries no rdfs:comment"
|
||||
},
|
||||
{
|
||||
"verdict": "VIOLATION",
|
||||
"rule": "TN-024",
|
||||
"term": "lastReviewDate",
|
||||
"message": "carries no rdfs:comment"
|
||||
},
|
||||
{
|
||||
"verdict": "REVIEW",
|
||||
"rule": "TN-025",
|
||||
"term": "lastReviewDate",
|
||||
"message": "declares itself polymorphic; confirm the scope is declared in SHACL"
|
||||
},
|
||||
{
|
||||
"verdict": "VIOLATION",
|
||||
"rule": "TN-024",
|
||||
"term": "logicalFormat",
|
||||
"message": "carries no rdfs:comment"
|
||||
},
|
||||
{
|
||||
"verdict": "VIOLATION",
|
||||
"rule": "TN-024",
|
||||
"term": "ordinalPosition",
|
||||
"message": "carries no rdfs:comment"
|
||||
},
|
||||
{
|
||||
"verdict": "VIOLATION",
|
||||
"rule": "TN-024",
|
||||
"term": "physicalDataType",
|
||||
"message": "carries no rdfs:comment"
|
||||
},
|
||||
{
|
||||
"verdict": "REVIEW",
|
||||
"rule": "TN-025",
|
||||
"term": "physicalName",
|
||||
"message": "declares itself polymorphic; confirm the scope is declared in SHACL"
|
||||
},
|
||||
{
|
||||
"verdict": "VIOLATION",
|
||||
"rule": "TN-024",
|
||||
"term": "queryCount",
|
||||
"message": "carries no rdfs:comment"
|
||||
},
|
||||
{
|
||||
"verdict": "REVIEW",
|
||||
"rule": "TN-025",
|
||||
"term": "sourceIdentifier",
|
||||
"message": "declares itself polymorphic; confirm the scope is declared in SHACL"
|
||||
},
|
||||
{
|
||||
"verdict": "VIOLATION",
|
||||
"rule": "TN-024",
|
||||
"term": "status",
|
||||
"message": "carries no rdfs:comment"
|
||||
},
|
||||
{
|
||||
"verdict": "REVIEW",
|
||||
"rule": "TN-025",
|
||||
"term": "status",
|
||||
"message": "declares itself polymorphic; confirm the scope is declared in SHACL"
|
||||
},
|
||||
{
|
||||
"verdict": "VIOLATION",
|
||||
"rule": "TN-024",
|
||||
"term": "synonym",
|
||||
"message": "carries no rdfs:comment"
|
||||
},
|
||||
{
|
||||
"verdict": "VIOLATION",
|
||||
"rule": "TN-024",
|
||||
"term": "technicalDefinition",
|
||||
"message": "carries no rdfs:comment"
|
||||
},
|
||||
{
|
||||
"verdict": "VIOLATION",
|
||||
"rule": "TN-024",
|
||||
"term": "timeAggregation",
|
||||
"message": "carries no rdfs:comment"
|
||||
},
|
||||
{
|
||||
"verdict": "REVIEW",
|
||||
"rule": "TN-025",
|
||||
"term": "unit",
|
||||
"message": "declares itself polymorphic; confirm the scope is declared in SHACL"
|
||||
},
|
||||
{
|
||||
"verdict": "VIOLATION",
|
||||
"rule": "TN-024",
|
||||
"term": "version",
|
||||
"message": "carries no rdfs:comment"
|
||||
},
|
||||
{
|
||||
"verdict": "REVIEW",
|
||||
"rule": "TN-025",
|
||||
"term": "version",
|
||||
"message": "declares itself polymorphic; confirm the scope is declared in SHACL"
|
||||
},
|
||||
{
|
||||
"verdict": "VIOLATION",
|
||||
"rule": "TN-024",
|
||||
"term": "viewDefinition",
|
||||
"message": "carries no rdfs:comment"
|
||||
},
|
||||
{
|
||||
"verdict": "VIOLATION",
|
||||
"rule": "TN-024",
|
||||
"term": "acronym",
|
||||
"message": "carries no rdfs:comment"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
# T-BOX AND A-BOX MIGRATION TO v2.2 — GOVERNANCE ASSIGNMENTS
|
||||
# =============================================================================
|
||||
# Data consumed by migrate_tbox_v2_2.py.
|
||||
#
|
||||
# An actor node is an ASSIGNMENT, not a person. The post is stable and carries
|
||||
# the canonical name, the identifier and the domain it answers to; the person
|
||||
# holding it is a value that changes. Modelled the other way round, a change of
|
||||
# incumbent would move nodes and edges rather than one literal, and the same
|
||||
# person covering two domains would exist twice.
|
||||
# =============================================================================
|
||||
|
||||
meta:
|
||||
from_version: "2.1"
|
||||
to_version: "2.2"
|
||||
namespace: "https://ontology.pernod-ricard.com/metamodel/"
|
||||
instance_namespace: "https://data.pernod-ricard.com/sodh/"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# T-BOX
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
declare_properties:
|
||||
- term: personName
|
||||
kind: DatatypeProperty
|
||||
domain: Actor
|
||||
range: "http://www.w3.org/2001/XMLSchema#string"
|
||||
comment: >
|
||||
The name of the person currently holding the post. Declared on Actor rather
|
||||
than on each role, since every role shares it and Actor is their smallest
|
||||
common ancestor below the layer root. Held as a value rather than a node
|
||||
because what changes is the person, not the assignment: the post keeps its
|
||||
identifier, its domain and every edge that points at it, and a change of
|
||||
holder rewrites one literal. Used on actors and nowhere else.
|
||||
|
||||
display_updates:
|
||||
- {term: DataSteward, acronym: DST}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# A-BOX — RENAME
|
||||
# ST is not the short form of Data Steward; DST is, and the rulebook now
|
||||
# declares it.
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
instance_renames:
|
||||
- {from: ST_DD_04, to: DST_DD_04}
|
||||
- {from: ST_DD_05, to: DST_DD_05}
|
||||
- {from: ST_DD_06, to: DST_DD_06}
|
||||
- {from: ST_DD_10, to: DST_DD_10}
|
||||
- {from: ST_DD_16, to: DST_DD_16}
|
||||
- {from: ST_DD_21, to: DST_DD_21}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# A-BOX — COMPLETE THE EXISTING ASSIGNMENTS
|
||||
# The canonical name becomes the name of the POST. The person moves to heldBy.
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
complete:
|
||||
- {node: DDO_DD_04, identifier: "DDO-04", name: "Data Domain Owner", held_by: "David Carpio", domain: DD_04}
|
||||
- {node: DDO_DD_05, identifier: "DDO-05", name: "Data Domain Owner", held_by: "David Carpio", domain: DD_05}
|
||||
- {node: DDO_DD_06, identifier: "DDO-06", name: "Data Domain Owner", held_by: "Ludovic Cottier", domain: DD_06}
|
||||
- {node: DDO_DD_10, identifier: "DDO-10", name: "Data Domain Owner", held_by: "Alberto Lupano", domain: DD_10}
|
||||
- {node: DDO_DD_16, identifier: "DDO-16", name: "Data Domain Owner", held_by: "Julien Soisson", domain: DD_16}
|
||||
- {node: DDO_DD_21, identifier: "DDO-21", name: "Data Domain Owner", held_by: "Vincent Meunier", domain: DD_21}
|
||||
- {node: DGL_DD_04, identifier: "DGL-04", name: "Data Governance Lead", held_by: "Marie Carabin", domain: DD_04}
|
||||
- {node: DGL_DD_05, identifier: "DGL-05", name: "Data Governance Lead", held_by: "Alberic Piot", domain: DD_05}
|
||||
- {node: DGL_DD_06, identifier: "DGL-06", name: "Data Governance Lead", held_by: "Bastien Gourdon", domain: DD_06}
|
||||
- {node: DGL_DD_10, identifier: "DGL-10", name: "Data Governance Lead", held_by: "Anas El Kesri", domain: DD_10}
|
||||
- {node: DGL_DD_16, identifier: "DGL-16", name: "Data Governance Lead", held_by: "Gaelle Seret", domain: DD_16}
|
||||
- {node: DGL_DD_21, identifier: "DGL-21", name: "Data Governance Lead", held_by: "Bastien Gourdon", domain: DD_21}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# A-BOX — SUB-DOMAIN OWNERS, 10
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
create_sub_domain_owners:
|
||||
- {node: SDO_SD_04_01, identifier: "SDO-04.01", held_by: "Etienne Coulon", sub_domain: SD_04_01, domain: DD_04}
|
||||
- {node: SDO_SD_04_02, identifier: "SDO-04.02", held_by: "Etienne Coulon", sub_domain: SD_04_02, domain: DD_04}
|
||||
- {node: SDO_SD_04_03, identifier: "SDO-04.03", held_by: "Etienne Coulon", sub_domain: SD_04_03, domain: DD_04}
|
||||
- {node: SDO_SD_05_01, identifier: "SDO-05.01", held_by: "David Carpio", sub_domain: SD_05_01, domain: DD_05}
|
||||
- {node: SDO_SD_06_01, identifier: "SDO-06.01", held_by: "Loic Berger", sub_domain: SD_06_01, domain: DD_06}
|
||||
- {node: SDO_SD_10_01, identifier: "SDO-10.01", held_by: "Alberto Lupano", sub_domain: SD_10_01, domain: DD_10}
|
||||
- {node: SDO_SD_16_01, identifier: "SDO-16.01", held_by: "Julien Soisson", sub_domain: SD_16_01, domain: DD_16}
|
||||
- {node: SDO_SD_16_02, identifier: "SDO-16.02", held_by: "Julien Soisson", sub_domain: SD_16_02, domain: DD_16}
|
||||
- {node: SDO_SD_16_03, identifier: "SDO-16.03", held_by: "Julien Soisson", sub_domain: SD_16_03, domain: DD_16}
|
||||
- {node: SDO_SD_21_01, identifier: "SDO-21.01", held_by: "Vincent Meunier", sub_domain: SD_21_01, domain: DD_21}
|
||||
|
||||
sub_domain_owner_name: Data Sub Domain Owner
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# A-BOX — PRODUCT OWNER, 1
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
create_product_owners:
|
||||
- {node: PO_SODH, identifier: "PO-06.01-001", held_by: "Guilherme Muller", product: DP_06_01_001, domain: DD_06}
|
||||
|
||||
product_owner_name: Data Product Owner
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# A-BOX — STEWARDS, ONE ASSIGNMENT PER BUSINESS OBJECT
|
||||
# Derived from the graph rather than listed: the business objects are the source
|
||||
# of truth for how many assignments exist and which domain each answers to.
|
||||
# The incumbent is read from this map by domain.
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
steward_holders:
|
||||
DD_04: Marc Chambouleyron
|
||||
DD_05: Francois Vadrouille
|
||||
DD_06: Michael Flores
|
||||
DD_10: Magda Kugli
|
||||
DD_16: Clementine Sauries
|
||||
DD_21: Alberic Piot
|
||||
|
||||
steward_name: Data Steward
|
||||
|
||||
# The six per-domain steward nodes are replaced by one per business object.
|
||||
# monitoredBy is repointed from the old node to the new one, and the old node is
|
||||
# withdrawn once nothing names it (EV-003).
|
||||
retire_domain_stewards: true
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# A-BOX — GOVERN A TRANSVERSE CONCEPT
|
||||
# Panel Coverage is used by four business objects through usesConcept and is the
|
||||
# subject of none. A concept mobilised without an object that carries it has no
|
||||
# owner, no steward and no governance chain: it is used without being governed.
|
||||
# Creating the missing business object closes the chain BC -> BO -> SD -> DD,
|
||||
# and the steward assignment follows automatically since assignments are derived
|
||||
# from the business objects present in the graph.
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
create_business_objects:
|
||||
- node: BO_06_01_006
|
||||
identifier: "BO-06.01-006"
|
||||
name: Panel Coverage
|
||||
is_about: BC_06_01_003
|
||||
sub_domain: SD_06_01
|
||||
domain: DD_06
|
||||
# -----------------------------------------------------------------------------
|
||||
# A-BOX — ownedBy AGAINST THE OLD DOMAIN-LEVEL STEWARDS
|
||||
# 53 subjects declared they belonged to the per-domain steward node. That node
|
||||
# disappears, so each reference is resolved rather than dropped in silence.
|
||||
#
|
||||
# business object -> removed. It already carries monitoredBy towards the
|
||||
# same assignment, and two properties holding one fact
|
||||
# are two answers to one question waiting to diverge.
|
||||
# sub-domain -> repointed to its Data Sub Domain Owner, whose function
|
||||
# this is; a steward had no business holding it.
|
||||
# everything else -> repointed to the steward assignment of the business
|
||||
# object it belongs to, followed through the graph.
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
owned_by_resolution:
|
||||
remove_on: [BusinessObject]
|
||||
repoint_sub_domains: true
|
||||
repoint_to_business_object_assignment: true
|
||||
# Chains followed to reach the business object, in order. The first that
|
||||
# resolves wins; anything unresolved is reported and left untouched.
|
||||
chains:
|
||||
- {via: isAbout, direction: inbound} # a BO is about this concept
|
||||
- {via: represents, direction: outbound} # this object represents a BO
|
||||
- {via: measures, direction: outbound} # a metric measures a concept, then isAbout
|
||||
# -----------------------------------------------------------------------------
|
||||
# COMMON
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
status: PUBLISHED
|
||||
version: "1.0"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# SHAPES — NR-002 EXEMPTION
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
shape_notes:
|
||||
- rule: NR-002
|
||||
change: exempt descendants of Actor from the canonical-name uniqueness query
|
||||
reason: >
|
||||
An assignment is a post, not a catalogued object. Five steward posts in one
|
||||
domain are legitimately homonymous; what tells them apart is the identifier
|
||||
and the business object that names them. Applying a uniqueness rule written
|
||||
for catalogued objects would force fifteen invented names for one job.
|
||||
@@ -0,0 +1,166 @@
|
||||
# T-BOX MIGRATION TO v2.1 — DECLARATIONS
|
||||
# =============================================================================
|
||||
# Data consumed by migrate_tbox_v2_1.py. No renaming here: v2.1 declares what
|
||||
# v2.0 left silent.
|
||||
#
|
||||
# Three subjects, each closing a class of TN-025, TN-026 and TN-027 findings:
|
||||
# 1. polymorphism made explicit rather than inferred from prose
|
||||
# 2. domains stated wherever a single class of use exists
|
||||
# 3. provenance moved off the terms and onto the concrete classes
|
||||
# =============================================================================
|
||||
|
||||
meta:
|
||||
from_version: "2.0"
|
||||
to_version: "2.1"
|
||||
namespace: "https://ontology.pernod-ricard.com/metamodel/"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# THE ANNOTATION ITSELF (TN-025)
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
declare_annotations:
|
||||
- term: polymorphic
|
||||
label: polymorphic
|
||||
comment: >
|
||||
Declares that a property deliberately carries no rdfs:domain because it
|
||||
serves several classes with no useful common ancestor. Its scope is
|
||||
controlled class by class in SHACL instead. Without this axiom a deliberate
|
||||
omission and a forgotten one look identical, and no check can tell them
|
||||
apart.
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# POLYMORPHIC PROPERTIES — 18
|
||||
#
|
||||
# The test: a property takes as domain the smallest common ancestor of its
|
||||
# classes of use, PROVIDED that ancestor sits below a layer root. A layer root,
|
||||
# a provenance axis and MetaModelObject are all too high to say anything; a
|
||||
# property whose smallest common ancestor is one of those is polymorphic.
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
polymorphic:
|
||||
- {term: identifier, reason: "borne by every object in the graph"}
|
||||
- {term: canonicalName, reason: "borne by every object in the graph"}
|
||||
- {term: status, reason: "borne by every object in the graph"}
|
||||
- {term: version, reason: "borne by every object in the graph"}
|
||||
- {term: creationDate, reason: "borne by every object in the graph"}
|
||||
- {term: lastReviewDate, reason: "borne by every object in the graph"}
|
||||
- {term: arbitrationStatus, reason: "any declared object may be under arbitration"}
|
||||
- {term: ownedByDomain, reason: "smallest common ancestor is a provenance axis, not a layer class"}
|
||||
- {term: harvestDate, reason: "smallest common ancestor is a provenance axis, not a layer class"}
|
||||
- {term: sourceIdentifier, reason: "smallest common ancestor is a provenance axis, not a layer class"}
|
||||
- {term: belongsTo, reason: "sub-domain to domain and business object to sub-domain: two couples with no useful ancestor"}
|
||||
- {term: storedIn, reason: "two granularities, data object to structure and data element to field"}
|
||||
- {term: represents, reason: "data object to business object and data element to business concept"}
|
||||
- {term: unit, reason: "data element and metric sit in different layers"}
|
||||
- {term: sourcedFrom, reason: "data object and data structure sit in different layers"}
|
||||
- {term: fullyQualifiedName, reason: "DATABASE.SCHEMA.RELATION[.COLUMN] names a structure and a column alike"}
|
||||
- {term: ownedBy, reason: "any object may have an owner; Actor is the range, the domain is free"}
|
||||
- {term: exampleValue, reason: "observed on a field, documented on a data element"}
|
||||
- {term: physicalName, reason: "a data object names its table as a data element names its column; BR-013 names one carrier where there are two"}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# DOMAINS TO DECLARE — 18
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
domains:
|
||||
- {term: formula, domain: Metric, basis: "shape; forbidden on BusinessConcept, which is what distinguishes a metric"}
|
||||
- {term: timeAggregation, domain: Metric, basis: "belongs to the calculation, alongside formula"}
|
||||
- {term: businessDefinition, domain: BusinessConcept, basis: "shape; forbidden on BusinessObject, whose meaning lives in its concept"}
|
||||
- {term: businessRule, domain: BusinessConcept, basis: "meaning belongs to the concept"}
|
||||
- {term: synonym, domain: BusinessConcept, basis: "meaning belongs to the concept"}
|
||||
- {term: technicalDefinition, domain: BusinessConcept, basis: "meaning belongs to the concept"}
|
||||
- {term: monitoredBy, domain: BusinessObject, basis: "shape; forbidden on DataElement and DataObject, where stewardship is inherited"}
|
||||
- {term: logicalFormat, domain: DataElement, basis: "a logical characteristic of the element"}
|
||||
- {term: isNullable, domain: Field, basis: "a property of the column"}
|
||||
- {term: ordinalPosition, domain: Field, basis: "a property of the column"}
|
||||
- {term: physicalDataType, domain: Field, basis: "a property of the column"}
|
||||
- {term: isInSchema, domain: DataStructure, basis: "shape"}
|
||||
- {term: viewDefinition, domain: View, basis: "a table has no SQL definition"}
|
||||
- {term: queryCount, domain: DataStructure, basis: "ACCESS_HISTORY traces tables, views and external tables alike"}
|
||||
- {term: lastQueryDate, domain: DataStructure, basis: "ACCESS_HISTORY traces tables, views and external tables alike"}
|
||||
- {term: definitionAddress, domain: Transformation, basis: "the Git URI of the code behind a transformation; a view uses viewDefinition"}
|
||||
- {term: expression, domain: BusinessIntelligenceField, basis: "the calculated-field formula, compared against the metric formula to detect drift"}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# NEW TERM (procedure C1)
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
declare_properties:
|
||||
- term: serviceCommitment
|
||||
kind: DatatypeProperty
|
||||
domain: DataContract
|
||||
range: "http://www.w3.org/2001/XMLSchema#string"
|
||||
comment: >
|
||||
What the contract guarantees about the product it covers: refresh schedule,
|
||||
freshness, quality thresholds, support channel, notice period on a breaking
|
||||
change. Held as prose for now, which no shape can verify; the structured
|
||||
form, aligned on ODCS, is the next step. Recorded here because the
|
||||
commitment had been written into businessRule for want of anywhere else,
|
||||
where it sat next to genuine business rules and could not be told apart.
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# RANGES MISSING ON POLYMORPHIC PROPERTIES
|
||||
# A polymorphic property may still have a single range: what it points AT can be
|
||||
# settled even when what it starts FROM cannot.
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
ranges: []
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# PROVENANCE (TN-026)
|
||||
#
|
||||
# A vocabulary term is declared, always and by definition. What has a provenance
|
||||
# is the INSTANCES, and the concrete class is where the model states which one.
|
||||
# Nothing is inherited: each class declares its own mode, and the mode agrees
|
||||
# with the provenance axis the class descends from.
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
provenance:
|
||||
strip_from_non_concrete: true # abstract classes, properties, vocabulary terms
|
||||
derive_from_axis: true # CapturedObject -> HARVESTED, DefinedObject -> ASSERTED
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# ATTACHMENT (TN-027)
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
attach:
|
||||
- term: GovernanceLayerObject
|
||||
parent: DefinedObject
|
||||
basis: >
|
||||
A layer root sits on a provenance axis like any other. Actors and governance
|
||||
objects are declared by governance, never harvested, so the governance layer
|
||||
descends from DefinedObject and its five roles inherit the axis.
|
||||
|
||||
# SystemType remains outside the layers: it is a controlled-vocabulary class,
|
||||
# the named and closed exception of TN-027. Recorded as an open arbitration
|
||||
# rather than silently resolved here.
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# INSTANCE CORRECTIONS
|
||||
# Each one was surfaced by declaring a domain: an instance carrying a property
|
||||
# outside its domain is retyped by RDFS inference, and the shapes of the wrong
|
||||
# class then fire on it.
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
instance_fixes:
|
||||
- action: remove
|
||||
property: businessDefinition
|
||||
subjects: [DI_06_01_001, DI_06_01_002]
|
||||
reason: >
|
||||
Meaning belongs to the Business Concept. An interface is a channel through
|
||||
which data is delivered; it defines nothing. Same rule the shapes already
|
||||
enforce on Business Object.
|
||||
- action: remove
|
||||
property: sourcedFrom
|
||||
subjects: [DI_06_01_001, DI_06_01_002]
|
||||
reason: >
|
||||
An interface has no system of truth: the data it exposes is sourced by the
|
||||
Data Objects behind it, and those name the system.
|
||||
- action: move
|
||||
property: businessRule
|
||||
to: serviceCommitment
|
||||
subjects: [DC_06_01_001]
|
||||
reason: >
|
||||
The text is a service commitment, not a business rule: refresh window,
|
||||
notice period, support channel. It was written into businessRule for want
|
||||
of a property to hold it.
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"attempt_timestamp": "2026-08-06T12:21:45",
|
||||
"mode": "dry-run",
|
||||
"from_version": "2.0",
|
||||
"to_version": "2.1",
|
||||
"input_checksums": {
|
||||
"ontology/pr_metamodel.ttl": "32929c506d402738807b8b30c04fbb48"
|
||||
},
|
||||
"steps": {
|
||||
"annotations_declared": 1,
|
||||
"polymorphic_declared": 18,
|
||||
"domains_declared": 18,
|
||||
"attached": 1,
|
||||
"provenance_stripped": 5,
|
||||
"provenance_declared": 29,
|
||||
"provenance_no_axis": 1,
|
||||
"version_bumped": 1
|
||||
},
|
||||
"total_changes": 74,
|
||||
"triples_before": 723,
|
||||
"triples_after": 786,
|
||||
"notes": [
|
||||
"annotations_declared: polymorphic",
|
||||
"attached: GovernanceLayerObject -> DefinedObject",
|
||||
"provenance_stripped: storedIn",
|
||||
"provenance_stripped: primarilyStoredIn",
|
||||
"provenance_stripped: DefinedObject",
|
||||
"provenance_stripped: exposesMetric",
|
||||
"provenance_stripped: CapturedObject",
|
||||
"provenance_no_axis: SystemType",
|
||||
"version_bumped: https://ontology.pernod-ricard.com/metamodel/2.1"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"attempt_timestamp": "2026-08-06T12:22:48",
|
||||
"mode": "apply",
|
||||
"from_version": "2.0",
|
||||
"to_version": "2.1",
|
||||
"input_checksums": {
|
||||
"ontology/pr_metamodel.ttl": "32929c506d402738807b8b30c04fbb48"
|
||||
},
|
||||
"steps": {
|
||||
"annotations_declared": 1,
|
||||
"polymorphic_declared": 18,
|
||||
"domains_declared": 18,
|
||||
"attached": 1,
|
||||
"provenance_stripped": 5,
|
||||
"provenance_declared": 29,
|
||||
"provenance_no_axis": 1,
|
||||
"version_bumped": 1
|
||||
},
|
||||
"total_changes": 74,
|
||||
"triples_before": 723,
|
||||
"triples_after": 786,
|
||||
"notes": [
|
||||
"annotations_declared: polymorphic",
|
||||
"attached: GovernanceLayerObject -> DefinedObject",
|
||||
"provenance_stripped: DefinedObject",
|
||||
"provenance_stripped: CapturedObject",
|
||||
"provenance_stripped: storedIn",
|
||||
"provenance_stripped: exposesMetric",
|
||||
"provenance_stripped: primarilyStoredIn",
|
||||
"provenance_no_axis: SystemType",
|
||||
"version_bumped: https://ontology.pernod-ricard.com/metamodel/2.1"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"attempt_timestamp": "2026-08-06T12:22:50",
|
||||
"mode": "dry-run",
|
||||
"from_version": "2.0",
|
||||
"to_version": "2.1",
|
||||
"input_checksums": {
|
||||
"ontology/pr_metamodel.ttl": "04273379898d04b9952de68004de5768"
|
||||
},
|
||||
"steps": {
|
||||
"provenance_no_axis": 1
|
||||
},
|
||||
"total_changes": 1,
|
||||
"triples_before": 786,
|
||||
"triples_after": 786,
|
||||
"notes": [
|
||||
"provenance_no_axis: SystemType"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"attempt_timestamp": "2026-08-06T14:34:17",
|
||||
"mode": "dry-run",
|
||||
"from_version": "2.0",
|
||||
"to_version": "2.1",
|
||||
"input_checksums": {
|
||||
"ontology/pr_metamodel.ttl": "32929c506d402738807b8b30c04fbb48",
|
||||
"instances/sodh.ttl": "6bef7bad65c091738cd58cf5f0f1a395"
|
||||
},
|
||||
"steps": {
|
||||
"annotations_declared": 1,
|
||||
"properties_declared": 1,
|
||||
"polymorphic_declared": 19,
|
||||
"domains_declared": 17,
|
||||
"attached": 1,
|
||||
"provenance_stripped": 5,
|
||||
"provenance_declared": 29,
|
||||
"provenance_no_axis": 1,
|
||||
"instances_removed": 4,
|
||||
"instances_moved": 1,
|
||||
"version_bumped": 1
|
||||
},
|
||||
"total_changes": 80,
|
||||
"triples_before": {
|
||||
"ontology/pr_metamodel.ttl": 723,
|
||||
"instances/sodh.ttl": 2308
|
||||
},
|
||||
"triples_after": {
|
||||
"ontology/pr_metamodel.ttl": 791,
|
||||
"instances/sodh.ttl": 2304
|
||||
},
|
||||
"notes": [
|
||||
"annotations_declared: polymorphic",
|
||||
"properties_declared: serviceCommitment",
|
||||
"attached: GovernanceLayerObject -> DefinedObject",
|
||||
"provenance_stripped: CapturedObject",
|
||||
"provenance_stripped: storedIn",
|
||||
"provenance_stripped: primarilyStoredIn",
|
||||
"provenance_stripped: DefinedObject",
|
||||
"provenance_stripped: exposesMetric",
|
||||
"provenance_no_axis: SystemType",
|
||||
"instances_removed: DI_06_01_001 businessDefinition",
|
||||
"instances_removed: DI_06_01_002 businessDefinition",
|
||||
"instances_removed: DI_06_01_001 sourcedFrom",
|
||||
"instances_removed: DI_06_01_002 sourcedFrom",
|
||||
"instances_moved: DC_06_01_001 businessRule",
|
||||
"version_bumped: https://ontology.pernod-ricard.com/metamodel/2.1"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"attempt_timestamp": "2026-08-06T14:35:03",
|
||||
"mode": "apply",
|
||||
"from_version": "2.0",
|
||||
"to_version": "2.1",
|
||||
"input_checksums": {
|
||||
"ontology/pr_metamodel.ttl": "32929c506d402738807b8b30c04fbb48",
|
||||
"instances/sodh.ttl": "6bef7bad65c091738cd58cf5f0f1a395"
|
||||
},
|
||||
"steps": {
|
||||
"annotations_declared": 1,
|
||||
"properties_declared": 1,
|
||||
"polymorphic_declared": 19,
|
||||
"domains_declared": 17,
|
||||
"attached": 1,
|
||||
"provenance_stripped": 5,
|
||||
"provenance_declared": 29,
|
||||
"provenance_no_axis": 1,
|
||||
"instances_removed": 4,
|
||||
"instances_moved": 1,
|
||||
"version_bumped": 1
|
||||
},
|
||||
"total_changes": 80,
|
||||
"triples_before": {
|
||||
"ontology/pr_metamodel.ttl": 723,
|
||||
"instances/sodh.ttl": 2308
|
||||
},
|
||||
"triples_after": {
|
||||
"ontology/pr_metamodel.ttl": 791,
|
||||
"instances/sodh.ttl": 2304
|
||||
},
|
||||
"notes": [
|
||||
"annotations_declared: polymorphic",
|
||||
"properties_declared: serviceCommitment",
|
||||
"attached: GovernanceLayerObject -> DefinedObject",
|
||||
"provenance_stripped: storedIn",
|
||||
"provenance_stripped: primarilyStoredIn",
|
||||
"provenance_stripped: DefinedObject",
|
||||
"provenance_stripped: CapturedObject",
|
||||
"provenance_stripped: exposesMetric",
|
||||
"provenance_no_axis: SystemType",
|
||||
"instances_removed: DI_06_01_001 businessDefinition",
|
||||
"instances_removed: DI_06_01_002 businessDefinition",
|
||||
"instances_removed: DI_06_01_001 sourcedFrom",
|
||||
"instances_removed: DI_06_01_002 sourcedFrom",
|
||||
"instances_moved: DC_06_01_001 businessRule",
|
||||
"version_bumped: https://ontology.pernod-ricard.com/metamodel/2.1"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"attempt_timestamp": "2026-08-06T14:35:07",
|
||||
"mode": "dry-run",
|
||||
"from_version": "2.0",
|
||||
"to_version": "2.1",
|
||||
"input_checksums": {
|
||||
"ontology/pr_metamodel.ttl": "d264f924c61faa2fe8caf7be8ec7490c",
|
||||
"instances/sodh.ttl": "135278dd413486cb1273fe193a5aa590"
|
||||
},
|
||||
"steps": {
|
||||
"provenance_no_axis": 1
|
||||
},
|
||||
"total_changes": 1,
|
||||
"triples_before": {
|
||||
"ontology/pr_metamodel.ttl": 791,
|
||||
"instances/sodh.ttl": 2304
|
||||
},
|
||||
"triples_after": {
|
||||
"ontology/pr_metamodel.ttl": 791,
|
||||
"instances/sodh.ttl": 2304
|
||||
},
|
||||
"notes": [
|
||||
"provenance_no_axis: SystemType"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"attempt_timestamp": "2026-08-07T10:27:29",
|
||||
"mode": "apply",
|
||||
"from_version": "2.0",
|
||||
"to_version": "2.1",
|
||||
"input_checksums": {
|
||||
"ontology/pr_metamodel.ttl": "32929c506d402738807b8b30c04fbb48",
|
||||
"instances/sodh.ttl": "6bef7bad65c091738cd58cf5f0f1a395"
|
||||
},
|
||||
"steps": {
|
||||
"annotations_declared": 1,
|
||||
"properties_declared": 1,
|
||||
"polymorphic_declared": 19,
|
||||
"domains_declared": 17,
|
||||
"attached": 1,
|
||||
"provenance_stripped": 5,
|
||||
"provenance_declared": 29,
|
||||
"provenance_no_axis": 1,
|
||||
"instances_removed": 4,
|
||||
"instances_moved": 1,
|
||||
"version_bumped": 1
|
||||
},
|
||||
"total_changes": 80,
|
||||
"triples_before": {
|
||||
"ontology/pr_metamodel.ttl": 723,
|
||||
"instances/sodh.ttl": 2308
|
||||
},
|
||||
"triples_after": {
|
||||
"ontology/pr_metamodel.ttl": 791,
|
||||
"instances/sodh.ttl": 2304
|
||||
},
|
||||
"notes": [
|
||||
"annotations_declared: polymorphic",
|
||||
"properties_declared: serviceCommitment",
|
||||
"attached: GovernanceLayerObject -> DefinedObject",
|
||||
"provenance_stripped: storedIn",
|
||||
"provenance_stripped: exposesMetric",
|
||||
"provenance_stripped: DefinedObject",
|
||||
"provenance_stripped: CapturedObject",
|
||||
"provenance_stripped: primarilyStoredIn",
|
||||
"provenance_no_axis: SystemType",
|
||||
"instances_removed: DI_06_01_001 businessDefinition",
|
||||
"instances_removed: DI_06_01_002 businessDefinition",
|
||||
"instances_removed: DI_06_01_001 sourcedFrom",
|
||||
"instances_removed: DI_06_01_002 sourcedFrom",
|
||||
"instances_moved: DC_06_01_001 businessRule",
|
||||
"version_bumped: https://ontology.pernod-ricard.com/metamodel/2.1"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"attempt_timestamp": "2026-08-06T15:17:04",
|
||||
"mode": "dry-run",
|
||||
"from_version": "2.1",
|
||||
"to_version": "2.2",
|
||||
"input_checksums": {
|
||||
"ontology/pr_metamodel.ttl": "d264f924c61faa2fe8caf7be8ec7490c",
|
||||
"instances/sodh.ttl": "135278dd413486cb1273fe193a5aa590"
|
||||
},
|
||||
"steps": {
|
||||
"properties_declared": 1,
|
||||
"display_updated": 1,
|
||||
"instances_renamed": 80,
|
||||
"assignments_completed": 12,
|
||||
"sub_domain_owners_created": 10,
|
||||
"product_owners_created": 1,
|
||||
"stewards_created": 15,
|
||||
"steward_still_referenced": 6,
|
||||
"version_bumped": 1
|
||||
},
|
||||
"total_changes": 127,
|
||||
"triples_before": {
|
||||
"ontology/pr_metamodel.ttl": 791,
|
||||
"instances/sodh.ttl": 2304
|
||||
},
|
||||
"triples_after": {
|
||||
"ontology/pr_metamodel.ttl": 796,
|
||||
"instances/sodh.ttl": 2557
|
||||
},
|
||||
"notes": [
|
||||
"properties_declared: heldBy",
|
||||
"display_updated: DataSteward -> DST",
|
||||
"steward_still_referenced: DST_DD_04 cited by 8 subject(s)",
|
||||
"steward_still_referenced: DST_DD_05 cited by 3 subject(s)",
|
||||
"steward_still_referenced: DST_DD_06 cited by 23 subject(s)",
|
||||
"steward_still_referenced: DST_DD_10 cited by 4 subject(s)",
|
||||
"steward_still_referenced: DST_DD_16 cited by 11 subject(s)",
|
||||
"steward_still_referenced: DST_DD_21 cited by 4 subject(s)",
|
||||
"version_bumped: https://ontology.pernod-ricard.com/metamodel/2.2"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"attempt_timestamp": "2026-08-06T15:23:27",
|
||||
"mode": "dry-run",
|
||||
"from_version": "2.1",
|
||||
"to_version": "2.2",
|
||||
"input_checksums": {
|
||||
"ontology/pr_metamodel.ttl": "d264f924c61faa2fe8caf7be8ec7490c",
|
||||
"instances/sodh.ttl": "135278dd413486cb1273fe193a5aa590"
|
||||
},
|
||||
"steps": {
|
||||
"properties_declared": 1,
|
||||
"display_updated": 1,
|
||||
"instances_renamed": 80,
|
||||
"assignments_completed": 12,
|
||||
"sub_domain_owners_created": 10,
|
||||
"product_owners_created": 1,
|
||||
"stewards_created": 15,
|
||||
"ownedby_removed": 15,
|
||||
"ownedby_repointed_to_sdo": 7,
|
||||
"ownedby_repointed_to_steward": 30,
|
||||
"ownedby_unresolved": 1,
|
||||
"domain_stewards_retired": 5,
|
||||
"steward_still_referenced": 1,
|
||||
"version_bumped": 1
|
||||
},
|
||||
"total_changes": 180,
|
||||
"triples_before": {
|
||||
"ontology/pr_metamodel.ttl": 791,
|
||||
"instances/sodh.ttl": 2304
|
||||
},
|
||||
"triples_after": {
|
||||
"ontology/pr_metamodel.ttl": 796,
|
||||
"instances/sodh.ttl": 2532
|
||||
},
|
||||
"notes": [
|
||||
"properties_declared: heldBy",
|
||||
"display_updated: DataSteward -> DST",
|
||||
"ownedby_unresolved: BC_06_01_003 reaches no business object",
|
||||
"domain_stewards_retired: DST_DD_04",
|
||||
"domain_stewards_retired: DST_DD_05",
|
||||
"steward_still_referenced: DST_DD_06 cited by 1 subject(s)",
|
||||
"domain_stewards_retired: DST_DD_10",
|
||||
"domain_stewards_retired: DST_DD_16",
|
||||
"domain_stewards_retired: DST_DD_21",
|
||||
"version_bumped: https://ontology.pernod-ricard.com/metamodel/2.2"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"attempt_timestamp": "2026-08-06T19:49:37",
|
||||
"mode": "dry-run",
|
||||
"from_version": "2.1",
|
||||
"to_version": "2.2",
|
||||
"input_checksums": {
|
||||
"ontology/pr_metamodel.ttl": "d264f924c61faa2fe8caf7be8ec7490c",
|
||||
"instances/sodh.ttl": "135278dd413486cb1273fe193a5aa590"
|
||||
},
|
||||
"steps": {
|
||||
"properties_declared": 1,
|
||||
"display_updated": 1,
|
||||
"instances_renamed": 80,
|
||||
"assignments_completed": 12,
|
||||
"sub_domain_owners_created": 10,
|
||||
"product_owners_created": 1,
|
||||
"business_objects_created": 1,
|
||||
"stewards_created": 16,
|
||||
"ownedby_repointed_to_steward": 31,
|
||||
"ownedby_removed": 15,
|
||||
"ownedby_repointed_to_sdo": 7,
|
||||
"domain_stewards_retired": 6,
|
||||
"version_bumped": 1
|
||||
},
|
||||
"total_changes": 182,
|
||||
"triples_before": {
|
||||
"ontology/pr_metamodel.ttl": 791,
|
||||
"instances/sodh.ttl": 2304
|
||||
},
|
||||
"triples_after": {
|
||||
"ontology/pr_metamodel.ttl": 796,
|
||||
"instances/sodh.ttl": 2546
|
||||
},
|
||||
"notes": [
|
||||
"properties_declared: heldBy",
|
||||
"display_updated: DataSteward -> DST",
|
||||
"business_objects_created: BO_06_01_006",
|
||||
"domain_stewards_retired: DST_DD_04",
|
||||
"domain_stewards_retired: DST_DD_05",
|
||||
"domain_stewards_retired: DST_DD_06",
|
||||
"domain_stewards_retired: DST_DD_10",
|
||||
"domain_stewards_retired: DST_DD_16",
|
||||
"domain_stewards_retired: DST_DD_21",
|
||||
"version_bumped: https://ontology.pernod-ricard.com/metamodel/2.2"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"attempt_timestamp": "2026-08-06T19:51:46",
|
||||
"mode": "apply",
|
||||
"from_version": "2.1",
|
||||
"to_version": "2.2",
|
||||
"input_checksums": {
|
||||
"ontology/pr_metamodel.ttl": "d264f924c61faa2fe8caf7be8ec7490c",
|
||||
"instances/sodh.ttl": "135278dd413486cb1273fe193a5aa590"
|
||||
},
|
||||
"steps": {
|
||||
"properties_declared": 1,
|
||||
"display_updated": 1,
|
||||
"instances_renamed": 80,
|
||||
"assignments_completed": 12,
|
||||
"sub_domain_owners_created": 10,
|
||||
"product_owners_created": 1,
|
||||
"business_objects_created": 1,
|
||||
"stewards_created": 16,
|
||||
"ownedby_repointed_to_steward": 31,
|
||||
"ownedby_removed": 15,
|
||||
"ownedby_repointed_to_sdo": 7,
|
||||
"domain_stewards_retired": 6,
|
||||
"version_bumped": 1
|
||||
},
|
||||
"total_changes": 182,
|
||||
"triples_before": {
|
||||
"ontology/pr_metamodel.ttl": 791,
|
||||
"instances/sodh.ttl": 2304
|
||||
},
|
||||
"triples_after": {
|
||||
"ontology/pr_metamodel.ttl": 796,
|
||||
"instances/sodh.ttl": 2546
|
||||
},
|
||||
"notes": [
|
||||
"properties_declared: heldBy",
|
||||
"display_updated: DataSteward -> DST",
|
||||
"business_objects_created: BO_06_01_006",
|
||||
"domain_stewards_retired: DST_DD_04",
|
||||
"domain_stewards_retired: DST_DD_05",
|
||||
"domain_stewards_retired: DST_DD_06",
|
||||
"domain_stewards_retired: DST_DD_10",
|
||||
"domain_stewards_retired: DST_DD_16",
|
||||
"domain_stewards_retired: DST_DD_21",
|
||||
"version_bumped: https://ontology.pernod-ricard.com/metamodel/2.2"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"attempt_timestamp": "2026-08-06T19:51:51",
|
||||
"mode": "dry-run",
|
||||
"from_version": "2.1",
|
||||
"to_version": "2.2",
|
||||
"input_checksums": {
|
||||
"ontology/pr_metamodel.ttl": "e705cde06956d746631e412719b6cc6d",
|
||||
"instances/sodh.ttl": "82ced2220c1e0525c1dcd5028afbc546"
|
||||
},
|
||||
"steps": {
|
||||
"instances_renamed": 0
|
||||
},
|
||||
"total_changes": 0,
|
||||
"triples_before": {
|
||||
"ontology/pr_metamodel.ttl": 796,
|
||||
"instances/sodh.ttl": 2546
|
||||
},
|
||||
"triples_after": {
|
||||
"ontology/pr_metamodel.ttl": 796,
|
||||
"instances/sodh.ttl": 2546
|
||||
},
|
||||
"notes": []
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"attempt_timestamp": "2026-08-07T10:21:51",
|
||||
"mode": "apply",
|
||||
"from_version": "2.1",
|
||||
"to_version": "2.2",
|
||||
"input_checksums": {
|
||||
"ontology/pr_metamodel.ttl": "32929c506d402738807b8b30c04fbb48",
|
||||
"instances/sodh.ttl": "6bef7bad65c091738cd58cf5f0f1a395"
|
||||
},
|
||||
"steps": {
|
||||
"properties_declared": 1,
|
||||
"display_updated": 1,
|
||||
"instances_renamed": 80,
|
||||
"assignments_completed": 12,
|
||||
"sub_domain_owners_created": 10,
|
||||
"product_owners_created": 1,
|
||||
"business_objects_created": 1,
|
||||
"stewards_created": 16,
|
||||
"ownedby_repointed_to_steward": 31,
|
||||
"ownedby_removed": 15,
|
||||
"ownedby_repointed_to_sdo": 7,
|
||||
"domain_stewards_retired": 6,
|
||||
"version_bumped": 1
|
||||
},
|
||||
"total_changes": 182,
|
||||
"triples_before": {
|
||||
"ontology/pr_metamodel.ttl": 723,
|
||||
"instances/sodh.ttl": 2308
|
||||
},
|
||||
"triples_after": {
|
||||
"ontology/pr_metamodel.ttl": 729,
|
||||
"instances/sodh.ttl": 2550
|
||||
},
|
||||
"notes": [
|
||||
"properties_declared: personName",
|
||||
"display_updated: DataSteward -> DST",
|
||||
"business_objects_created: BO_06_01_006",
|
||||
"domain_stewards_retired: DST_DD_04",
|
||||
"domain_stewards_retired: DST_DD_05",
|
||||
"domain_stewards_retired: DST_DD_06",
|
||||
"domain_stewards_retired: DST_DD_10",
|
||||
"domain_stewards_retired: DST_DD_16",
|
||||
"domain_stewards_retired: DST_DD_21",
|
||||
"version_bumped: https://ontology.pernod-ricard.com/metamodel/2.2"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"attempt_timestamp": "2026-08-07T10:21:59",
|
||||
"mode": "dry-run",
|
||||
"from_version": "2.1",
|
||||
"to_version": "2.2",
|
||||
"input_checksums": {
|
||||
"ontology/pr_metamodel.ttl": "6f2deb192272d0bceecac31fcb525ad1",
|
||||
"instances/sodh.ttl": "7c0d42f33590d2208b0af7b2068f559a"
|
||||
},
|
||||
"steps": {
|
||||
"instances_renamed": 0
|
||||
},
|
||||
"total_changes": 0,
|
||||
"triples_before": {
|
||||
"ontology/pr_metamodel.ttl": 729,
|
||||
"instances/sodh.ttl": 2550
|
||||
},
|
||||
"triples_after": {
|
||||
"ontology/pr_metamodel.ttl": 729,
|
||||
"instances/sodh.ttl": 2550
|
||||
},
|
||||
"notes": []
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"attempt_timestamp": "2026-08-07T10:27:33",
|
||||
"mode": "apply",
|
||||
"from_version": "2.1",
|
||||
"to_version": "2.2",
|
||||
"input_checksums": {
|
||||
"ontology/pr_metamodel.ttl": "96687771a06f85759e7b894f9d248083",
|
||||
"instances/sodh.ttl": "135278dd413486cb1273fe193a5aa590"
|
||||
},
|
||||
"steps": {
|
||||
"properties_declared": 1,
|
||||
"display_updated": 1,
|
||||
"instances_renamed": 80,
|
||||
"assignments_completed": 12,
|
||||
"sub_domain_owners_created": 10,
|
||||
"product_owners_created": 1,
|
||||
"business_objects_created": 1,
|
||||
"stewards_created": 16,
|
||||
"ownedby_repointed_to_steward": 31,
|
||||
"ownedby_removed": 15,
|
||||
"ownedby_repointed_to_sdo": 7,
|
||||
"domain_stewards_retired": 6,
|
||||
"version_bumped": 1
|
||||
},
|
||||
"total_changes": 182,
|
||||
"triples_before": {
|
||||
"ontology/pr_metamodel.ttl": 791,
|
||||
"instances/sodh.ttl": 2304
|
||||
},
|
||||
"triples_after": {
|
||||
"ontology/pr_metamodel.ttl": 797,
|
||||
"instances/sodh.ttl": 2546
|
||||
},
|
||||
"notes": [
|
||||
"properties_declared: personName",
|
||||
"display_updated: DataSteward -> DST",
|
||||
"business_objects_created: BO_06_01_006",
|
||||
"domain_stewards_retired: DST_DD_04",
|
||||
"domain_stewards_retired: DST_DD_05",
|
||||
"domain_stewards_retired: DST_DD_06",
|
||||
"domain_stewards_retired: DST_DD_10",
|
||||
"domain_stewards_retired: DST_DD_16",
|
||||
"domain_stewards_retired: DST_DD_21",
|
||||
"version_bumped: https://ontology.pernod-ricard.com/metamodel/2.2"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,296 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Migrate the Pernod Ricard Data MetaModel from v2.0 to v2.1.
|
||||
|
||||
USAGE
|
||||
python3 migrate_tbox_v2_1.py # dry run, writes nothing
|
||||
python3 migrate_tbox_v2_1.py --apply
|
||||
|
||||
v2.1 renames nothing. It declares what v2.0 left silent: which properties are
|
||||
polymorphic on purpose, what domain the others take, and how the instances of
|
||||
each concrete class are produced.
|
||||
|
||||
DESIGN
|
||||
EV-004 dry run is the default
|
||||
EV-005 every edit goes through rdflib
|
||||
EV-006 guards test the target state, so a replay reports zero change
|
||||
EV-015 an execution log is written for every attempt
|
||||
"""
|
||||
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
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
SPEC = os.path.join(HERE, "declarations_v2_1.yaml")
|
||||
|
||||
|
||||
class Report(object):
|
||||
def __init__(self):
|
||||
self.steps = OrderedDict()
|
||||
self.notes = []
|
||||
|
||||
def add(self, step, n=1, 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 fh:
|
||||
for chunk in iter(lambda: fh.read(65536), b""):
|
||||
h.update(chunk)
|
||||
return h.hexdigest()
|
||||
|
||||
|
||||
def derive_label(name):
|
||||
"""TN-018 and TN-023: a label is derived from the local name, never typed."""
|
||||
spaced = re.sub(r"(?<=[a-z0-9])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])", " ", name)
|
||||
return spaced[0].lower() + spaced[1:]
|
||||
|
||||
|
||||
def local(uri, ns):
|
||||
s = str(uri)
|
||||
return s[len(ns):] if s.startswith(ns) else None
|
||||
|
||||
|
||||
def ancestors(g, term):
|
||||
seen, stack = set(), [term]
|
||||
while stack:
|
||||
node = stack.pop()
|
||||
for parent in g.objects(node, RDFS.subClassOf):
|
||||
if isinstance(parent, URIRef) and parent not in seen:
|
||||
seen.add(parent)
|
||||
stack.append(parent)
|
||||
return seen
|
||||
|
||||
|
||||
def check_version(g, spec, path):
|
||||
"""EV-010 applied to a migration script: refuse to run against the wrong input.
|
||||
|
||||
A script announcing 2.1 -> 2.2 will happily work on a file still in 2.0 and
|
||||
report success, leaving a state that is neither version. The same silent
|
||||
failure the shapes are guarded against, one layer down.
|
||||
"""
|
||||
ns = spec["meta"]["namespace"]
|
||||
expected = URIRef(ns.rstrip("/") + "/" + spec["meta"]["from_version"])
|
||||
target = URIRef(ns.rstrip("/") + "/" + spec["meta"]["to_version"])
|
||||
found = set(g.objects(None, OWL.versionIRI))
|
||||
if target in found:
|
||||
print("ALREADY AT %s — nothing to do." % spec["meta"]["to_version"])
|
||||
sys.exit(0)
|
||||
if expected not in found:
|
||||
print("ABORT — %s declares %s; this script migrates from %s."
|
||||
% (os.path.basename(path),
|
||||
", ".join(sorted(str(f) for f in found)) or "no version",
|
||||
expected))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def migrate(g, instances, spec, report):
|
||||
ns = spec["meta"]["namespace"]
|
||||
pr = Namespace(ns)
|
||||
|
||||
# 1 — declare the annotation property itself
|
||||
for a in spec.get("declare_annotations") or []:
|
||||
term = pr[a["term"]]
|
||||
if (term, RDF.type, OWL.AnnotationProperty) not in g:
|
||||
g.add((term, RDF.type, OWL.AnnotationProperty))
|
||||
g.add((term, RDFS.label, Literal(a["label"])))
|
||||
g.add((term, RDFS.comment, Literal(" ".join(a["comment"].split()))))
|
||||
report.add("annotations_declared", 1, a["term"])
|
||||
|
||||
# 2 — declare the new properties (procedure C1)
|
||||
for d in spec.get("declare_properties") or []:
|
||||
term = pr[d["term"]]
|
||||
kind = getattr(OWL, d["kind"])
|
||||
if (term, RDF.type, kind) not in g:
|
||||
g.add((term, RDF.type, kind))
|
||||
g.add((term, RDFS.label, Literal(derive_label(d["term"]))))
|
||||
g.add((term, RDFS.domain, pr[d["domain"]]))
|
||||
g.add((term, RDFS.range, URIRef(d["range"])))
|
||||
g.add((term, RDFS.comment, Literal(" ".join(d["comment"].split()))))
|
||||
report.add("properties_declared", 1, d["term"])
|
||||
|
||||
# 3 — mark the deliberately polymorphic properties (TN-025)
|
||||
for p in spec.get("polymorphic") or []:
|
||||
term = pr[p["term"]]
|
||||
if (term, None, None) not in g:
|
||||
report.add("polymorphic_missing", 1, p["term"])
|
||||
continue
|
||||
if (term, pr.polymorphic, Literal(True)) not in g:
|
||||
g.add((term, pr.polymorphic, Literal(True)))
|
||||
report.add("polymorphic_declared", 1)
|
||||
if list(g.objects(term, RDFS.domain)):
|
||||
report.add("polymorphic_has_domain", 1,
|
||||
"%s is marked polymorphic and still declares a domain" % p["term"])
|
||||
|
||||
# 4 — declare the domains that were merely missing (TN-025)
|
||||
for d in spec.get("domains") or []:
|
||||
term, target = pr[d["term"]], pr[d["domain"]]
|
||||
if (term, None, None) not in g:
|
||||
report.add("domain_missing_term", 1, d["term"])
|
||||
continue
|
||||
if (target, RDF.type, OWL.Class) not in g:
|
||||
report.add("domain_missing_class", 1, "%s -> %s" % (d["term"], d["domain"]))
|
||||
continue
|
||||
if (term, RDFS.domain, target) not in g:
|
||||
g.remove((term, RDFS.domain, None))
|
||||
g.add((term, RDFS.domain, target))
|
||||
report.add("domains_declared", 1)
|
||||
|
||||
# 5 — ranges, where a polymorphic property still points at one class
|
||||
for r in spec.get("ranges") or []:
|
||||
term, target = pr[r["term"]], pr[r["range"]]
|
||||
if (term, RDFS.range, target) not in g:
|
||||
g.remove((term, RDFS.range, None))
|
||||
g.add((term, RDFS.range, target))
|
||||
report.add("ranges_declared", 1)
|
||||
|
||||
# 6 — attach a layer root to its provenance axis (TN-027)
|
||||
for a in spec.get("attach") or []:
|
||||
term, parent = pr[a["term"]], pr[a["parent"]]
|
||||
if (term, RDFS.subClassOf, parent) not in g:
|
||||
g.add((term, RDFS.subClassOf, parent))
|
||||
report.add("attached", 1, "%s -> %s" % (a["term"], a["parent"]))
|
||||
|
||||
# 7 — provenance belongs to the concrete classes, and to nothing else (TN-026)
|
||||
prov = spec.get("provenance") or {}
|
||||
concrete = {c for c in g.subjects(RDF.type, OWL.Class)
|
||||
if (c, pr.isAbstract, Literal(False)) in g}
|
||||
|
||||
if prov.get("strip_from_non_concrete"):
|
||||
for s in set(g.subjects(pr.authoringMode, None)):
|
||||
if s in concrete:
|
||||
continue
|
||||
g.remove((s, pr.authoringMode, None))
|
||||
g.remove((s, pr.harvestSource, None))
|
||||
report.add("provenance_stripped", 1, local(s, ns))
|
||||
|
||||
if prov.get("derive_from_axis"):
|
||||
for cls in sorted(concrete, key=str):
|
||||
up = ancestors(g, cls)
|
||||
if pr.CapturedObject in up:
|
||||
want = "HARVESTED"
|
||||
elif pr.DefinedObject in up:
|
||||
want = "ASSERTED"
|
||||
else:
|
||||
report.add("provenance_no_axis", 1, local(cls, ns))
|
||||
continue
|
||||
if (cls, pr.authoringMode, Literal(want)) in g:
|
||||
continue
|
||||
g.remove((cls, pr.authoringMode, None))
|
||||
g.add((cls, pr.authoringMode, Literal(want)))
|
||||
report.add("provenance_declared", 1)
|
||||
|
||||
# 8 — instance corrections, each one surfaced by declaring a domain
|
||||
ex = Namespace("https://data.pernod-ricard.com/sodh/")
|
||||
for fix in spec.get("instance_fixes") or []:
|
||||
prop = pr[fix["property"]]
|
||||
dest = pr[fix["to"]] if fix.get("to") else None
|
||||
for subject in fix["subjects"]:
|
||||
node = ex[subject]
|
||||
for gi in instances:
|
||||
for _, _, value in list(gi.triples((node, prop, None))):
|
||||
gi.remove((node, prop, value))
|
||||
if dest is not None:
|
||||
gi.add((node, dest, value))
|
||||
report.add("instances_%sd" % fix["action"], 1,
|
||||
"%s %s" % (subject, fix["property"]))
|
||||
|
||||
# 9 — bump the version (EV-014: the namespace itself never moves)
|
||||
target = URIRef(ns.rstrip("/") + "/" + spec["meta"]["to_version"])
|
||||
for onto in set(g.subjects(RDF.type, OWL.Ontology)):
|
||||
if (onto, OWL.versionIRI, target) not in g:
|
||||
g.remove((onto, OWL.versionIRI, None))
|
||||
g.add((onto, OWL.versionIRI, target))
|
||||
report.add("version_bumped", 1, str(target))
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--apply", action="store_true")
|
||||
ap.add_argument("--ontology", default=os.path.join(HERE, "..", "..", "ontology",
|
||||
"pr_metamodel.ttl"))
|
||||
ap.add_argument("--instances", action="append", default=[])
|
||||
ap.add_argument("--log-dir", default=os.path.join(HERE, "logs"))
|
||||
args = ap.parse_args()
|
||||
|
||||
if not os.path.exists(args.ontology):
|
||||
print("MISSING INPUT: %s" % args.ontology)
|
||||
sys.exit(1)
|
||||
|
||||
spec = yaml.safe_load(open(SPEC, encoding="utf-8"))
|
||||
checksums = OrderedDict((p, md5(p)) for p in [args.ontology] + args.instances)
|
||||
|
||||
g = Graph()
|
||||
g.parse(args.ontology, format="turtle")
|
||||
instances = []
|
||||
for path in args.instances:
|
||||
gi = Graph()
|
||||
gi.parse(path, format="turtle")
|
||||
instances.append(gi)
|
||||
check_version(g, spec, args.ontology)
|
||||
|
||||
before = [len(g)] + [len(x) for x in instances]
|
||||
|
||||
report = Report()
|
||||
migrate(g, instances, spec, report)
|
||||
after = [len(g)] + [len(x) for x in instances]
|
||||
|
||||
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 path, b, a in zip([args.ontology] + args.instances, before, after):
|
||||
print(" %-46s %6d -> %6d triples" % (os.path.basename(path), b, a))
|
||||
if report.notes:
|
||||
print()
|
||||
for n in report.notes:
|
||||
print(" note: " + n)
|
||||
|
||||
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_v2_1_%s.json" % stamp)
|
||||
json.dump({"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": checksums,
|
||||
"steps": report.steps, "total_changes": report.total,
|
||||
"triples_before": dict(zip([args.ontology] + args.instances, before)),
|
||||
"triples_after": dict(zip([args.ontology] + args.instances, after)),
|
||||
"notes": report.notes},
|
||||
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
|
||||
|
||||
g.serialize(destination=args.ontology, format="turtle")
|
||||
for path, gi in zip(args.instances, instances):
|
||||
gi.serialize(destination=path, 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,367 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Migrate the Pernod Ricard Data MetaModel and the SODH instances to v2.2.
|
||||
|
||||
USAGE
|
||||
python3 migrate_tbox_v2_2.py --ontology ... --instances ...
|
||||
python3 migrate_tbox_v2_2.py --ontology ... --instances ... --apply
|
||||
|
||||
v2.2 turns the actor nodes into governance ASSIGNMENTS: a stable post carrying
|
||||
an identifier, a domain and a name, plus the person holding it as a value. It
|
||||
completes the twelve that existed, creates the ten sub-domain owners, the
|
||||
product owner, and one steward assignment per business object.
|
||||
|
||||
DESIGN
|
||||
EV-004 dry run is the default
|
||||
EV-005 every edit goes through rdflib
|
||||
EV-006 guards test the target state, so a replay reports zero change
|
||||
EV-015 an execution log is written for every attempt
|
||||
"""
|
||||
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
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
SPEC = os.path.join(HERE, "assignments_v2_2.yaml")
|
||||
|
||||
|
||||
class Report(object):
|
||||
def __init__(self):
|
||||
self.steps = OrderedDict()
|
||||
self.notes = []
|
||||
|
||||
def add(self, step, n=1, 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 derive_label(name):
|
||||
"""TN-018 and TN-023: a label is derived from the local name, never typed."""
|
||||
spaced = re.sub(r"(?<=[a-z0-9])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])", " ", name)
|
||||
return spaced[0].lower() + spaced[1:]
|
||||
|
||||
|
||||
def md5(path):
|
||||
h = hashlib.md5()
|
||||
with open(path, "rb") as fh:
|
||||
for chunk in iter(lambda: fh.read(65536), b""):
|
||||
h.update(chunk)
|
||||
return h.hexdigest()
|
||||
|
||||
|
||||
def rename_node(g, old, new, report, step):
|
||||
"""EV-001 applied to an instance: rewrite every triple naming the node."""
|
||||
if (new, None, None) in g and (old, None, None) not in g:
|
||||
return 0
|
||||
n = 0
|
||||
for s, p, o in list(g):
|
||||
ns, no = (new if s == old else s), (new if o == old else o)
|
||||
if (ns, no) != (s, o):
|
||||
g.remove((s, p, o))
|
||||
g.add((ns, p, no))
|
||||
n += 1
|
||||
report.add(step, n)
|
||||
return n
|
||||
|
||||
|
||||
def assign(g, pr, node, role, identifier, name, held_by, domain, spec, report, step):
|
||||
"""Write one assignment. The guard tests the identifier, so a replay is a no-op."""
|
||||
if (node, pr.identifier, Literal(identifier)) in g:
|
||||
return False
|
||||
g.add((node, RDF.type, role))
|
||||
g.remove((node, pr.identifier, None))
|
||||
g.add((node, pr.identifier, Literal(identifier)))
|
||||
g.remove((node, pr.canonicalName, None))
|
||||
g.add((node, pr.canonicalName, Literal(name)))
|
||||
g.remove((node, pr.personName, None))
|
||||
if held_by:
|
||||
g.add((node, pr.personName, Literal(held_by)))
|
||||
g.remove((node, pr.status, None))
|
||||
g.add((node, pr.status, Literal(spec["status"])))
|
||||
g.remove((node, pr.version, None))
|
||||
g.add((node, pr.version, Literal(spec["version"])))
|
||||
if domain is not None:
|
||||
g.remove((node, pr.ownedByDomain, None))
|
||||
g.add((node, pr.ownedByDomain, domain))
|
||||
report.add(step, 1)
|
||||
return True
|
||||
|
||||
|
||||
def check_version(g, spec, path):
|
||||
"""EV-010 applied to a migration script: refuse to run against the wrong input.
|
||||
|
||||
A script announcing 2.1 -> 2.2 will happily work on a file still in 2.0 and
|
||||
report success, leaving a state that is neither version. The same silent
|
||||
failure the shapes are guarded against, one layer down.
|
||||
"""
|
||||
ns = spec["meta"]["namespace"]
|
||||
expected = URIRef(ns.rstrip("/") + "/" + spec["meta"]["from_version"])
|
||||
target = URIRef(ns.rstrip("/") + "/" + spec["meta"]["to_version"])
|
||||
found = set(g.objects(None, OWL.versionIRI))
|
||||
if target in found:
|
||||
print("ALREADY AT %s — nothing to do." % spec["meta"]["to_version"])
|
||||
sys.exit(0)
|
||||
if expected not in found:
|
||||
print("ABORT — %s declares %s; this script migrates from %s."
|
||||
% (os.path.basename(path),
|
||||
", ".join(sorted(str(f) for f in found)) or "no version",
|
||||
expected))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def migrate(onto, data, spec, report):
|
||||
ns, ins = spec["meta"]["namespace"], spec["meta"]["instance_namespace"]
|
||||
pr, ex = Namespace(ns), Namespace(ins)
|
||||
|
||||
# 1 — declare heldBy on Actor (procedure C1)
|
||||
for d in spec.get("declare_properties") or []:
|
||||
term = pr[d["term"]]
|
||||
if (term, RDF.type, getattr(OWL, d["kind"])) not in onto:
|
||||
onto.add((term, RDF.type, getattr(OWL, d["kind"])))
|
||||
onto.add((term, RDFS.label, Literal(derive_label(d["term"]))))
|
||||
onto.add((term, RDFS.domain, pr[d["domain"]]))
|
||||
onto.add((term, RDFS.range, URIRef(d["range"])))
|
||||
onto.add((term, RDFS.comment, Literal(" ".join(d["comment"].split()))))
|
||||
report.add("properties_declared", 1, d["term"])
|
||||
|
||||
# 2 — the short form of Data Steward is DST, and the rulebook now says so
|
||||
for u in spec.get("display_updates") or []:
|
||||
term = pr[u["term"]]
|
||||
if (term, pr.acronym, Literal(u["acronym"])) not in onto:
|
||||
onto.remove((term, pr.acronym, None))
|
||||
onto.add((term, pr.acronym, Literal(u["acronym"])))
|
||||
report.add("display_updated", 1, "%s -> %s" % (u["term"], u["acronym"]))
|
||||
|
||||
# 3 — rename the instance nodes that carried the wrong short form
|
||||
for r in spec.get("instance_renames") or []:
|
||||
rename_node(data, ex[r["from"]], ex[r["to"]], report, "instances_renamed")
|
||||
|
||||
# 4 — complete the assignments that already existed
|
||||
for c in spec.get("complete") or []:
|
||||
node = ex[c["node"]]
|
||||
if (node, None, None) not in data:
|
||||
report.add("complete_missing", 1, c["node"])
|
||||
continue
|
||||
role = next(iter(data.objects(node, RDF.type)), None)
|
||||
assign(data, pr, node, role, c["identifier"], c["name"],
|
||||
c["held_by"], ex[c["domain"]], spec, report, "assignments_completed")
|
||||
|
||||
# 5 — sub-domain owners
|
||||
sdo_of = {}
|
||||
for s in spec.get("create_sub_domain_owners") or []:
|
||||
node = ex[s["node"]]
|
||||
sdo_of[ex[s["sub_domain"]]] = node
|
||||
if assign(data, pr, node, pr.DataSubDomainOwner, s["identifier"],
|
||||
spec["sub_domain_owner_name"], s["held_by"], ex[s["domain"]],
|
||||
spec, report, "sub_domain_owners_created"):
|
||||
data.add((ex[s["sub_domain"]], pr.hasSubDomainOwner, node))
|
||||
|
||||
# 6 — product owners
|
||||
for p in spec.get("create_product_owners") or []:
|
||||
node = ex[p["node"]]
|
||||
product = ex[p["product"]]
|
||||
if (product, None, None) not in data:
|
||||
report.add("product_missing", 1, p["product"])
|
||||
continue
|
||||
if assign(data, pr, node, pr.DataProductOwner, p["identifier"],
|
||||
spec["product_owner_name"], p["held_by"], ex[p["domain"]],
|
||||
spec, report, "product_owners_created"):
|
||||
data.add((product, pr.hasProductOwner, node))
|
||||
|
||||
# 7 — create the business objects that close a governance chain
|
||||
for b in spec.get("create_business_objects") or []:
|
||||
node = ex[b["node"]]
|
||||
if (node, pr.identifier, Literal(b["identifier"])) in data:
|
||||
continue
|
||||
data.add((node, RDF.type, pr.BusinessObject))
|
||||
data.add((node, pr.identifier, Literal(b["identifier"])))
|
||||
data.add((node, pr.canonicalName, Literal(b["name"])))
|
||||
data.add((node, pr.isAbout, ex[b["is_about"]]))
|
||||
data.add((node, pr.belongsTo, ex[b["sub_domain"]]))
|
||||
data.add((node, pr.ownedByDomain, ex[b["domain"]]))
|
||||
data.add((node, pr.status, Literal(spec["status"])))
|
||||
data.add((node, pr.version, Literal(spec["version"])))
|
||||
report.add("business_objects_created", 1, b["node"])
|
||||
|
||||
# 8 — one steward assignment per business object, derived from the graph
|
||||
holders = spec.get("steward_holders") or {}
|
||||
steward_of = {}
|
||||
for bo in sorted(data.subjects(RDF.type, pr.BusinessObject), key=str):
|
||||
ident = next((str(v) for v in data.objects(bo, pr.identifier)), None)
|
||||
if not ident:
|
||||
report.add("steward_no_identifier", 1, str(bo).replace(ins, ""))
|
||||
continue
|
||||
suffix = ident.split("-", 1)[1] # 06.01-001
|
||||
domain_key = "DD_" + suffix.split(".", 1)[0] # DD_06
|
||||
holder = holders.get(domain_key)
|
||||
if holder is None:
|
||||
report.add("steward_no_holder", 1, domain_key)
|
||||
continue
|
||||
node = ex["DST_" + suffix.replace(".", "_").replace("-", "_")]
|
||||
steward_of[bo] = node
|
||||
if assign(data, pr, node, pr.DataSteward, "DST-" + suffix,
|
||||
spec["steward_name"], holder, ex[domain_key],
|
||||
spec, report, "stewards_created"):
|
||||
for _, _, old in list(data.triples((bo, pr.monitoredBy, None))):
|
||||
data.remove((bo, pr.monitoredBy, old))
|
||||
data.add((bo, pr.monitoredBy, node))
|
||||
|
||||
# 9 — resolve every ownedBy pointing at a per-domain steward node
|
||||
res = spec.get("owned_by_resolution") or {}
|
||||
old_stewards = {ex[r["from"]] for r in (spec.get("instance_renames") or [])} | \
|
||||
{ex[r["to"]] for r in (spec.get("instance_renames") or [])}
|
||||
|
||||
def business_object_of(subject):
|
||||
"""Follow the declared chains until a business object is reached."""
|
||||
for chain in res.get("chains") or []:
|
||||
prop = pr[chain["via"]]
|
||||
if chain["direction"] == "inbound":
|
||||
for candidate in data.subjects(prop, subject):
|
||||
if (candidate, RDF.type, pr.BusinessObject) in data:
|
||||
return candidate
|
||||
else:
|
||||
for target in data.objects(subject, prop):
|
||||
if (target, RDF.type, pr.BusinessObject) in data:
|
||||
return target
|
||||
for candidate in data.subjects(pr.isAbout, target):
|
||||
if (candidate, RDF.type, pr.BusinessObject) in data:
|
||||
return candidate
|
||||
return None
|
||||
|
||||
remove_on = {pr[c] for c in (res.get("remove_on") or [])}
|
||||
for subject, _, target in list(data.triples((None, pr.ownedBy, None))):
|
||||
if target not in old_stewards:
|
||||
continue
|
||||
types = set(data.objects(subject, RDF.type))
|
||||
if types & remove_on:
|
||||
data.remove((subject, pr.ownedBy, target))
|
||||
report.add("ownedby_removed", 1)
|
||||
continue
|
||||
if pr.DataSubDomain in types and res.get("repoint_sub_domains"):
|
||||
owner = sdo_of.get(subject)
|
||||
if owner is None:
|
||||
report.add("ownedby_unresolved", 1,
|
||||
"%s has no sub-domain owner" % str(subject).replace(ins, ""))
|
||||
continue
|
||||
data.remove((subject, pr.ownedBy, target))
|
||||
data.add((subject, pr.ownedBy, owner))
|
||||
report.add("ownedby_repointed_to_sdo", 1)
|
||||
continue
|
||||
bo = business_object_of(subject)
|
||||
owner = steward_of.get(bo) if bo is not None else None
|
||||
if owner is None:
|
||||
report.add("ownedby_unresolved", 1,
|
||||
"%s reaches no business object" % str(subject).replace(ins, ""))
|
||||
continue
|
||||
data.remove((subject, pr.ownedBy, target))
|
||||
data.add((subject, pr.ownedBy, owner))
|
||||
report.add("ownedby_repointed_to_steward", 1)
|
||||
|
||||
# 10 — withdraw the per-domain steward nodes, once nothing names them (EV-003)
|
||||
if spec.get("retire_domain_stewards"):
|
||||
for r in spec.get("instance_renames") or []:
|
||||
node = ex[r["to"]]
|
||||
referrers = {s for s in data.subjects(None, node)}
|
||||
if referrers:
|
||||
report.add("steward_still_referenced", 1,
|
||||
"%s cited by %d subject(s)" % (r["to"], len(referrers)))
|
||||
continue
|
||||
n = 0
|
||||
for t in list(data.triples((node, None, None))):
|
||||
data.remove(t)
|
||||
n += 1
|
||||
if n:
|
||||
report.add("domain_stewards_retired", 1, r["to"])
|
||||
|
||||
# 11 — bump the version
|
||||
target = URIRef(ns.rstrip("/") + "/" + spec["meta"]["to_version"])
|
||||
for o in set(onto.subjects(RDF.type, OWL.Ontology)):
|
||||
if (o, OWL.versionIRI, target) not in onto:
|
||||
onto.remove((o, OWL.versionIRI, None))
|
||||
onto.add((o, OWL.versionIRI, target))
|
||||
report.add("version_bumped", 1, str(target))
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--apply", action="store_true")
|
||||
ap.add_argument("--ontology", required=True)
|
||||
ap.add_argument("--instances", required=True)
|
||||
ap.add_argument("--log-dir", default=os.path.join(HERE, "logs"))
|
||||
args = ap.parse_args()
|
||||
|
||||
for path in (args.ontology, args.instances):
|
||||
if not os.path.exists(path):
|
||||
print("MISSING INPUT: %s" % path)
|
||||
sys.exit(1)
|
||||
|
||||
spec = yaml.safe_load(open(SPEC, encoding="utf-8"))
|
||||
checksums = OrderedDict((p, md5(p)) for p in (args.ontology, args.instances))
|
||||
|
||||
onto, data = Graph(), Graph()
|
||||
onto.parse(args.ontology, format="turtle")
|
||||
data.parse(args.instances, format="turtle")
|
||||
check_version(onto, spec, args.ontology)
|
||||
|
||||
before = (len(onto), len(data))
|
||||
|
||||
report = Report()
|
||||
migrate(onto, data, spec, report)
|
||||
after = (len(onto), len(data))
|
||||
|
||||
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 path, b, a in zip((args.ontology, args.instances), before, after):
|
||||
print(" %-46s %6d -> %6d triples" % (os.path.basename(path), b, a))
|
||||
if report.notes:
|
||||
print()
|
||||
for n in report.notes:
|
||||
print(" note: " + n)
|
||||
|
||||
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_v2_2_%s.json" % stamp)
|
||||
json.dump({"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": checksums,
|
||||
"steps": report.steps, "total_changes": report.total,
|
||||
"triples_before": dict(zip((args.ontology, args.instances), before)),
|
||||
"triples_after": dict(zip((args.ontology, args.instances), after)),
|
||||
"notes": report.notes},
|
||||
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")
|
||||
data.serialize(destination=args.instances, 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,152 @@
|
||||
# NAMING LEXICON
|
||||
# =============================================================================
|
||||
# The word knowledge the naming checks need and cannot derive. No script can
|
||||
# decide on its own that "qualified" is a participle and "ordinal" an adjective.
|
||||
#
|
||||
# Anything absent from this file is not a violation: it is reported as REVIEW.
|
||||
# A checker that guessed would either block on false positives or, worse, stay
|
||||
# silent on real ones. Adding a word here is a deliberate act, reviewed like any
|
||||
# other change to the vocabulary.
|
||||
#
|
||||
# Consumed by check_tbox_naming.py.
|
||||
# =============================================================================
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# VERBS — TN-009. A relation begins with one of these.
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
active_verbs:
|
||||
- belongs
|
||||
- computes
|
||||
- consumes
|
||||
- constrains
|
||||
- contains
|
||||
- depends
|
||||
- exposes
|
||||
- has
|
||||
- lives
|
||||
- measures
|
||||
- packages
|
||||
- produces
|
||||
- references
|
||||
- represents
|
||||
- serves
|
||||
- sources
|
||||
- stores
|
||||
- uses
|
||||
|
||||
# A past participle is a verb in first position (TN-009). Listed separately
|
||||
# because the two forms read differently on an edge: the active says what the
|
||||
# subject does, the participial what was done to it.
|
||||
participles:
|
||||
- computed
|
||||
- derived
|
||||
- governed
|
||||
- hosted
|
||||
- monitored
|
||||
- operated
|
||||
- owned
|
||||
- packaged
|
||||
- qualified
|
||||
- served
|
||||
- sourced
|
||||
- stored
|
||||
|
||||
# The copula, admitted by TN-010 on object properties only.
|
||||
copula:
|
||||
- is
|
||||
|
||||
# TN-009: an adverb qualifying the verb is transparent. The checker skips it and
|
||||
# tests the token that follows.
|
||||
adverbs:
|
||||
- primarily
|
||||
- fully
|
||||
- partially
|
||||
- directly
|
||||
- initially
|
||||
- currently
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# ACRONYMS — TN-002 and TN-019.
|
||||
# Forbidden in an IRI, with the single exception of TN-011: a relation
|
||||
# reproducing the short label of the class it targets carries it as it stands.
|
||||
# Listed so that the checker names what it found rather than reporting an
|
||||
# anonymous run of capitals.
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
known_acronyms:
|
||||
BI: Business Intelligence
|
||||
KPI: Key Performance Indicator
|
||||
DD: Data Domain
|
||||
SD: Data Sub Domain
|
||||
BO: Business Object
|
||||
DDO: Data Domain Owner
|
||||
SDO: Data Sub Domain Owner
|
||||
PO: Data Product Owner
|
||||
DGL: Data Governance Lead
|
||||
URI: Uniform Resource Identifier
|
||||
URL: Uniform Resource Locator
|
||||
API: Application Programming Interface
|
||||
SQL: Structured Query Language
|
||||
ETL: Extract Transform Load
|
||||
UAT: User Acceptance Testing
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# TYPE WORDS — TN-005. A class name carries none of these unless the word names
|
||||
# a genuine abstraction of the model, which the exceptions below record.
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
type_words:
|
||||
- Entity
|
||||
- Item
|
||||
- Element
|
||||
- Thing
|
||||
- Record
|
||||
|
||||
type_word_exceptions:
|
||||
- MetaModelObject
|
||||
- DefinedObject
|
||||
- CapturedObject
|
||||
- OwnershipLayerObject
|
||||
- BusinessLayerObject
|
||||
- LogicalLayerObject
|
||||
- PhysicalLayerObject
|
||||
- DeliveryLayerObject
|
||||
- ConsumptionLayerObject
|
||||
- GovernanceLayerObject
|
||||
- BusinessObject
|
||||
- DataElement
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# PLURALS — TN-005. A class name is singular. English plural detection is
|
||||
# unreliable, so the checker reports a suspected plural as REVIEW and this list
|
||||
# silences the words that only look plural.
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
not_plural:
|
||||
- Status
|
||||
- Address
|
||||
- Business
|
||||
- Analysis
|
||||
- Schema
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# RELATIONAL SUFFIXES — TN-014. Reserved for object properties. An attribute
|
||||
# ending in one of these reads as an edge and will be mistaken for one.
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
relational_suffixes:
|
||||
- By
|
||||
- In
|
||||
- From
|
||||
- On
|
||||
- To
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# DATE RANGES — TN-013. A property of one of these ranges is a date attribute
|
||||
# and takes the nominal form.
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
date_ranges:
|
||||
- "http://www.w3.org/2001/XMLSchema#date"
|
||||
- "http://www.w3.org/2001/XMLSchema#dateTime"
|
||||
+48
-24
@@ -17,7 +17,7 @@
|
||||
|
||||
meta:
|
||||
title: Pernod Ricard Data MetaModel — T-Box Rulebook
|
||||
version: "1.2"
|
||||
version: "1.4"
|
||||
status: Draft for review
|
||||
date: "2026-08-03"
|
||||
scope: >
|
||||
@@ -249,8 +249,9 @@ rules:
|
||||
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.
|
||||
participle is a verb in first position. An adverb qualifying that verb is
|
||||
transparent and may precede it. No relation begins with a preposition or
|
||||
with a noun.
|
||||
scope: [object_property]
|
||||
severity: BLOCKING
|
||||
control: {tier: script, executor: check_tbox_naming.py, procedure: null}
|
||||
@@ -258,12 +259,14 @@ rules:
|
||||
rationale: >
|
||||
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.
|
||||
declaration. Two clauses are stated rather than left implicit, because a
|
||||
literal reading would condemn sound relations: the participial one, which
|
||||
covers a whole family such as computedBy, storedIn and derivedFrom, and the
|
||||
adverbial one, which covers a relation whose verb is qualified rather than
|
||||
replaced.
|
||||
examples:
|
||||
- {from: "pr:inDatabase", to: "pr:isInDatabase", note: a preposition is not a verb}
|
||||
- {from: "pr:primaryLocation", to: "pr:primarilyStoredIn", note: a noun is not a verb}
|
||||
- {from: "pr:primaryLocation", to: "pr:primarilyStoredIn", note: "a noun is not a verb; the adverb is transparent and the verb follows"}
|
||||
- {from: "pr:computedBy", to: "pr:computedBy", note: participial form is conforming}
|
||||
|
||||
- id: TN-010
|
||||
@@ -596,9 +599,9 @@ rules:
|
||||
title: 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.
|
||||
property is deliberately polymorphic. A polymorphic property declares
|
||||
pr:polymorphic true and has its scope declared in SHACL. Silent absence is a
|
||||
violation; declared absence is not.
|
||||
scope: [object_property, datatype_property]
|
||||
severity: BLOCKING
|
||||
control: {tier: [script, human], executor: check_tbox_naming.py, procedure: null}
|
||||
@@ -608,30 +611,51 @@ rules:
|
||||
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.
|
||||
the two cases rather than demanding a domain everywhere. The distinction is
|
||||
carried by an axiom and not by a sentence in the comment: a checker looking
|
||||
for a phrase in prose fails the moment someone words it differently, and
|
||||
what matters is declared rather than read, exactly as for abstractness.
|
||||
examples:
|
||||
- {from: a polymorphic property with no domain and no comment, to: violation, note: silence is indistinguishable from omission}
|
||||
- {from: a polymorphic property with no domain, documented, to: conforming, note: scope declared in SHACL}
|
||||
- {from: a property with no domain and no declaration, to: violation, note: silence is indistinguishable from omission}
|
||||
- {from: "a property with no domain, declaring pr:polymorphic true", to: conforming, note: scope declared in SHACL}
|
||||
|
||||
- id: TN-026
|
||||
category: Declaration
|
||||
title: Every term declares how it was authored
|
||||
title: Every concrete class declares how its instances are produced
|
||||
statement: >
|
||||
pr:authoringMode is mandatory on every term. pr:harvestSource is mandatory
|
||||
if and only if the mode is HARVESTED.
|
||||
scope: [class, object_property, datatype_property, annotation_property]
|
||||
Every CONCRETE CLASS declares pr:authoringMode, stating whether its
|
||||
instances are ASSERTED by governance or HARVESTED from a system.
|
||||
pr:harvestSource is mandatory if and only if the mode is HARVESTED. The
|
||||
declared mode agrees with the provenance axis the class descends from: a
|
||||
class under CapturedObject declares HARVESTED, one under DefinedObject
|
||||
declares ASSERTED. Nothing else carries the property: not an abstract class,
|
||||
not a property, not a term of the vocabulary in its own right.
|
||||
scope: [class]
|
||||
severity: BLOCKING
|
||||
control: {tier: script, executor: check_tbox_naming.py, procedure: null}
|
||||
filiation: null
|
||||
rationale: >
|
||||
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.
|
||||
A term of the vocabulary is declared, always and by definition; recording
|
||||
that on every term would repeat one fact a hundred times and say nothing. It
|
||||
is the INSTANCES that have a provenance, and the class is where the model
|
||||
states which one: a governance object is asserted by a domain, a physical
|
||||
column is harvested from a system. The distinction matters because a class
|
||||
can be declared and describe captured things at once — CapturedObject itself
|
||||
is a declared term naming harvested instances, and conflating the two is
|
||||
what once put a harvesting mode on an abstraction.
|
||||
|
||||
Nothing is inherited. A subclass declares its own mode rather than taking
|
||||
its parent's, so that the declaration is read where the instances are typed
|
||||
and not chased up a chain. The agreement clause is what keeps that free of
|
||||
contradiction: the axis says what kind of thing the class describes, the
|
||||
mode says how those things arrive, and a class under CapturedObject
|
||||
declaring ASSERTED is stating both at once.
|
||||
|
||||
Declared this way, provenance also makes a harvester specifiable from the
|
||||
model itself rather than from a side document.
|
||||
examples:
|
||||
- {from: harvestSource present, authoringMode absent, to: "authoringMode HARVESTED", note: null}
|
||||
- {from: "authoringMode on an abstract class", to: nothing, note: an abstraction has no instances to produce}
|
||||
- {from: "a class under CapturedObject declaring ASSERTED", to: violation, note: the mode contradicts the axis}
|
||||
- {from: "authoringMode HARVESTED, harvestSource absent", to: harvestSource declared, note: null}
|
||||
|
||||
- id: TN-027
|
||||
|
||||
Reference in New Issue
Block a user