SW-15 — Le coup argumentatif : greffer AIF sur le coup ontologique

Concept : SW-14 a montre que le coup ontologique \(\eta : L_t \to L_{t+1}\) s’incarne comme un diff de graphe executable (triplets ajoutes, verdict SHACL, delta owlrl, provenance par reification RDF + PROV). Ce notebook realise la greffe symetrique : le graphe etendu n’est plus une ontologie de bibliotheque mais un graphe d’argumentation AIF — ici, le debat sur l’extension Ebook elle-meme — et le coup etend le vocabulaire argumentatif : un nouveau schema d’inference et un nouveau type de conflit, plus les instances qui les habitent.

Plan — les quatre gestes de SW-14, executes sur un objet nouveau :

  1. Exercice 1 — le graphe de debat \(L_t\) en triplets AIF, puis le coup \(\eta\) (vocabulaire + instances) et son diff ;
  2. Exercice 2 — le juge SHACL : le coup conforme, et le controle negatif (un coup malforme rejete) ;
  3. Exercice 3 — le delta d’inferences owlrl, puis la bascule de l’extension grounded de Dung avant/apres coup ;
  4. Exercice 4 — la provenance du coup par reification RDF classique + PROV : quel agent a propose le coup, et quand.

Sources — vocabulaire AIF et taxonomie des schemes repris de l’ontologie Argumentum du depot (ontologies/argumentum_fallacies.owl, etudiee dans Argumentation-Onto-01-AIF-OWL2-Python.ipynb) ; semantiques de Dung selon Argumentation-03-Dung-AF-Semantics-Python.ipynb (Dung 1995) ; pipeline (SHACL / OWL-RL / diff / provenance) selon SW-14-Python-Coup-Ontologique.ipynb. La syntaxe RDF-star (<< s p o >>, quoted triples) est la cible conceptuelle documentee dans SW-10-Python-RDFStar ; sur rdflib 7.6 c’est la reification classique qui s’execute (voir la cellule de solution de l’exercice 4).

# 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.31.0
owlrl    : 7.1.4
Installation OK.

Contexte — le debat sur le coup ontologique

Le debat porte sur l’objet meme de SW-14 : faut-il adopter la classe Ebook dans l’ontologie de la bibliotheque ? Le graphe \(L_t\) ci-dessous modelise ce debat en AIF (Argument Interchange Format) :

  • les I-nodes portent l’information (premisses, conclusions, enonces attaques) ;
  • les RA-nodes (rule application) sont les arguments proprement dits : premisses \(\vdash\) conclusion, chacun type par un schema d’inference Walton materialise dans l’ontologie Argumentum (ExpertOpinion_Inference, NegativeConsequences_Inference, Example_Inference, Bias_Inference) ;
  • les CA-nodes (conflict application) sont les attaques : qui attaque quoi.

Le pont vers le modele de donnees AIF est explicite : l’ontologie Argumentum declare taxonomiquement aif:I-node, aif:RA-node, a:CA-node, aif:Inference_Scheme, aif:Conflict et les proprietes arg:aifAttackedNode / aif:hasConflictedElement. On reprend ces IRIs tels quels au niveau instance, avec deux proprietes de pont declarees (ax:from, ax:to) pour les aretes d’attaque.

Exercice 1 — Le graphe de debat \(L_t\) et le coup argumentatif

Enonce : (a) construire le graphe AIF du debat ci-dessus ; (b) construire le coup \(\eta\) propose par la commission de revision — un nouveau schema d’inference RepresentativeSample_Inference (convention de nommage Argumentum <Nom>_Inference), un nouveau type de conflit SelectionBiasRebuttal_Conflict (<Nom>_Conflict), et les instances qui les utilisent ; (c) exhiber le diff de triplets \(L_{t+1} \setminus L_t\).

# Exercice 1 — a completer
# Etape 1 : declarer le pont AIF dans g_base (classes + proprietes avec domaines/portees)
# Etape 2 : peupler les I-nodes du debat (rdfs:label en francais)
# Etape 3 : construire RA1..RA4 (premisses, conclusion, schema) puis CA1..CA3
# Etape 4 : construire le coup dans g_ext (schema + conflit + instances) puis le diff
# Indice : miroir de la cellule 4 de SW-14 — le diff est set(g_ext) - set(g_base)
from rdflib import Graph
g_base = Graph()
g_ext = Graph()
print("Exercice a completer : L_t, le coup et le diff de triplets")
Exercice a completer : L_t, le coup et le diff de triplets

Solution — Exercice 1

# Solution complete — Exercice 1

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

AIF = Namespace("http://www.arg.dundee.ac.uk/aif#")            # noeuds AIF (ontologie Argumentum)
ARG = Namespace("https://www.argumentum.games/argumentum_fallacies.owl#")
AX  = Namespace("http://example.org/aif-ext#")                  # pont + extension du coup

g_base = Graph()
g_base.bind("aif", AIF); g_base.bind("arg", ARG); g_base.bind("ax", AX)
g_base.bind("rdfs", RDFS); g_base.bind("owl", OWL)

# --- Pont : modele de noeuds AIF (classes reelles de l'ontologie Argumentum) ---
for c in ["I-node", "RA-node", "CA-node", "Inference_Scheme", "Conflict"]:
    g_base.add((AIF[c], RDF.type, OWL.Class))

# Proprietes du modele de donnees : domaines/portees AIF + pont d'attaque
g_base.add((AIF.Premise, RDF.type, OWL.ObjectProperty))
g_base.add((AIF.Premise, RDFS.domain, AIF["RA-node"]))
g_base.add((AIF.Premise, RDFS.range, AIF["I-node"]))
g_base.add((AIF.Conclusion, RDF.type, OWL.ObjectProperty))
g_base.add((AIF.Conclusion, RDFS.domain, AIF["RA-node"]))
g_base.add((AIF.Conclusion, RDFS.range, AIF["I-node"]))
g_base.add((AIF.scheme, RDF.type, OWL.ObjectProperty))
g_base.add((AIF.scheme, RDFS.domain, AIF["RA-node"]))
g_base.add((AIF.scheme, RDFS.range, AIF.Inference_Scheme))
for p, dom in [(AX["from"], AIF["CA-node"]), (AX.to, AIF["CA-node"])]:
    g_base.add((p, RDF.type, OWL.ObjectProperty))
    g_base.add((p, RDFS.domain, dom))

# Schemes Walton materialises dans Argumentum (classes citees telles quelles)
for s in ["ExpertOpinion_Inference", "NegativeConsequences_Inference",
          "Example_Inference", "Bias_Inference"]:
    g_base.add((AIF[s], RDF.type, AIF.Inference_Scheme))

# --- Les I-nodes du debat ---
claims = {
    "I1": "Les bibliothequaires recommandent l'acces numerique",
    "I2": "La maintenance numerique coute cher",
    "I3": "Le budget d'extension est serre",
    "I4": "Le pilote papier-vers-numerique affiche 82% de satisfaction",
    "I5": "Le pilote n'a interroge que des membres deja numeriques",
    "I6": "La satisfaction du pilote n'est pas representative",
    "C1": "Adopter la classe Ebook dans l'ontologie",
    "C2": "Reporter l'extension de l'ontologie",
}
for nid, txt in claims.items():
    g_base.add((AX[nid], RDF.type, AIF["I-node"]))
    g_base.add((AX[nid], RDFS.label, Literal(txt, lang="fr")))

# --- Les RA-nodes (arguments) : premisses, conclusion, schema ---
def ra(g, nid, scheme_uri, premises, conclusion):
    g.add((AX[nid], RDF.type, AIF["RA-node"]))
    g.add((AX[nid], AIF.scheme, scheme_uri))
    for p in premises:
        g.add((AX[nid], AIF.Premise, AX[p]))
    g.add((AX[nid], AIF.Conclusion, AX[conclusion]))

ra(g_base, "RA1", AIF.ExpertOpinion_Inference,        ["I1"], "C1")
ra(g_base, "RA2", AIF.NegativeConsequences_Inference, ["I2", "I3"], "C2")
ra(g_base, "RA3", AIF.Example_Inference,              ["I4"], "C1")
ra(g_base, "RA4", AIF.Bias_Inference,                 ["I5"], "I6")

# --- Les CA-nodes (attaques) : qui attaque quoi ---
def ca(g, nid, attacker, target):
    g.add((AX[nid], RDF.type, AIF["CA-node"]))
    g.add((AX[nid], AX["from"], AX[attacker]))
    g.add((AX[nid], ARG.aifAttackedNode, AX[target]))

ca(g_base, "CA1", "RA2", "RA1")   # undercut : l'argument de cout frappe le lien expertise -> adoption
ca(g_base, "CA2", "RA1", "RA2")   # rebuttal : l'analyse des experts couvre deja les couts
ca(g_base, "CA3", "RA4", "I4")    # undermine : le biais de selection atteint la premisse du pilote

print(f"Etat initial L_t : {len(g_base)} triplets")
print(f"  I-nodes : 8    RA-nodes : 4 (RA1..RA4)    CA-nodes : 3 (CA1..CA3)")

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

# 1. Vocabulaire : nouveau schema + nouveau type de conflit
g_ext.add((AX.RepresentativeSample_Inference, RDF.type, AIF.Inference_Scheme))
g_ext.add((AX.RepresentativeSample_Inference, RDFS.subClassOf, AIF.Inference_Scheme))
g_ext.add((AX.SelectionBiasRebuttal_Conflict, RDF.type, AIF.Conflict))
g_ext.add((AX.SelectionBiasRebuttal_Conflict, RDFS.subClassOf, AIF.Conflict))

# 2. Instances : preuve corrigeant le biais, argument qui la porte, attaque qui retourne RA4
g_ext.add((AX["I7"], RDF.type, AIF["I-node"]))
g_ext.add((AX["I7"], RDFS.label, Literal("L'etude complementaire couvre tous les profils de membres", lang="fr")))
g_ext.add((AX["I8"], RDF.type, AIF["I-node"]))
g_ext.add((AX["I8"], RDFS.label, Literal("Le biais du pilote est corrige", lang="fr")))
# idiome Argumentum : le scheme est materialise comme classe, l'argument en est instance
ra(g_ext, "RA5", AX.RepresentativeSample_Inference, ["I7"], "I8")  # schema du coup
g_ext.add((AX["RA5"], RDF.type, AX.RepresentativeSample_Inference))

g_ext.add((AX["CA4"], RDF.type, AIF["CA-node"]))
g_ext.add((AX["CA4"], RDF.type, AX.SelectionBiasRebuttal_Conflict))  # type de conflit du coup
g_ext.add((AX["CA4"], AX["from"], AX["RA5"]))
g_ext.add((AX["CA4"], ARG.aifAttackedNode, AX["I5"]))               # undermine : la premisse du biais tombe

# --- Le diff de triplets ---
diff_set = set(g_ext) - set(g_base)
print(f"\nLe coup ajoute {len(diff_set)} triplets :")
print("-" * 60)
diff_list = sorted(diff_set, key=lambda t: (str(t[0]), str(t[1]), str(t[2])))
for s, p, o in diff_list:
    print(f"  {s.n3(g_ext.namespace_manager)} {p.n3(g_ext.namespace_manager)} {o.n3(g_ext.namespace_manager)} .")
Etat initial L_t : 64 triplets
  I-nodes : 8    RA-nodes : 4 (RA1..RA4)    CA-nodes : 3 (CA1..CA3)

Le coup ajoute 17 triplets :
------------------------------------------------------------
  <http://example.org/aif-ext#CA4> <http://example.org/aif-ext#from> <http://example.org/aif-ext#RA5> .
  <http://example.org/aif-ext#CA4> rdf:type <http://example.org/aif-ext#SelectionBiasRebuttal_Conflict> .
  <http://example.org/aif-ext#CA4> rdf:type <http://www.arg.dundee.ac.uk/aif#CA-node> .
  <http://example.org/aif-ext#CA4> <https://www.argumentum.games/argumentum_fallacies.owl#aifAttackedNode> <http://example.org/aif-ext#I5> .
  <http://example.org/aif-ext#I7> rdf:type <http://www.arg.dundee.ac.uk/aif#I-node> .
  <http://example.org/aif-ext#I7> rdfs:label "L'etude complementaire couvre tous les profils de membres"@fr .
  <http://example.org/aif-ext#I8> rdf:type <http://www.arg.dundee.ac.uk/aif#I-node> .
  <http://example.org/aif-ext#I8> rdfs:label "Le biais du pilote est corrige"@fr .
  <http://example.org/aif-ext#RA5> <http://www.arg.dundee.ac.uk/aif#Conclusion> <http://example.org/aif-ext#I8> .
  <http://example.org/aif-ext#RA5> <http://www.arg.dundee.ac.uk/aif#Premise> <http://example.org/aif-ext#I7> .
  <http://example.org/aif-ext#RA5> <http://www.arg.dundee.ac.uk/aif#scheme> <http://example.org/aif-ext#RepresentativeSample_Inference> .
  <http://example.org/aif-ext#RA5> rdf:type <http://example.org/aif-ext#RepresentativeSample_Inference> .
  <http://example.org/aif-ext#RA5> rdf:type <http://www.arg.dundee.ac.uk/aif#RA-node> .
  <http://example.org/aif-ext#RepresentativeSample_Inference> rdf:type <http://www.arg.dundee.ac.uk/aif#Inference_Scheme> .
  <http://example.org/aif-ext#RepresentativeSample_Inference> rdfs:subClassOf <http://www.arg.dundee.ac.uk/aif#Inference_Scheme> .
  <http://example.org/aif-ext#SelectionBiasRebuttal_Conflict> rdf:type <http://www.arg.dundee.ac.uk/aif#Conflict> .
  <http://example.org/aif-ext#SelectionBiasRebuttal_Conflict> rdfs:subClassOf <http://www.arg.dundee.ac.uk/aif#Conflict> .

Interpretation — le coup comme double extension

Le diff se lit en deux etages, exactement comme le coup ontologique de SW-14 :

  • vocabulaire (4 triplets) : ax:RepresentativeSample_Inference et ax:SelectionBiasRebuttal_Conflict sont declares rdfs:subClassOf des classes AIF existantes — le coup n’invente pas un formalisme, il etend la taxonomie Argumentum en suivant ses conventions de nommage ;
  • instances (13 triplets) : deux I-nodes, un RA-node type par le schema du coup, un CA-node type par le conflit du coup. L’attaque CA4 vise I5 — la premisse de l’argument de biais RA4 — et non sa conclusion : c’est un undermine, symetrique du CA3 initial.

Un detail technique porte tout le reste : les aretes d’attaque utilisent arg:aifAttackedNode (l’IRI de l’ontologie Argumentum) avec ax:from pour l’attaquant — le pont minimal entre la taxonomie Argumentum et le modele de noeuds AIF.

Exercice 2 — L’admissibilite SHACL et le controle negatif

Enonce : (a) ecrire les shapes qui encodent les contraintes structurelles AIF (un RA-node a au moins une premisse, exactement une conclusion, un schema ; un CA-node a un attaquant type et une cible ; un I-node porte un libelle) ; (b) valider le coup (cas A, attendu conforme) ; (c) construire et valider un coup malforme (cas B, attendu rejete) — un argument sans premisse ni schema, une attaque sans attaquant.

# Exercice 2 — a completer
# Etape 1 : ecrire les 3 NodeShapes (RAShape, CAShape, IShape) avec leurs property shapes
# Etape 2 : valider g_ext (cas A, attendu conforme)
# Etape 3 : construire g_bad — un RA-node sans premisse ni schema, un CA-node sans attaquant — et valider
# Indice : miroir de la cellule 7 de SW-14 — sh:minCount, sh:maxCount, sh:class
from rdflib import Graph
g_shapes = Graph()
print("Exercice a completer : le juge SHACL et son controle negatif")
Exercice a completer : le juge SHACL et son controle negatif

Solution — Exercice 2

# Solution complete — Exercice 2

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

AIF = Namespace("http://www.arg.dundee.ac.uk/aif#")
ARG = Namespace("https://www.argumentum.games/argumentum_fallacies.owl#")
AX  = Namespace("http://example.org/aif-ext#")

# --- Les shapes SHACL : contraintes structurelles du modele AIF ---
g_shapes = Graph()
g_shapes.bind("aif", AIF); g_shapes.bind("arg", ARG); g_shapes.bind("ax", AX); g_shapes.bind("sh", SH)

def pshape(parent, path, mn=None, mx=None, cls=None):
    ps = BNode()
    g_shapes.add((parent, SH.property, ps))
    g_shapes.add((ps, SH.path, path))
    if mn is not None: g_shapes.add((ps, SH.minCount, Literal(mn)))
    if mx is not None: g_shapes.add((ps, SH.maxCount, Literal(mx)))
    if cls  is not None: g_shapes.add((ps, SH["class"], cls))
    return ps

RAShape, CAShape, IShape = AX.RAShape, AX.CAShape, AX.IShape
for sh, target in [(RAShape, AIF["RA-node"]), (CAShape, AIF["CA-node"]), (IShape, AIF["I-node"])]:
    g_shapes.add((sh, RDF.type, SH.NodeShape))
    g_shapes.add((sh, SH.targetClass, target))

pshape(RAShape, AIF.Premise,    mn=1)
pshape(RAShape, AIF.Conclusion, mn=1, mx=1)
pshape(RAShape, AIF.scheme,     mn=1, mx=1)
pshape(CAShape, AX["from"],     mn=1, cls=AIF["RA-node"])
pshape(CAShape, ARG.aifAttackedNode, mn=1)
pshape(IShape,  RDFS.label,     mn=1)

print(f"Shapes charges : {len(g_shapes)} triplets (3 NodeShapes, 6 property shapes)")

# --- CAS A : le coup 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 — le coup de la commission (g_ext)")
print("=" * 60)
print(f"  conforms  : {conforms_a}")

# --- CAS B : le coup malforme (controle negatif) ---
g_bad = Graph()
for t in g_base: g_bad.add(t)
# un argument aveugle : conclusion sans premisse ni schema
g_bad.add((AX["RA6"], RDF.type, AIF["RA-node"]))
g_bad.add((AX["RA6"], AIF.Conclusion, AX["C2"]))
# une attaque orpheline : cible sans attaquant
g_bad.add((AX["CA5"], RDF.type, AIF["CA-node"]))
g_bad.add((AX["CA5"], ARG.aifAttackedNode, AX["I1"]))

conforms_b, report_g_b, report_text_b = validate(
    g_bad, shacl_graph=g_shapes, inference='none', debug=False, serialize_report=False)
print()
print("=" * 60)
print("CAS B — le coup malforme (RA6 sans premisse ni schema, CA5 sans attaquant)")
print("=" * 60)
print(f"  conforms  : {conforms_b}")

if not conforms_b:
    print("  - detail des violations :")
    n_viol = 0
    for s, p, o in sorted(report_g_b.triples((None, SH.result, None)), key=lambda t: str(t[2])):
        focus = path = None
        for s2, p2, o2 in report_g_b.triples((o, None, None)):
            if p2 == SH.focusNode: focus = o2
            if p2 == SH.resultPath: path = o2
        n_viol += 1
        print(f"    violation #{n_viol}: focusNode={focus.n3(g_bad.namespace_manager)}, "
              f"path={path.n3(g_bad.namespace_manager) if path else '(absente)'}")
Shapes charges : 27 triplets (3 NodeShapes, 6 property shapes)
============================================================
CAS A — le coup de la commission (g_ext)
============================================================
  conforms  : True

============================================================
CAS B — le coup malforme (RA6 sans premisse ni schema, CA5 sans attaquant)
============================================================
  conforms  : False
  - detail des violations :
    violation #1: focusNode=<http://example.org/aif-ext#RA6>, path=<http://www.arg.dundee.ac.uk/aif#scheme>
    violation #2: focusNode=<http://example.org/aif-ext#RA6>, path=<http://www.arg.dundee.ac.uk/aif#Premise>
    violation #3: focusNode=<http://example.org/aif-ext#CA5>, path=<http://example.org/aif-ext#from>

Interpretation — un juge qui a dit non

Le cas A passe : le coup de la commission respecte toutes les contraintes structurelles du modele AIF. Le cas B, lui, est rejete sur (au moins) trois violations : RA6 n’a ni premisse ni schema (un argument qui n’en est pas un — une affirmation nue), et CA5 n’a pas d’attaquant (une attaque venue de nulle part).

C’est ce couple conforme/rejete qui donne sa valeur au verdict SHACL : un juge qui n’aurait jamais dit non ne discriminerait rien. La grille AIF encode ce qu’un coup argumentatif admissible doit etre — et le cas B montre qu’elle mord.

Exercice 3 — Le delta d’inferences et la bascule de Dung

Enonce : (a) mesurer le delta d’inferences owlrl \(\mathrm{cl}(L_{t+1}) \setminus \mathrm{cl}(L_t)\) ; (b) projeter le graphe AIF en cadre d’argumentation abstrait (AF de Dung) — un argument est un RA-node, une attaque defeat l’argument vise directement (undercut) ou celui dont la premisse est visee (undermine) ; (c) calculer l’extension grounded avant et apres le coup, et exhiber la bascule.

# Exercice 3 — a completer
# Etape 1 : fermeture owlrl de g_base et g_ext, puis le delta d'inferences
# Etape 2 : project_af — arguments = RA-nodes, attaques = CA-nodes (undercut + undermine par premisse)
# Etape 3 : etiquetage grounded avant/apres le coup
# Indice : un argument non attaque entre IN ; un argument est defendu quand tous ses attaquants sont OUT
print("Exercice a completer : delta owlrl et bascule de l'extension grounded")
Exercice a completer : delta owlrl et bascule de l'extension grounded

Solution — Exercice 3

# Solution complete — Exercice 3

from rdflib import Graph, Namespace
from rdflib.namespace import RDF, RDFS
import owlrl

AIF = Namespace("http://www.arg.dundee.ac.uk/aif#")
ARG = Namespace("https://www.argumentum.games/argumentum_fallacies.owl#")
AX  = Namespace("http://example.org/aif-ext#")

# --- (a) Delta d'inferences owlrl ---
def closure(g_src):
    g_inf = Graph()
    for t in g_src: g_inf.add(t)
    owlrl.DeductiveClosure(owlrl.OWLRL_Semantics).expand(g_inf)
    return set(g_inf)

inf_base, inf_ext = closure(g_base), closure(g_ext)
delta_inf = inf_ext - inf_base
print(f"cl(L_t)   : {len(inf_base)} triplets")
print(f"cl(L_t+1) : {len(inf_ext)} triplets")
print(f"\nDelta d'inferences : {len(delta_inf)} nouveaux triplets")
print("-" * 60)
for s, p, o in sorted(delta_inf, key=lambda t: (str(t[0]), str(t[1]), str(t[2])))[:12]:
    print(f"  {s.n3(g_ext.namespace_manager)} {p.n3(g_ext.namespace_manager)} {o.n3(g_ext.namespace_manager)} .")
if len(delta_inf) > 12:
    print(f"  ... et {len(delta_inf) - 12} de plus.")

# --- (b) Projection AIF -> AF abstrait de Dung ---
def project_af(g):
    """Arguments = RA-nodes ; attaques = CA-nodes (undercut direct + undermine par premisse)."""
    args = {s for s, _, _ in g.triples((None, RDF.type, AIF["RA-node"]))}
    premises_of = {}   # I-node -> ensemble des RA-nodes qui l'utilisent en premisse
    for ra_n, _, i in g.triples((None, AIF.Premise, None)):
        premises_of.setdefault(i, set()).add(ra_n)
    attacks = set()    # (attaquant, vise)
    for ca_n, _, _ in g.triples((None, RDF.type, AIF["CA-node"])):
        frm = next(g.objects(ca_n, AX["from"]), None)
        tgt = next(g.objects(ca_n, ARG.aifAttackedNode), None)
        if frm is None or tgt is None:
            continue
        if tgt in args:                     # undercut : l'attaque vise un argument
            attacks.add((frm, tgt))
        for owner in premises_of.get(tgt, ()):   # undermine : la premisse tombe, l'argument tombe
            if owner != frm:
                attacks.add((frm, owner))
    return args, attacks

def grounded(args, attacks):
    """Etiquetage IN/OUT/UNDEC jusqu'au point fixe (semantique grounded, Dung 1995).

    IN  : non attaque, ou tous ses attaquants sont OUT (defendu) ;
    OUT : attaque par au moins un IN ;
    UNDEC : ni l'un ni l'autre. L'extension grounded = le label IN final.
    """
    attackers = {a: {b for (b, t) in attacks if t == a} for a in args}
    label = {a: "UNDEC" for a in args}
    changed = True
    while changed:
        changed = False
        for a in args:
            if label[a] != "UNDEC":
                continue
            if any(label[b] == "IN" for b in attackers[a]):
                label[a] = "OUT"; changed = True
            elif all(label[b] == "OUT" for b in attackers[a]):
                label[a] = "IN"; changed = True
    return {a for a in args if label[a] == "IN"}

def fmt(ext):
    return sorted(str(x.split('#')[-1]) for x in map(str, ext))

args_b, att_b = project_af(g_base)
G_before = grounded(args_b, att_b)
args_e, att_e = project_af(g_ext)
G_after = grounded(args_e, att_e)

print("\n" + "=" * 60)
print("Projection AF et extension grounded")
print("=" * 60)
print(f"AVANT le coup : AF = {len(args_b)} arguments, {len(att_b)} attaques")
print(f"  attaques       : {sorted((a.split('#')[-1] + ' -> ' + b.split('#')[-1]) for a, b in att_b)}")
print(f"  grounded       : {fmt(G_before)}")
print(f"APRES le coup  : AF = {len(args_e)} arguments, {len(att_e)} attaques")
print(f"  attaques       : {sorted((a.split('#')[-1] + ' -> ' + b.split('#')[-1]) for a, b in att_e)}")
print(f"  grounded       : {fmt(G_after)}")

gained = G_after - G_before
lost = G_before - G_after
print(f"\nBascule du coup : +{fmt(gained)} GAGNES, -{fmt(lost)} PERDUS")
cl(L_t)   : 270 triplets
cl(L_t+1) : 305 triplets

Delta d'inferences : 35 nouveaux triplets
------------------------------------------------------------
  "L'etude complementaire couvre tous les profils de membres"@fr owl:sameAs "L'etude complementaire couvre tous les profils de membres"@fr .
  "Le biais du pilote est corrige"@fr owl:sameAs "Le biais du pilote est corrige"@fr .
  <http://example.org/aif-ext#CA4> <http://example.org/aif-ext#from> <http://example.org/aif-ext#RA5> .
  <http://example.org/aif-ext#CA4> rdf:type <http://example.org/aif-ext#SelectionBiasRebuttal_Conflict> .
  <http://example.org/aif-ext#CA4> rdf:type <http://www.arg.dundee.ac.uk/aif#CA-node> .
  <http://example.org/aif-ext#CA4> rdf:type <http://www.arg.dundee.ac.uk/aif#Conflict> .
  <http://example.org/aif-ext#CA4> rdf:type owl:Thing .
  <http://example.org/aif-ext#CA4> owl:sameAs <http://example.org/aif-ext#CA4> .
  <http://example.org/aif-ext#CA4> <https://www.argumentum.games/argumentum_fallacies.owl#aifAttackedNode> <http://example.org/aif-ext#I5> .
  <http://example.org/aif-ext#I7> rdf:type <http://www.arg.dundee.ac.uk/aif#I-node> .
  <http://example.org/aif-ext#I7> rdf:type owl:Thing .
  <http://example.org/aif-ext#I7> rdfs:label "L'etude complementaire couvre tous les profils de membres"@fr .
  ... et 23 de plus.

============================================================
Projection AF et extension grounded
============================================================
AVANT le coup : AF = 4 arguments, 3 attaques
  attaques       : ['RA1 -> RA2', 'RA2 -> RA1', 'RA4 -> RA3']
  grounded       : ['RA4']
APRES le coup  : AF = 5 arguments, 4 attaques
  attaques       : ['RA1 -> RA2', 'RA2 -> RA1', 'RA4 -> RA3', 'RA5 -> RA4']
  grounded       : ['RA3', 'RA5']

Bascule du coup : +['RA3', 'RA5'] GAGNES, -['RA4'] PERDUS

Interpretation — une consequence qui se mesure deux fois

Cote owlrl, le delta n’est pas vide : la re-typification par les nouvelles sous-classes propage (rdfs9) — RA5 devient instance de Inference_Scheme, CA4 instance de Conflict — et les domaines aif:scheme / ax:from re-typent les nouveaux noeuds. Le coup a des consequences deductives.

Cote Dung, la consequence est plus forte : le coup bascule l’extension grounded. Avant, RA4 (l’argument de biais, inattaque) etait IN et tenait RA3 (l’argument du pilote) en OUT — le debat sur l’adoption restait flottant (RA1/RA2 mutuellement attaquants, UNDEC). Apres, RA5 — inattaque — fait tomber la premisse I5 de RA4 : RA4 passe OUT, RA3 est rehabilite IN. L’extension grounded passe de {RA4} a {RA3, RA5}.

C’est la difference entre une decoration (des triplets en plus sans effet) et un coup : ici, une etude complementaire typee par un nouveau schema renverse le verdict d’acceptabilite d’un argument existant. La meme distinction que le coup nul de SW-14, mais vue du cote de la controverse.

Exercice 4 — La provenance du coup (reification RDF classique + PROV)

Enonce : (a) encapsuler le triplet signature du coup (RA5 rdf:type ax:RepresentativeSample_Inference) par reification RDF classique (rdf:Statement) et lui attacher la provenance : quel agent a propose le coup, quand ; (b) interrogation SPARQL : retrouver les elements du coup attribues a cet agent.

Note de transparence : la syntaxe RDF-star (<< s p o >>, quoted triples) est la cible conceptuelle — presentee comme telle dans SW-10-Python-RDFStar. Sur rdflib 7.6, le support natif n’est pas disponible : la cellule de solution teste la capacite (try: from rdflib.term import Triple) et retombe sur la reification classique, l’implementation pratique et executee. C’est la double lecture etablie par SW-10.

# Exercice 4 — a completer
# Etape 1 : encapsuler le triplet signature du coup en rdf:Statement
# Etape 2 : attacher prov:wasAttributedTo (l'agent) et prov:generatedAtTime
# Etape 3 : requete SPARQL listant les coups attribues a cet agent
# Indice : miroir de la cellule 13 de SW-14 — l'encapsulation puis le filtre sur l'agent
from rdflib import Graph
print("Exercice a completer : la provenance du coup (reification + PROV)")
Exercice a completer : la provenance du coup (reification + PROV)

Solution — Exercice 4

# Solution complete — Exercice 4

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

# Support RDF-star natif (Turtle-Star / quoted triple) ? Meme convention que SW-10 :
# on teste la CAPACITE, pas le numero de version. La reification classique rdf:Statement
# est l'implementation pratique ; RDF-star (<< s p o >>) reste la syntaxe cible documentee.
# voir SW-10-Python-RDFStar.ipynb, qui etablit cette double lecture sur rdflib 7.6.
try:
    from rdflib.term import Triple as QuotedTriple
    RDFSTAR_NATIVE = True
except ImportError:
    RDFSTAR_NATIVE = False

print(f"Support RDF-star natif (quoted triple) : {'OUI' if RDFSTAR_NATIVE else 'NON — reification classique utilisee'}")

AIF = Namespace("http://www.arg.dundee.ac.uk/aif#")
AX  = Namespace("http://example.org/aif-ext#")

g_prov = Graph()
for t in g_ext: g_prov.add(t)
g_prov.bind("aif", AIF); g_prov.bind("ax", AX); g_prov.bind("prov", PROV)

# --- Reification RDF classique du triplet signature (rdf:Statement) ---
assertion = BNode()
g_prov.add((assertion, RDF.type, RDF.Statement))
g_prov.add((assertion, RDF.subject, AX["RA5"]))
g_prov.add((assertion, RDF.predicate, RDF.type))
g_prov.add((assertion, RDF.object, AX.RepresentativeSample_Inference))

# --- Provenance : QUI a propose le coup, et QUAND ---
g_prov.add((AX.agentCommission, RDFS.label, Literal("Commission de revision des debats", lang="fr")))
g_prov.add((assertion, PROV.wasAttributedTo, AX.agentCommission))
g_prov.add((assertion, PROV.generatedAtTime, Literal("2026-08-30T12:00:00", datatype=XSD.dateTime)))

print(f"Graphe avec provenance : {len(g_prov)} triplets")

# --- Interrogation SPARQL ---
sparql_q = '''
PREFIX prov: <http://www.w3.org/ns/prov#>
PREFIX rdf:  <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX ax:   <http://example.org/aif-ext#>

SELECT ?agent ?sujet ?objet ?moment
WHERE {
  ?assertion a rdf:Statement ;
             rdf:subject ?sujet ;
             rdf:predicate rdf:type ;
             rdf:object ?objet ;
             prov:wasAttributedTo ?agent ;
             prov:generatedAtTime ?moment .
  FILTER(?agent = ax:agentCommission)
}
'''

results = list(g_prov.query(sparql_q))
print(f"\nResultat SPARQL : {len(results)} coup(s) attribue(s)")
for row in results:
    print(f"  agent   = {row['agent'].n3(g_prov.namespace_manager)}")
    print(f"  triplet = {row['sujet'].n3(g_prov.namespace_manager)} rdf:type {row['objet'].n3(g_prov.namespace_manager)}")
    print(f"  moment  = {row['moment'].n3(g_prov.namespace_manager)}")
Support RDF-star natif (quoted triple) : NON — reification classique utilisee
Graphe avec provenance : 88 triplets

Resultat SPARQL : 1 coup(s) attribue(s)
  agent   = ax:agentCommission
  triplet = ax:RA5 rdf:type ax:RepresentativeSample_Inference
  moment  = "2026-08-30T12:00:00"^^xsd:dateTime

Interpretation — la provenance comme memoire du geste

La reification RDF classique du triplet signature + prov:wasAttributedTo nomme l’auteur du coup : la commission de revision, a l’instant ou elle l’a propose. Comme dans SW-14, cette trace repond a la question qui surgit des qu’un vocabulaire est etendu sous controverse : qui a propose ce schema, et au nom de quoi ? La requete SPARQL la rend interrogeable mecaniquement — la memoire du debat est dans le graphe, pas dans un paratexte.

RDF-star (<< s p o >>) ferait la meme chose avec une syntaxe plus compacte (le triplet signe comme sujet de l’annotation, sans recours a rdf:Statement) — mais, comme SW-10-Python-RDFStar le documente, rdflib 7.6 ne supporte pas encore la syntaxe quoted-triple native. La cellule de solution l’annonce explicitement en executant la portee (le try: from rdflib.term import Triple), et la reification classique est l’implementation qui s’execute ici.

Conclusion — la greffe complete

Les quatre gestes du coup ontologique de SW-14, reexecutes sur un graphe d’argumentation AIF :

Geste SW-14 (ontologie bibliotheque) SW-15 (graphe AIF)
Extension \(\eta\) classe Ebook + propriete fmt schema RepresentativeSample_Inference + conflit SelectionBiasRebuttal_Conflict + instances
Juge SHACL fmt obligatoire sur Ebook contraintes structurelles AIF ; coup malforme rejete (controle negatif)
Consequence delta owlrl non vide delta owlrl et bascule de l’extension grounded {RA4} -> {RA3, RA5}
Provenance ex:alice attribue l’assertion Ebook (reification + PROV) ax:agentCommission attribue le coup argumentatif (reification + PROV)

La greffe tient en une phrase : le coup ontologique est le format naturel d’un coup argumentatif — etendre le vocabulaire d’un debat (nouveaux schemes, nouveaux types de conflit) est une operation sur \(L_t\) dont l’admissibilite se juge (SHACL) et la consequence se mesure (owlrl + Dung). Ce que SW-14 montrait sur une ontologie se transplante sans changement d’outils sur la controverse elle-meme.

Perspective — le prolongement naturel est le lien avec les notebooks Argument_Analysis_* : la taxonomie complete des schemes Walton (1 509 classes) comme reserve de vocabulaire pour de futurs coups, et les semantiques preferred/stable comme juges alternatifs au grounded choisi ici (le plus sceptique — donc le plus difficile a faire basculer : un coup qui deplace le grounded deplace a fortiori les autres).

Retour au sommet