tbox: v2.0 - 47 renommages de vocabulaire, alignement T-Box/A-Box sur les attributs transverses, GovernanceLayerObject inseree au-dessus d'Actor, isAbstract declare sur les 44 classes, ActivationStatus et Environment fermes en litteraux, labels derives de l'IRI, conformsTo pose sur les shapes, requetes SPARQL embarquees reecrites. Validation SHACL sans violation bloquante, rejeu a zero modification. Viewer T-Box regenere mais encore porteur d'IRI en dur, cf backlog

This commit is contained in:
Bastien Gourdon
2026-08-05 14:32:49 +02:00
parent 1719ec1934
commit 908826601a
18 changed files with 4230 additions and 3677 deletions
+99 -22
View File
@@ -31,6 +31,7 @@ from collections import OrderedDict
import yaml
from rdflib import Graph, Literal, Namespace, RDF, RDFS, OWL, URIRef, XSD
from rdflib.namespace import SH
HERE = os.path.dirname(os.path.abspath(__file__))
SPEC = os.path.join(HERE, "renames.yaml")
@@ -133,7 +134,13 @@ def drop_subject(graph, subject, report, step):
def count_instances(graphs, term):
"""EV-002. The proof required before any permanent withdrawal."""
"""EV-002. The proof required before any permanent withdrawal.
Counted on INSTANCE graphs only. A mention inside a shapes graph is not an
instantiation: a shape carrying sh:maxCount 0 on a term is a guard forbidding
its reappearance, which is the opposite of a use, and it keeps working after
the term is gone since it checks an identifier rather than a declaration.
"""
n = 0
for g in graphs:
n += len(list(g.triples((None, RDF.type, term))))
@@ -142,34 +149,84 @@ def count_instances(graphs, term):
return n
def migrate(onto, others, spec, rules, report):
def rewrite_queries(graph, renames, report):
"""Rename inside embedded SPARQL queries.
A sh:select or sh:ask body is a literal, so no triple-level rename reaches
it: the query keeps naming a term that no longer exists and the constraint
silently stops matching. This is not the line-by-line editing EV-005
forbids: the graph is parsed, the literal is retrieved as a value, and what
is rewritten is a structured query language where a prefixed name has clear
token boundaries.
"""
n = 0
for prop in (SH.select, SH.ask, SH.construct):
for s, p, o in list(graph.triples((None, prop, None))):
text = str(o)
new = text
for old, target in renames:
new = re.sub(r"\bpr:%s\b" % re.escape(old), "pr:%s" % target, new)
if new != text:
graph.remove((s, p, o))
graph.add((s, p, Literal(new)))
n += 1
report.add("queries_rewritten", n)
return n
def migrate(onto, instances, shapes, spec, rules, report):
ns = spec["meta"]["namespace"]
pr = Namespace(ns)
others = instances + shapes
all_graphs = [onto] + others
# 1 — deprecated terms are withdrawn outright (phase clause), proof first
# 1 — deprecated terms are withdrawn outright (phase clause), proof first.
# The proof is taken on instance graphs only: a mention in a shapes graph
# is a guard against reappearance, not a use.
# Reference cleaning likewise stops at the ontology and the instances.
# EV-003 guards against phantom nodes rebuilt by inference in the model
# graph; a mention inside a shape is a constraint on an identifier, in a
# separate graph, taking part in no inference over the ontology. Removing
# it would strip the sh:path from a guard and leave a property shape
# carrying a maxCount and no path — an invalid shape, and the loss of the
# very rule that forbids the term from coming back.
if spec["structural"].get("drop_deprecated"):
for subj in list(onto.subjects(OWL.deprecated, Literal(True))):
name = local(subj, ns) or str(subj)
used = sum(count_instances([g], subj) for g in others)
used = count_instances(instances, subj)
if used:
report.add("deprecated_kept", 1, "%s still used %d times" % (name, used))
report.add("deprecated_kept", 1, "%s instantiated %d times" % (name, used))
continue
for g in all_graphs:
for g in [onto] + instances:
drop_subject(g, subj, report, "deprecated_dropped")
# 2 — renames, in dependency order
# 2 — vocabulary renames, in dependency order
for order in (1, 2, 3):
for r in [x for x in spec["renames"] if x["order"] == order]:
for g in all_graphs:
rename(g, pr, r["from"], r["to"], report, "rename_order_%d" % order)
# 3 — merges
# 3 — instance-side alignment: forms the instances use that the vocabulary
# never declared and that the TN-012 renames do not converge on.
# Applied to the shapes too: they target these forms in sh:path.
for r in spec.get("abox_renames") or []:
for g in all_graphs:
rename(g, pr, r["from"], r["to"], report, "abox_aligned")
# 4 — embedded SPARQL queries carry term names as text, invisible to a
# triple-level rename. Rewritten with the same map, after it.
pairs = [(r["from"], r["to"]) for r in spec["renames"]]
pairs += [(r["from"], r["to"]) for r in (spec.get("abox_renames") or [])]
pairs += [(m["from"], m["into"]) for m in (spec.get("merges") or [])]
for g in all_graphs:
rewrite_queries(g, pairs, report)
# 5 — merges
for m in spec.get("merges") or []:
for g in all_graphs:
merge(g, pr, m["from"], m["into"], report)
# 4 — reclassify display properties (TN-003)
# 6 — reclassify display properties (TN-003)
for rc in spec["structural"]["reclassify"]:
term = pr[rc["term"]]
if (term, RDF.type, OWL.AnnotationProperty) not in onto:
@@ -177,7 +234,7 @@ def migrate(onto, others, spec, rules, report):
onto.add((term, RDF.type, OWL.AnnotationProperty))
report.add("reclassified", 1, rc["term"])
# 5 — create the governance layer root (TN-027)
# 7 — create the governance layer root (TN-027)
for c in spec["structural"].get("create_classes") or []:
term = pr[c["term"]]
if (term, RDF.type, OWL.Class) not in onto:
@@ -188,21 +245,21 @@ def migrate(onto, others, spec, rules, report):
onto.add((term, pr.isAbstract, Literal(True)))
report.add("classes_created", 1, c["term"])
# 6 — reparent (TN-027)
# 8 — reparent (TN-027)
for rp in spec["structural"].get("reparent") or []:
term, parent = pr[rp["term"]], pr[rp["parent"]]
if (term, RDFS.subClassOf, parent) not in onto:
onto.add((term, RDFS.subClassOf, parent))
report.add("reparented", 1, "%s -> %s" % (rp["term"], rp["parent"]))
# 7 — sub-properties (TN-028)
# 9 — sub-properties (TN-028)
for sp in spec["structural"].get("subproperties") or []:
term, parent = pr[sp["term"]], pr[sp["parent"]]
if (term, RDFS.subPropertyOf, parent) not in onto:
onto.add((term, RDFS.subPropertyOf, parent))
report.add("subproperties", 1, "%s -> %s" % (sp["term"], sp["parent"]))
# 8 — controlled values become literals (TN-016, TN-017)
# 10 — controlled values become literals (TN-016, TN-017)
for conv in spec["structural"].get("to_literal") or []:
prop = pr[conv["property"]]
if (prop, RDF.type, OWL.DatatypeProperty) not in onto:
@@ -222,10 +279,10 @@ def migrate(onto, others, spec, rules, report):
for ind in conv["drop_individuals"] + [conv["drop_class"]]:
subj = pr[ind]
if (subj, None, None) in onto:
for g in all_graphs:
for g in [onto] + instances: # shapes keep their guards
drop_subject(g, subj, report, "to_literal_dropped")
# 9 — abstractness, from the rulebook annex (TN-006, TN-007)
# 11 — abstractness, from the rulebook annex (TN-006, TN-007)
for a in rules["abstractness"]:
term = pr[a["term"]]
if (term, RDF.type, OWL.Class) not in onto:
@@ -237,7 +294,7 @@ def migrate(onto, others, spec, rules, report):
onto.add((term, pr.isAbstract, want))
report.add("isAbstract_declared", 1)
# 10 — display annotations, from the rulebook annex (TN-022)
# 12 — display annotations, from the rulebook annex (TN-022)
# The annex is authoritative AND exhaustive: a null value means any existing
# annotation is removed. A short label identical to the label carries no
# information and is one more thing to keep in step.
@@ -255,7 +312,7 @@ def migrate(onto, others, spec, rules, report):
onto.remove((term, prop, None))
report.add("display_removed", len(current))
# 11 — regenerate every label by derivation (TN-018, TN-023)
# 13 — regenerate every label by derivation (TN-018, TN-023)
kinds = (OWL.Class, OWL.ObjectProperty, OWL.DatatypeProperty, OWL.AnnotationProperty)
for kind in kinds:
for term in set(onto.subjects(RDF.type, kind)):
@@ -268,7 +325,7 @@ def migrate(onto, others, spec, rules, report):
onto.add((term, RDFS.label, want))
report.add("labels_regenerated", 1)
# 12 — bump the ontology version (EV-014: the namespace itself never moves)
# 14 — bump the ontology version (EV-014: the namespace itself never moves)
target = URIRef(ns.rstrip("/") + "/" + spec["meta"]["to_version"])
for onto_iri in set(onto.subjects(RDF.type, OWL.Ontology)):
if (onto_iri, OWL.versionIRI, target) not in onto:
@@ -276,6 +333,19 @@ def migrate(onto, others, spec, rules, report):
onto.add((onto_iri, OWL.versionIRI, target))
report.add("version_bumped", 1, str(target))
# 15 — the shapes declare the ontology version they target (EV-010).
# The shapes graph carries no ontology node, so it is created here.
so = spec["structural"].get("shapes_ontology")
if so:
node = URIRef(so["iri"])
for g in shapes:
if (node, DCTERMS.conformsTo, target) not in g:
g.remove((node, DCTERMS.conformsTo, None))
g.add((node, RDF.type, OWL.Ontology))
g.add((node, RDFS.label, Literal(so["label"])))
g.add((node, DCTERMS.conformsTo, target))
report.add("shapes_conformsTo", 1, str(target))
# ----------------------------------------------------------------------- main
@@ -303,15 +373,22 @@ def main():
onto = Graph()
onto.parse(args.ontology, format="turtle")
others, other_paths = [], args.instances + args.shapes
for p in other_paths:
instances, shapes = [], []
for p in args.instances:
g = Graph()
g.parse(p, format="turtle")
others.append(g)
instances.append(g)
for p in args.shapes:
g = Graph()
g.parse(p, format="turtle")
g.bind("dcterms", DCTERMS)
shapes.append(g)
others = instances + shapes
other_paths = args.instances + args.shapes
before = [len(onto)] + [len(g) for g in others]
report = Report()
migrate(onto, others, spec, rules, report)
migrate(onto, instances, shapes, spec, rules, report)
after = [len(onto)] + [len(g) for g in others]
print("MIGRATION %s -> %s %s"