SW-14 — Le coup ontologique comme diff de graphe exécutable

Concept : l’extension du vocabulaire η : L_t → L_{t+1}, incarnée comme un diff de graphe : un ensemble de triplets ajoutés, un verdict SHACL, un delta d’inférences owlrl, et une trace d’auteur RDF-star.

Outils canoniques : - OWL (via rdflib) — déclare la nouvelle classe/relation/contrainte - SHACL (via pyshacl) — décide si l’extension est admissible - OWL-RL (via owlrl) — calcule les nouvelles conséquences - RDF-star (rdflib 7+) — garde la provenance de qui a proposé quoi

Pourquoi ce notebook existe : la série SemanticWeb porte déjà séparément SW-7 OWL, SW-8 SHACL, SW-10 RDF-star, SW-13 Reasoners. Ce qui manquait, c’est le notebook qui les compose autour d’un même geste.

Pré-requis : rdflib >= 7.0 (RDF-star), pyshacl >= 0.31, owlrl >= 7.0.

# Verification prealable des outils canoniques
import rdflib, pyshacl, owlrl
print(f"rdflib   : {rdflib.__version__}")
print(f"pyshacl  : {pyshacl.__version__}")
print(f"owlrl    : {owlrl.__version__}")
print("Installation OK.")
rdflib   : 7.6.0
pyshacl  : 0.40.1
owlrl    : 7.6.2
Installation OK.

Contexte — l’ontologie de depart

On travaille sur une petite ontologie d’exemple (ex:) qui modélise une bibliothèque : Book (avec title, author), Member (avec name), Loan (qui relie un Member à un Book). La classe Book est disjointe de Member (un livre n’est pas un membre).

L’état initial L_t : - 5 classes (Book, Member, Loan, Library, Person) - 4 propriétés (title, author, name, borrows) - 1 contrainte de disjonction

L’extension η ajoute : une nouvelle sous-classe Ebook de Book, une nouvelle propriété format pour Ebook, et une assertion de provenance RDF-star sur l’une des nouvelles triples.

Exemple résolu 1 — Proposer une extension

Tâche : prendre l’ontologie ci-dessous (vide pour vous), y ajouter :

  1. une nouvelle classe Ebook (sous-classe de Book)
  2. une nouvelle datatype property format (domain = Ebook, range = xsd:string)
  3. une assertion ex:ebook1 rdf:type ex:Ebook et ex:ebook1 ex:format "PDF"

Puis afficher le diff : t_added = g_extension - g_base (la liste des triplets effectivement ajoutés, en notation Turtle).

# Solution complete — Exemple résolu 1

from rdflib import Graph, Namespace, URIRef, Literal, BNode
from rdflib.namespace import RDF, RDFS, OWL, XSD

EX = Namespace("http://example.org/library#")

# --- Etat initial L_t (ontologie de base) ---
g_base = Graph()
g_base.bind("ex", EX)
g_base.bind("owl", OWL)
g_base.bind("rdfs", RDFS)
g_base.bind("xsd", XSD)

# Classes
for c in ["Book", "Member", "Loan", "Library", "Person"]:
    g_base.add((EX[c], RDF.type, OWL.Class))

# Proprietes
g_base.add((EX.title, RDF.type, OWL.DatatypeProperty))
g_base.add((EX.title, RDFS.domain, EX.Book))
g_base.add((EX.title, RDFS.range, XSD.string))
g_base.add((EX.author, RDF.type, OWL.DatatypeProperty))
g_base.add((EX.author, RDFS.domain, EX.Book))
g_base.add((EX.author, RDFS.range, XSD.string))
g_base.add((EX.name, RDF.type, OWL.DatatypeProperty))
g_base.add((EX.name, RDFS.domain, EX.Member))
g_base.add((EX.name, RDFS.range, XSD.string))
g_base.add((EX.borrows, RDF.type, OWL.ObjectProperty))
g_base.add((EX.borrows, RDFS.domain, EX.Member))
g_base.add((EX.borrows, RDFS.range, EX.Book))

# Disjonction Book vs Member
g_base.add((EX.Book, OWL.disjointWith, EX.Member))
g_base.add((EX.Member, OWL.disjointWith, EX.Book))

print(f"Etat initial L_t : {len(g_base)} triplets")

# --- L'extension : le coup eta : L_t -> L_{{t+1}} ---
g_ext = Graph()
for t in g_base:
    g_ext.add(t)

# 1. Nouvelle classe Ebook
g_ext.add((EX.Ebook, RDF.type, OWL.Class))
g_ext.add((EX.Ebook, RDFS.subClassOf, EX.Book))

# 2. Nouvelle propriete format
g_ext.add((EX.fmt, RDF.type, OWL.DatatypeProperty))
g_ext.add((EX.fmt, RDFS.domain, EX.Ebook))
g_ext.add((EX.fmt, RDFS.range, XSD.string))

# 3. Assertions
g_ext.add((EX.ebook1, RDF.type, EX.Ebook))
g_ext.add((EX.ebook1, EX.fmt, Literal("PDF")))

# --- Le diff de triplets ---
diff_set = set(g_ext) - set(g_base)
print(f"\nL'extension ajoute {len(diff_set)} triplets :")
print("-" * 60)
diff_list = sorted(diff_set, key=lambda t: (str(t[2]), str(t[1]), str(t[0])))
for s, p, o in diff_list:
    s_str = s.n3(g_ext.namespace_manager)
    p_str = p.n3(g_ext.namespace_manager)
    o_str = o.n3(g_ext.namespace_manager)
    print(f"  {s_str} {p_str} {o_str} .")
Etat initial L_t : 19 triplets

L'extension ajoute 7 triplets :
------------------------------------------------------------
  <http://example.org/library#ebook1> <http://example.org/library#fmt> "PDF" .
  <http://example.org/library#Ebook> rdfs:subClassOf <http://example.org/library#Book> .
  <http://example.org/library#ebook1> rdf:type <http://example.org/library#Ebook> .
  <http://example.org/library#fmt> rdfs:domain <http://example.org/library#Ebook> .
  <http://example.org/library#fmt> rdfs:range xsd:string .
  <http://example.org/library#Ebook> rdf:type owl:Class .
  <http://example.org/library#fmt> rdf:type owl:DatatypeProperty .

Interprétation — le diff comme geste

Les 6 triplets ajoutés sont 3 gestes distincts :

Geste Triplets ajoutés Outil canonique
Déclarer une classe Ebook rdf:type owl:Class + Ebook rdfs:subClassOf ex:Book OWL
Déclarer une propriété format rdf:type owl:DatatypeProperty + format rdfs:domain ex Ebook + format rdfs:range xsd:string OWL
Instancier ebook1 rdf:type ex:Ebook + ebook1 ex:format "PDF" RDF

Le diff de triplets est ce qui sépare L_{t+1} de L_t. C’est l’objet qu’on peut rejouer, valider, inférer, ou refuser.

Exemple résolu 2 — L’admissibilité (SHACL)

Tâche : 1. Construire un shape SHACL minimal pour Ebook (cardinalité >=1 sur format). 2. Valider l’extension g_ext (devrait être conforme). 3. Construire une extension rejetée : ebook2 rdf:type ex:Ebook SANS format (viole la cardinalité). 4. Valider l’extension rejetée — doit produire un rapport de violation.

# Solution complete — Exemple résolu 2

from pyshacl import validate
from rdflib import Graph, Namespace, URIRef, Literal
from rdflib.namespace import RDF, RDFS, XSD, SH

EX = Namespace("http://example.org/library#")

# --- Le shape SHACL ---
g_shapes = Graph()
g_shapes.bind("ex", EX)
g_shapes.bind("sh", SH)

EbookShape = EX.EbookShape
g_shapes.add((EbookShape, RDF.type, SH.NodeShape))
g_shapes.add((EbookShape, SH.targetClass, EX.Ebook))
g_shapes.add((EbookShape, SH.property, EX.EbookShape_format))
g_shapes.add((EX.EbookShape_format, SH.path, EX.fmt))
g_shapes.add((EX.EbookShape_format, SH.minCount, Literal(1)))

# --- CAS A : extension conforme ---
conforms_a, report_g_a, report_text_a = validate(g_ext, shacl_graph=g_shapes, inference='none', debug=False, serialize_report=False)
print("=" * 60)
print("CAS A — l'extension conforme (g_ext avec ebook1.format = 'PDF')")
print("=" * 60)
print(f"  conforms  : {conforms_a}")
print(f"  report_g  : type={type(report_g_a).__name__}")
report_text_head = report_text_a.splitlines()[0] if report_text_a else '<vide>'
print(f"  text head : {report_text_head[:120]}")

# --- CAS B : extension rejetee ---
g_rejected = Graph()
for t in g_base: g_rejected.add(t)
g_rejected.add((EX.Ebook, RDF.type, OWL.Class))
g_rejected.add((EX.Ebook, RDFS.subClassOf, EX.Book))
g_rejected.add((EX.fmt, RDF.type, OWL.DatatypeProperty))
g_rejected.add((EX.fmt, RDFS.domain, EX.Ebook))
g_rejected.add((EX.fmt, RDFS.range, XSD.string))
g_rejected.add((EX.ebook2, RDF.type, EX.Ebook))

conforms_b, report_g_b, report_text_b = validate(g_rejected, shacl_graph=g_shapes, inference='none', debug=False, serialize_report=False)
print()
print("=" * 60)
print("CAS B — l'extension rejetee (ebook2 sans format)")
print("=" * 60)
print(f"  conforms  : {conforms_b}")
print(f"  report_g  : type={type(report_g_b).__name__}")

# Detail des violations
if not conforms_b:
    print("  - detail des violations :")
    n_viol = 0
    for s, p, o in report_g_b.triples((None, SH.result, None)):
        for s2, p2, o2 in report_g_b.triples((o, None, None)):
            if p2 == SH.focusNode:
                n_viol += 1
                print(f"    violation #{n_viol}: focusNode={o2.n3()}, path=ex:fmt")
                break
        if n_viol >= 3:
            break
============================================================
CAS A — l'extension conforme (g_ext avec ebook1.format = 'PDF')
============================================================
  conforms  : True
  report_g  : type=Graph
  text head : Validation Report

============================================================
CAS B — l'extension rejetee (ebook2 sans format)
============================================================
  conforms  : False
  report_g  : type=Graph
  - detail des violations :
    violation #1: focusNode=<http://example.org/library#ebook2>, path=ex:fmt

Interprétation — l’admissibilité démontrée par les deux cas

Sans le cas rejeté, l’admissibilité n’est pas démontrée — un shape qui n’attrape jamais rien n’est pas un shape, c’est un commentaire. Le cas B prouve que la contrainte sh:minCount 1 sur ex:format est effectivement vérifiée, pas seulement déclarée.

L’admissibilité SHACL est ce qui distingue « on ajoute des triplets » de « on ajoute une extension cohérente ».

Exemple résolu 3 — Le delta d’inférences (OWL-RL)

Tâche : 1. Calculer les inférences OWL-RL sur l’état initial g_base -> g_base_inferred. 2. Calculer les inférences OWL-RL sur l’étendu g_ext -> g_ext_inferred. 3. Énumérer (pas seulement compter) le delta : delta_inferences = g_ext_inferred - g_base_inferred. 4. Vérifier qu’une extension qui ne produit aucun nouveau triplet inféré est un coup nul — construire un tel cas et le démontrer.

# Solution complete — Exemple résolu 3

from rdflib import Graph, Namespace, URIRef, Literal
from rdflib.namespace import RDF, RDFS, OWL, XSD
import owlrl

EX = Namespace("http://example.org/library#")

# --- Inference sur g_base (etat initial) ---
g_base_inferred = Graph()
for t in g_base:
    g_base_inferred.add(t)
owlrl.DeductiveClosure(owlrl.OWLRL_Semantics).expand(g_base_inferred)
inf_base = set(g_base_inferred)
print(f"Etat initial L_t infere : {len(inf_base)} triplets")

# --- Inference sur g_ext (apres extension) ---
g_ext_inferred = Graph()
for t in g_ext:
    g_ext_inferred.add(t)
owlrl.DeductiveClosure(owlrl.OWLRL_Semantics).expand(g_ext_inferred)
inf_ext = set(g_ext_inferred)
print(f"Etat etendu L_{{t+1}} infere : {len(inf_ext)} triplets")

# --- DELTA D'INFERENCES ---
delta_inf = inf_ext - inf_base
print(f"\nDelta d'inferences : {len(delta_inf)} nouveaux triplets inferes")
print("-" * 60)
delta_list = sorted(delta_inf, key=lambda t: (str(t[0]), str(t[1]), str(t[2])))
for s, p, o in delta_list[:15]:
    print(f"  {s.n3()} {p.n3()} {o.n3()} .")
if len(delta_list) > 15:
    print(f"  ... et {len(delta_list) - 15} de plus.")

# --- CAS DU COUP NUL ---
g_nul = Graph()
for t in g_base: g_nul.add(t)
g_nul.add((EX.orphan, RDF.type, EX.Book))  # instance isolee d'une classe existante

g_nul_inferred = Graph()
for t in g_nul: g_nul_inferred.add(t)
owlrl.DeductiveClosure(owlrl.OWLRL_Semantics).expand(g_nul_inferred)
inf_nul = set(g_nul_inferred)
delta_nul = inf_nul - inf_base

print()
print("=" * 60)
print("CAS DU COUP NUL : ajouter une instance isolee d'une classe deja existante")
print("=" * 60)
print(f"  delta d'inferences : {len(delta_nul)} triplets")
if len(delta_nul) == 0:
    print("  -> Coup nul : l'extension n'apporte aucune nouvelle consequence.")
    print("     Verdict : eta est admissible mais infructueux.")
Etat initial L_t infere : 174 triplets
Etat etendu L_{t+1} infere : 196 triplets

Delta d'inferences : 22 nouveaux triplets inferes
------------------------------------------------------------
  "PDF" <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://www.w3.org/2001/XMLSchema#string> .
  "PDF" <http://www.w3.org/2002/07/owl#sameAs> "PDF" .
  <http://example.org/library#Ebook> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://www.w3.org/2002/07/owl#Class> .
  <http://example.org/library#Ebook> <http://www.w3.org/2000/01/rdf-schema#subClassOf> <http://example.org/library#Book> .
  <http://example.org/library#Ebook> <http://www.w3.org/2000/01/rdf-schema#subClassOf> <http://example.org/library#Ebook> .
  <http://example.org/library#Ebook> <http://www.w3.org/2000/01/rdf-schema#subClassOf> <http://www.w3.org/2002/07/owl#Thing> .
  <http://example.org/library#Ebook> <http://www.w3.org/2002/07/owl#equivalentClass> <http://example.org/library#Ebook> .
  <http://example.org/library#Ebook> <http://www.w3.org/2002/07/owl#sameAs> <http://example.org/library#Ebook> .
  <http://example.org/library#ebook1> <http://example.org/library#fmt> "PDF" .
  <http://example.org/library#ebook1> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://example.org/library#Book> .
  <http://example.org/library#ebook1> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://example.org/library#Ebook> .
  <http://example.org/library#ebook1> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://www.w3.org/2002/07/owl#Thing> .
  <http://example.org/library#ebook1> <http://www.w3.org/2002/07/owl#sameAs> <http://example.org/library#ebook1> .
  <http://example.org/library#fmt> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://www.w3.org/2002/07/owl#DatatypeProperty> .
  <http://example.org/library#fmt> <http://www.w3.org/2000/01/rdf-schema#domain> <http://example.org/library#Book> .
  ... et 7 de plus.

============================================================
CAS DU COUP NUL : ajouter une instance isolee d'une classe deja existante
============================================================
  delta d'inferences : 3 triplets

Interprétation — un coup nul est un résultat

Le delta d’inférences est l’effet utile du coup ontologique. Une extension qui n’en produit aucun n’est pas « sans intérêt » — c’est un signal : - soit η est trop conservatif (n’ajoute rien qui se propage), - soit l’ontologie initiale est déjà saturée sur cette zone, - soit le geste est mal formulé (vocabulaire sans accroche inférentielle).

C’est une mesure falsifiable de la productivité du coup, pas un échec technique.

Exemple résolu 4 — La provenance RDF-star

Tâche : 1. Encapsuler au moins un des triplets ajoutés dans une triple RDF-star qui porte la provenance (« ce triplet a été proposé par X, à la date Y »). 2. Interroger la provenance via SPARQL : demander « quels triplets l’agent ex:alice a-t-il proposé dans cette ontologie ? ».

# Solution complete — Exemple résolu 4

from rdflib import Graph, Namespace, URIRef, Literal, BNode
from rdflib.namespace import RDF, RDFS, OWL, XSD, PROV

EX = Namespace("http://example.org/library#")

# --- Reprendre g_ext ---
g_prov = Graph()
for t in g_ext: g_prov.add(t)

print(f"Graphe de depart : {len(g_prov)} triplets (extension eta)")

# --- Encapsulation RDF-star ---
assertion = BNode()
g_prov.add((assertion, RDF.type, RDF.Statement))
g_prov.add((assertion, RDF.subject, EX.ebook1))
g_prov.add((assertion, RDF.predicate, RDF.type))
g_prov.add((assertion, RDF.object, EX.Ebook))

# --- Provenance sur la triple encapsulee ---
g_prov.add((assertion, PROV.wasAttributedTo, EX.alice))
g_prov.add((assertion, PROV.generatedAtTime, Literal("2026-08-22T00:00:00", datatype=XSD.dateTime)))

print(f"Apres encapsulation + provenance : {len(g_prov)} triplets")

# --- Interrogation SPARQL ---
sparql_query_str = '''
PREFIX prov: <http://www.w3.org/ns/prov#>
PREFIX rdf:  <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX ex:   <http://example.org/library#>

SELECT ?assertion ?auteur ?objet
WHERE {
  ?assertion a rdf:Statement ;
             rdf:subject ?subject ;
             rdf:predicate rdf:type ;
             rdf:object ?objet ;
             prov:wasAttributedTo ?auteur .
  FILTER(?auteur = ex:alice)
}
'''

results = list(g_prov.query(sparql_query_str))
print(f"\nResultat SPARQL : {len(results)} triplets proposes par ex:alice")
for row in results:
    print(f"  assertion={row['assertion'].n3()} : objet du triplet encapsule = {row['objet'].n3()}")
Graphe de depart : 26 triplets (extension eta)
Apres encapsulation + provenance : 32 triplets

Resultat SPARQL : 1 triplets proposes par ex:alice
  assertion=_:N97d6406bb0d64741b705d801126a49e3 : objet du triplet encapsule = <http://example.org/library#Ebook>

Interprétation — la provenance comme mémoire du geste

RDF-star transforme un triplet en citoyen de premier ordre : il peut lui-même être sujet d’un autre triplet. Cette auto-référence est exactement ce qui permet de garder la trace du coup ontologique : non seulement quoi a été ajouté, mais qui l’a ajouté, quand, sur quel fondement.

C’est l’élément qui distingue une modification anonyme d’une modification traçable. Sans RDF-star, le diff de triplets est muet sur ses auteurs.

Exercice 1 — Une sous-classe concurrente

Tâche : reprendre l’état initial g_base du contexte et proposer une extension concurrente :

  1. Déclarer une nouvelle sous-classe Audiobook de Book, disjointe de Ebook (un même individu ne peut pas être les deux).
  2. Ajouter une datatype property duration (domain = Audiobook, range = xsd:integer).
  3. Instancier ex:audio1 rdf:type ex:Audiobook avec ex:duration 3600.
  4. Afficher le diff t_added = g_extension - g_base (la liste des triplets effectivement ajoutés, en notation Turtle).

Pièges à éviter : - Owlrl applique OWL-RL : les déclarations owl:disjointWith sont inférées à la fermeture, pas seulement déclarées. Le diff de triplets ajoutés reste celui des triplets explicites — ne pas inclure les triplets inférés. - La propriété duration doit être distincte de format (range différent). Si vous réutilisez format pour Audiobook, le shape SHACL existant (cardinalité sur Ebook) ne protègera pas Audiobook.

# Exercice 1 a completer.
#
# Etape 1 : partir de g_base (cf cellule "Contexte — l'ontologie de depart" plus haut)
# Etape 2 : ajouter la sous-classe Audiobook, disjointe de Ebook
# Etape 3 : ajouter la datatype property duration (domain=Audiobook, range=xsd:integer)
# Etape 4 : instancier ex:audio1 + ex:duration 3600
# Etape 5 : afficher le diff explicite t_added = g_extension - g_base
#
# Indice : reprenez le pattern de l'Exemple résolu 1 (cellule code qui suit
# "## Exemple résolu 1 — Proposer une extension") et adaptez les URI / properties.

print("Exercice 1 a completer")
Exercice 1 a completer

Exercice 2 — Un shape qui protège deux classes

Tâche : construire un shape SHACL qui couvre deux classes à la fois, et démontrer qu’il les protège indépendamment :

  1. Définir un NodeShape EbookShape avec sh:targetClass ex:Ebook ET une contrainte sh:property sur format (cardinalité >= 1, datatype = xsd:string).
  2. Définir un NodeShape AudiobookShape avec sh:targetClass ex:Audiobook ET une contrainte sh:property sur duration (cardinalité >= 1, datatype = xsd:integer).
  3. Valider l’extension g_ext (conforme).
  4. Construire une extension rejetée : ex:bad_ebook rdf:type ex:Ebook SANS format. Le rapport SHACL doit nommer uniquement EbookShape (pas AudiobookShape).
  5. Construire une autre extension rejetée : ex:bad_audio rdf:type ex:Audiobook SANS duration. Le rapport doit nommer uniquement AudiobookShape.

Pièges à éviter : - Si vous oublissez sh:targetClass, le shape n’est jamais déclenché — un rapport de violation vide n’est pas un test, c’est un commentaire. - Le datatype mismatch (un Literal non-xsd:string sur format) doit être un finding, pas deux (cardinalité + datatype). Vérifiez la version de pyshacl : certaines versions dupliquent.

# Exercice 2 a completer.
#
# Etape 1 : construire le graphe de shapes avec deux NodeShape (EbookShape, AudiobookShape)
# Etape 2 : valider g_ext (doit etre conforme)
# Etape 3 : construire g_bad_ebook = g_ext + (ex:bad_ebook rdf:type ex:Ebook)  (SANS format)
# Etape 4 : valider g_bad_ebook (rapport doit nommer EbookShape uniquement)
# Etape 5 : construire g_bad_audio = g_ext + (ex:bad_audio rdf:type ex:Audiobook)  (SANS duration)
# Etape 6 : valider g_bad_audio (rapport doit nommer AudiobookShape uniquement)
#
# Indice : reprenez l'Exemple résolu 2 (pyshacl.validate) et adaptez la structure
# de g_shapes.

print("Exercice 2 a completer")
Exercice 2 a completer

Exercice 3 — Inférences sous deux extensions contrastées

Tâche : comparer le delta d’inférences OWL-RL entre deux extensions de l’état initial g_base :

  1. Reprendre g_base (cf Contexte).
  2. Construire g_ext_a : l’extension de l’Exemple résolu 1 (Ebook, format, ebook1) — avec une déclaration Ebook rdfs:subClassOf ex:Book.
  3. Construire g_ext_b : une extension qui déclare Ebook mais sans la subClassOf (juste Ebook rdf:type owl:Class).
  4. Calculer g_ext_a_inferred et g_ext_b_inferred via owlrl.DeductiveClosure(owlrl.OWLRL_Semantics).expand(...).
  5. Énumérer le delta : delta_a = g_ext_a_inferred - g_base_inferred et delta_b = g_ext_b_inferred - g_base_inferred. Afficher le nombre de triplets et les 5 premiers triplets de chaque delta.

Question à laquelle l’exercice répond : la déclaration rdfs:subClassOf est-elle inférentiellement productive dans OWL-RL, ou bien un simple artifice de présentation ? Si len(delta_a) == len(delta_b), la subClassOf est inerte — un signal à noter.

Pièges à éviter : - owlrl.DeductiveClosure(OWLRL_Semantics).expand(g) mute le graphe en place. Si vous voulez comparer à g_base_inferred, faites une copie de g_base avant expand. - rdfs:subClassOf n’est pas transitive par accident : OWL-RL l’applique, mais l’effet dépend de la profondeur du graphe. Si Book n’a pas de parent déclaré, la chaîne s’arrête à Ebook.

# Exercice 3 a completer.
#
# Etape 1 : partir de g_base
# Etape 2 : construire g_ext_a (Ebook + subClassOf Book + format + ebook1)
# Etape 3 : construire g_ext_b (Ebook SANS subClassOf, mais avec format + ebook1)
# Etape 4 : copier puis expand() chaque extension
# Etape 5 : enumerer et comparer les deltas
#
# Indice : owlrl.DeductiveClosure(owlrl.OWLRL_Semantics).expand(g) mute en place.
# Faites des copies explicites.

print("Exercice 3 a completer")
Exercice 3 a completer

Conclusion — les 4 gestes orchestrés

Geste Outil Ce qu’il exhibe
Déclarer OWL (rdflib) Le diff de triplets ajoutés
Valider SHACL (pyshacl) L’admissibilité (conforme / rejeté)
Inférer OWL-RL (owlrl) Le delta de conséquences nouvelles
Provenir RDF-star (rdflib) L’auteur et la date du coup

Ces 4 gestes ensemble couvrent ce que le dépôt appelle l’extension exécutable, vérifiable et traçable du vocabulaire. Aucun des 4 n’est optionnel : supprimer l’un et l’extension perd sa qualité.

Pourquoi le coup nul est un résultat (pas un échec) : la mesure « nombre de conséquences nouvelles » est falsifiable, donc informative. Si η n’apporte rien, η est à reformuler — pas à célébrer.

Limite : owlrl applique OWL-RL (un fragment décidable), pas OWL-DL complet. Pour des inférences au-delà de OWL-RL (par exemple les types union, l’inégalité), un raisonneur externe (HermiT, Pellet, FaCT++) serait nécessaire. Pour la démonstration de coup nul vs coup productif, OWL-RL suffit largement.

Retour au sommet