La base de connaissances d’un débat — propositions, arguments, conflits
Série Argument_Analysis. Ce carnet distille le module knowledge_base du moteur de débat : la structure de données qui enregistre ce qui a été dit dans un débat formel — les propositions posées, les arguments avancés, et le conflit ouvert entre eux. Il complète la série en descendant d’un cran : Toulmin_Model déplie la structure d’UN argument, Schemes_Walton identifie sa forme stéréotypée, Dialogues_Protocolises organise qui parle quand — la base de connaissances est la mémoire commune où tous ces actes laissent une trace.
Provenance et contrat de fidélité. Le module distillé vient du moteur étudiant 1_2_7_argumentation_dialogique/local_db_arg/src/core/knowledge_base.py — chemin toujours présent dans le dépôt tronc. Il avait été restauré un temps dans le paquet argumentation_analysis/agents/core/debate/ (G8 #1184), puis ce chemin a été retiré du tronc par le triage #2137 (PR #2273) : ce carnet en assume désormais la conservation pédagogique, en le disant. Le port est verbatim sur le comportement Le port est verbatim sur le comportement et documente quatre divergences mesurées au lieu de les corriger en silence : rules et preferences (état mort jamais lu, non portés), entails (un test d’appartenance, pas un moteur d’inférence), la négation purement lexicale (¬¬P n’est pas P), et l’écrasement des propositions homonymes. La règle du dépôt s’applique : on restaure, on ne fabrique pas.
L’organe est déterministe : aucun LLM, aucune JVM, aucun aléatoire qui affecte un résultat. Tout ce que ce carnet affiche est rejouable à l’identique. Les exemples sont synthétiques et domaine-public — le banc de démonstration est celui du carnet source EPITA (deux camps opposés sur la concentration en télétravail).
Dans le moteur étudiant d’origine, cette base est le socle sur lequel tournent les stratégies de débat : chaque tour de dialogue interroge find_supporting_arguments pour savoir ce qui peut être avancé, et is_consistent pour savoir si le désaccord persiste. Le carnet n’emporte pas ces stratégies (elles appellent des LLM — c’est précisément le bruit que la distillation retire) : il isole la mémoire, pour qu’on puisse l’étudier sans dépendance. C’est la même discipline que pour les schémas de Walton : l’objet pédagogique est la structure déterministe, pas le service qui la consomme.
Ce que couvre ce carnet :
§1 la population transitive — l’argument porte ses prémisses et sa conclusion ;
§2 support et attaque — la convention lexicale de négation ¬ ;
§2 bis la mémoire vivante — le lien dirigé argument_network que le tronc a retenu pour la limite mesurée en §2 ;
§3 la cohérence — P et ¬P cohabitants signalent un débat ouvert, et le nommage honnête de entails ;
§4 les limites mesurées — non-inférence, double négation, écrasement ;
trois exercices.
§1 — Le modèle : population transitive
La base tient en deux dictionnaires : propositions (indexé par contenu) et arguments (indexé par identifiant). La méthode centrale est add_argument : elle enregistre l’argument et enregistre chacune de ses prémisses et sa conclusion comme propositions. C’est ce que nous appelons la population transitive — personne n’a besoin d’ajouter « Le télétravail isole les collaborateurs » explicitement : l’argument qui la cite comme prémisse la porte en base.
# Imports + construction du banc de demonstration (deux bases, comme le carnet source).from knowledge_base import FormalArgument, KnowledgeBase, Proposition, negationdef construire_deux_camps(): kb = KnowledgeBase() kb.add_argument(FormalArgument( premises=[Proposition(content="Le télétravail isole les collaborateurs")], conclusion=Proposition(content="Le télétravail réduit la concentration"), scheme="cause_effect", )) kb.add_argument(FormalArgument( premises=[Proposition(content="Les pauses régulières restaurent la concentration")], conclusion=Proposition(content="¬Le télétravail réduit la concentration"), ))return kbdef construire_un_seul_camp(): kb = KnowledgeBase() kb.add_argument(FormalArgument( premises=[Proposition(content="Le télétravail isole les collaborateurs")], conclusion=Proposition(content="Le télétravail réduit la concentration"), scheme="cause_effect", ))return kbkb = construire_deux_camps()kb_saine = construire_un_seul_camp()print(f"Propositions enregistrées ({len(kb.get_all_propositions())}) :")for p in kb.get_all_propositions():print(f" - {p.content}")print(f"Arguments enregistrés : {len(kb.get_all_arguments())}")
Propositions enregistrées (4) :
- Le télétravail isole les collaborateurs
- Le télétravail réduit la concentration
- Les pauses régulières restaurent la concentration
- ¬Le télétravail réduit la concentration
Arguments enregistrés : 2
Lecture du résultat. Quatre propositions pour deux arguments : les deux conclusions (« Le télétravail réduit la concentration » et sa négation préfixée « ¬Le télétravail réduit la concentration ») et les deux prémisses (« …isole les collaborateurs », « Les pauses régulières restaurent la concentration »). Aucune des deux prémisses n’a été ajoutée par un add_proposition explicite — c’est add_argument qui les a portées. Un décompte qui mérite l’œil : deux arguments, chacun portant une prémisse et une conclusion, cela fait quatre ajouts — et exactement quatre propositions. Aucun doublon ne s’est déclaré parce qu’aucune prémisse n’est partagée entre les deux arguments ; le §4 montrera ce qui arrive quand deux propositions de même contenu se présentent. Notez la convention déjà visible dans la liste : la négation n’est pas un champ ou un booléen, c’est le préfixe de chaîne ¬ collé au contenu — nous y revenons au §2.
# La preuve de la population transitive : une prémisse jamais ajoutée explicitement.premisse = Proposition(content="Le télétravail isole les collaborateurs")print(f"entails(premisse) = {kb.entails(premisse)}")print("'Le télétravail isole les collaborateurs' n'a jamais été ajoutée explicitement —")print("c'est l'argument qui la porte : la population est transitive.")assert kb.entails(premisse)# Et une proposition que PERSONNE n'a posee :intrus = Proposition(content="Le télétravail augmente la productivité")print(f"entails(intrus) = {kb.entails(intrus)}")assertnot kb.entails(intrus)
entails(premisse) = True
'Le télétravail isole les collaborateurs' n'a jamais été ajoutée explicitement —
c'est l'argument qui la porte : la population est transitive.
entails(intrus) = False
Lecture du résultat.True pour la prémisse portée, False pour l’intrus. Le test entails ne fait qu’une chose — vérifier l’appartenance au dictionnaire des propositions — mais la population transitive lui donne déjà un intérêt : il distingue ce qui a été dit (posé ou porté par un argument) de ce qui ne l’a pas été. La distinction est opérationnelle, pas philosophique : un agent de dialogue qui prépare son prochain coup demande à la mémoire « puis-je m’appuyer sur cette prémisse ? » — la réponse dépend de si elle a été posée, pas de si elle est vraie. C’est une mémoire de débat, pas une théorie logique : le §4 mesurera précisément ce que cette mémoire ne sait pas faire, notamment quand une conclusion « devrait » suivre sans qu’aucun argument ne l’ait jamais conclue.
§2 — Support et attaque : qui conclut quoi
Deux requêtes parcourent les arguments : find_supporting_arguments(P) renvoie ceux dont la conclusion est exactement P, find_attacking_arguments(P) ceux dont la conclusion est "¬" + contenu de P. La convention est lexicale — la négation est une construction de chaîne, pas un opérateur — et elle a une conséquence directe sur ce qu’une « attaque » est ici : un fait de conclusion, pas un acte dirigé. La base ne sait pas QUEL argument répond à quel autre, ni dans quel ordre ; elle sait seulement que la négation de P a été conclue quelque part. Le port expose le helper negation() pour rendre cette convention explicite et testable (le tronc construit la chaîne inline à chaque appel).
# Requêtes support/attaque sur la base à deux camps.prop = Proposition(content="Le télétravail réduit la concentration")soutiens = kb.find_supporting_arguments(prop)attaques = kb.find_attacking_arguments(prop)print(f"Soutiens de « {prop.content} » : {len(soutiens)}")for a in soutiens:print(f" {a} (schéma : {a.scheme})")print(f"Attaques (conclusion == negation(prop) == « {negation(prop).content} ») : {len(attaques)}")for a in attaques:print(f" {a} (schéma : {a.scheme})")assertlen(soutiens) ==1andlen(attaques) ==1
Soutiens de « Le télétravail réduit la concentration » : 1
[Le télétravail isole les collaborateurs] -> Le télétravail réduit la concentration (schéma : cause_effect)
Attaques (conclusion == negation(prop) == « ¬Le télétravail réduit la concentration ») : 1
[Les pauses régulières restaurent la concentration] -> ¬Le télétravail réduit la concentration (schéma : None)
Lecture du résultat. Un soutien, une attaque — le débat est équilibré. Le soutien conclut exactement P ; l’attaque conclut exactement ¬P (préfixe compris). Deux détails mesurables : le schéma est cause_effect sur le soutien et None sur l’attaque (le moteur étudiant ne labelait pas tous ses arguments), et le rendu str de l’argument affiche [prémisses] -> conclusion — c’est la convention d’affichage du port, pratique pour lire une base d’un coup d’œil. L’attaque ne réfute rien : elle coexiste — c’est le §3 qui dira si cette coexistence est un conflit.
# La même requête sur la base à un seul camp : aucune négation cohabitante.prop2 = Proposition(content="Le télétravail réduit la concentration")soutiens2 = kb_saine.find_supporting_arguments(prop2)attaques2 = kb_saine.find_attacking_arguments(prop2)print(f"Base saine — soutiens : {len(soutiens2)}, attaques : {len(attaques2)}")assertlen(soutiens2) ==1andlen(attaques2) ==0
Base saine — soutiens : 1, attaques : 0
Lecture du résultat. Même proposition, même requête, autre base : cette fois aucune attaque — personne n’a conclu ¬P. La structure de la question ne change pas, c’est l’état de la mémoire qui diffère — et c’est la leçon de méthode : interroger une base de connaissances ne révèle jamais que son état courant, jamais l’historique qui l’a produit. C’est tout ce que « attaquer » veut dire ici : avoir déjà conclu la négation de la cible. Une attaque n’est pas un acte daté ni un lien dirigé entre arguments (pour cela, voir les cadres d’argumentation abstraits de Dung_AF_Semantics, où les attaques sont des arêtes nommées d’un nœud vers un autre) — c’est un fait statistique sur les conclusions enregistrées.
§2 bis — La mémoire vivante : le lien dirigé que la base n’a pas
Le §2 a mesuré la limite : dans la base, une attaque est un fait de conclusion (¬P cohabite avec P), pas un acte dirigé — rien ne dit quel argument répond à quel argument, ni dans quel ordre. Le tronc EPITA a retenu exactement cette structure ailleurs : l’état vivant du débat, DebateState porte le champ argument_network: Dict[str, List[str]] — le graphe dirigé des réponses. La cellule suivante le distille (trois arguments nommés, deux arêtes) : c’est l’exact contraire du fait statistique du §2 — ici, l’arête est l’acte. Contrairement aux cadres abstraits de Dung_AF_Semantics (des arêtes entre arguments abstraits), ce réseau vit dans la mémoire du débat en cours, à côté des votes d’audience et de la phase — c’est ce qui permet aux stratégies du moteur de savoir à qui répondre.
# §2 bis — Distillation du champ argument_network de DebateState# (tronc EPITA argumentation_analysis/agents/core/debate/debate_definitions.py,# SHA ecfd9b9c : class DebateState L77-91, champ argument_network L89).# La base du carnet ne sait PAS dire qui repond a qui (mesure du §2) ;# le graphe dirige ci-dessous est la structure que le tronc a retenue.arguments = {"A_teletravail": "Le teletravail reduit la concentration des equipes","B_rythme": "Le rythme des jours equipe fixe la concentration, pas le lieu","C_correctif": "Les journees equipe hebdomadaires restaurent la concentration",}# argument_network : Dict[str, List[str]] -- l’arete (X, Y) dit « X repond a Y ».argument_network = {"B_rythme": ["A_teletravail"], # B refute la these de A"C_correctif": ["A_teletravail"], # C propose un correctif a la these de A"A_teletravail": [], # A ouvre le debat : ne repond a personne}# La requete DIRIGEE que la base de connaissances du carnet ne peut pas exprimer :cible ="A_teletravail"repondants = [src for src, cibles in argument_network.items() if cible in cibles]print(f"Qui repond a {cible} ? -> {repondants}")print(f"Ordre du debat : B repond a {argument_network['B_rythme'][0]}, C repond a {argument_network['C_correctif'][0]}")print("Degre sortant : "+", ".join(f"{k} -> {len(v)}"for k, v in argument_network.items()))
Qui repond a A_teletravail ? -> ['B_rythme', 'C_correctif']
Ordre du debat : B repond a A_teletravail, C repond a A_teletravail
Degre sortant : B_rythme -> 1, C_correctif -> 1, A_teletravail -> 0
Lecture du résultat. L’arête dit ce que la base ne dira jamais : B_rythmerépond àA_teletravail — un lien daté, dirigé, interrogeable dans les deux sens (qui répond à A ?, vers qui parle B ?). La même information dans la base du carnet serait deux conclusions opposées (P et ¬P) sans aucun moyen de relier les arguments qui les portent. Le tronc a gardé les deux organes séparés — la base pour la cohérence (§3), le réseau pour le tour de parole — et DebateState les réunit au moment du débat : la limite mesurée en §2 n’est donc pas un défaut de conception, c’est la frontière exacte entre mémoire et orchestration.
§3 — Cohérence, et un nommage honnête
is_consistent répond False dès qu’une proposition et sa négation cohabitent dans la base — sans notion de degré : un conflit ouvert sur UNE proposition suffit à déclarer toute la base incohérente, même si cent autres propositions sont d’accord entre elles. La cohérence d’un débat est ici binaire et globale, exactement comme celle d’une théorie logique. Et entails mérite qu’on le lise pour ce qu’il est : un test d’appartenance (content in propositions), pas un moteur d’inférence. La docstring du tronc le dit honnêtement (« contains ») ; la docstring du moteur étudiant promettait « implique » — le nom est plus ambitieux que la sémantique, et le savoir évite d’y brancher une attente déductive. Cette honnêteté de nommage est une leçon en soi : quand un nom promet plus que sa implémentation, le premier réflexe est de lire l’implémentation.
# Cohérence sur les deux bases + le banc de requêtes complet du carnet source.print(f"deux_camps is_consistent -> {kb.is_consistent()}")print(f"un_seul_camp is_consistent -> {kb_saine.is_consistent()}")assert kb.is_consistent() isFalseassert kb_saine.is_consistent() isTruequeries = [ ("Le télétravail isole les collaborateurs", True), ("Le télétravail augmente la productivité", False), ("¬Le télétravail réduit la concentration", True),]for contenu, attendu in queries: got = kb.entails(Proposition(content=contenu))print(f"entails({contenu!r:<50}) = {got} (attendu {attendu})")assert got == attendu
Lecture du résultat. La base à deux camps est incohérente — P et ¬P y cohabitent, le débat sur la concentration est ouvert ; la base à un camp est cohérente. C’est is_consistent qui joue le rôle de détecteur de désaccord : une base de débat cohérente est une base où il ne reste rien à débattre. Le banc de requêtes confirme la lecture d’appartenance dans les deux sens : la négation ¬P est « entraînée » au même titre que P — pour entails, c’est une proposition comme une autre, aucune sémantique de conflit dans le test lui-même.
§4 — Les limites mesurées : ce que la base ne sait PAS faire
Trois limites du modèle, mesurées plutôt que supposées : chacune est démontrée par une cellule exécutable ci-dessous, sur un cas où le comportement diverge de ce qu’une intuition logique classique prédit. Elles ne sont pas des défauts du port — chacune est un choix du moteur d’origine, documenté ici pour que l’utilisateur sache exactement ce qu’il tient (et ce qu’il ne tient pas). C’est la même discipline que pour les divergences des schémas de Walton : le port ne « répare » pas la sémantique mesurée, il la rend visible.
entails ne combine rien — même depuis une contradiction, il n’entraîne pas une proposition arbitraire (pas de principe d’explosion).
La négation est lexicale — ¬¬P n’est pas reconnu comme P.
L’écrasement par contenu — deux propositions homonymes ne laissent qu’un exemplaire.
# Limite 1 : pas d'explosion. Logique classique : d'une contradiction tout s'ensuit.# Ici, la base est incohérente et entails(arbitraire) reste faux.q = Proposition(content="Une proposition quelconque jamais posée")print(f"base incohérente : is_consistent = {kb.is_consistent()}")print(f"entails(arbitraire) = {kb.entails(q)} <- pas d'ex falso quodlibet")assertnot kb.entails(q)# Une proposition que personne n'a posée n'est jamais portée, même "évidente" :kb_min = KnowledgeBase()kb_min.add_proposition(Proposition(content="A"))print(f"base = {{A}} : entails(B) = {kb_min.entails(Proposition(content='B'))}")assertnot kb_min.entails(Proposition(content="B"))
base incohérente : is_consistent = False
entails(arbitraire) = False <- pas d'ex falso quodlibet
base = {A} : entails(B) = False
Lecture du résultat. La mémoire ne raisonne pas : elle regarde. Depuis P et ¬P cohabitants — l’état le plus chargé d’une logique classique, d’où TOUT s’ensuit par explosion — le test d’appartenance répond toujours False pour une proposition non posée. La seconde moitié de la cellule montre l’autre face : une base réduite à {A} n’entraîne pas B, même si l’utilisateur « voit » un lien. C’est la frontière exacte entre mémoire et moteur : pour enchaîner prémisses et conclusions, il faut un vrai moteur — le solveur de la série SMT/Z3-API pour la déduction, ou les sémantiques de Dung_AF_Semantics pour calculer ce qui survit aux attaques. La base, elle, se contente de répondre « est-ce que ça a été dit ? ». Pour un débat d’opinion, cette modestie est une vertu : personne ne veut qu’une mémoire d’échanges fabrique des conclusions que personne n’a tirées.
# Limite 2 : la double négation n'existe pas. negation(negation(P)) = « ¬¬P », pas « P ».p = Proposition(content="Le débat est ouvert")double = negation(negation(p))print(f"P = {p.content!r}")print(f"negation(negation(P)) = {double.content!r}")kb_dn = KnowledgeBase()kb_dn.add_proposition(p)kb_dn.add_proposition(double)print(f"Base {{P, ¬¬P}} : is_consistent = {kb_dn.is_consistent()} <- aucune paire P/¬P exacte")assert kb_dn.is_consistent()assert kb_dn.entails(p) and kb_dn.entails(double)
P = 'Le débat est ouvert'
negation(negation(P)) = '¬¬Le débat est ouvert'
Base {P, ¬¬P} : is_consistent = True <- aucune paire P/¬P exacte
Lecture du résultat. P et ¬¬P cohabitent sans conflit : is_consistent cherche la paire exacte (contenu, ¬ + contenu), et ¬¬P n’est le négation d’aucun des deux — la cellule affiche bien les DEUX propositions comme présentes (entails répond deux fois vrai), et pourtant aucune incohérence. Classiquement, ¬¬P est équivalent à P ; lexicalement, ce sont deux chaînes distinctes sans aucune relation. La convention est simple et vérifiable — mais elle impose une discipline à l’utilisateur : la forme exacte du contenu compte, caractère par caractère, y compris les espaces. Une base alimentée par des sources qui négativent différemment (« non-P », « il est faux que P ») ne détectera aucun de ces conflits — normaliser les énoncés en amont est le travail d’une couche que ce moteur n’a pas.
# Limite 3 : l'écrasement par contenu. Deux Proposition de même contenu, un seul exemplaire.kb_ec = KnowledgeBase()kb_ec.add_proposition(Proposition(content="La réunion est utile", confidence=0.9, source="expert"))kb_ec.add_proposition(Proposition(content="La réunion est utile", confidence=0.1, source="sondage"))restantes = kb_ec.get_all_propositions()print(f"Propositions en base : {len(restantes)}")for prop in restantes:print(f" {prop.content!r} confidence={prop.confidence} source={prop.source!r}")assertlen(restantes) ==1and restantes[0].confidence ==0.1# L'égalité STRUCTURELLE l'explique : deux Proposition de même contenu sont égales.a = Proposition(content="P", confidence=0.9)b = Proposition(content="P", confidence=0.2)print(f"Proposition('P', 0.9) == Proposition('P', 0.2) : {a == b} (hash égaux : {hash(a) ==hash(b)})")
Propositions en base : 1
'La réunion est utile' confidence=0.1 source='sondage'
Proposition('P', 0.9) == Proposition('P', 0.2) : True (hash égaux : True)
Lecture du résultat. Le dictionnaire est indexé par contenu : la seconde écriture écrase la première, et c’est le sondage (confidence 0,1) qui survit au expert (0,9) — l’ordre d’arrivée décide, pas la qualité. Dans un débat multi-agents où chacun dépose sa version de la même thèse, cela signifie concrètement : la dernière source qui parle gagne les métadonnées, sans trace de la précédente. La cause structurelle est en bas de cellule : l’égalité de Proposition ignore confidence, truth_value et source ; seul compte le contenu (et le hachage suit, ce qui autorise le dictionnaire). Ce choix a une vertu — une proposition reste unique en base, quel que soit le nombre d’arguments qui la portent, et le décompte du §1 reste stable — et un coût : les métadonnées de la dernière écriture écrasent silencieusement les précédentes.
§5 — Exercices
Convention C.1 : les stubs s’exécutent sans erreur (jamais raise). Remplir le corps, re-exécuter, vérifier.
Exercice 1 — Compter les soutiens d’une proposition
Construire une base de trois arguments dont deux concluent « Le tramway réduit les embouteillages » (prémisses au choix) et un conclut autre chose, puis afficher le nombre de soutiens de cette proposition.
# Exercice 1 : la base doit rendre exactement 2 soutiens pour la proposition cible.ma_base = KnowledgeBase()# TODO etudiant : ajouter trois arguments (deux concluants vers la cible, un autre).cible = Proposition(content="Le tramway réduit les embouteillages")nb_soutiens =0# TODO etudiant : remplacer par len(ma_base.find_supporting_arguments(cible))print(f"Exercice a completer — soutiens attendus : 2, obtenu : {nb_soutiens}")
Partir d’une base vide et la rendre incohérente avec un seul appel à add_argument. Indice : la conclusion de l’argument porte sa propre négation… si la conclusion est ¬Q, que faut-il avoir posé pour que Q et ¬Q cohabitent ?
# Exercice 2 : is_consistent() doit devenir False avec UN SEUL add_argument.kb_defi = KnowledgeBase()kb_defi.add_proposition(Proposition(content="La formation est utile")) # pose Q# TODO etudiant : un seul add_argument dont la conclusion est la négation lexicale de Q.verdict = kb_defi.is_consistent()print(f"Exercice a completer — base cohérente : {verdict}")
Exercice a completer — base cohérente : True
Exercice 3 — Prédire avant de tester
Une base contient « Le débat est vif » et « ¬¬Le débat est vif ». Avant d’exécuter la cellule suivante, noter votre prédiction : is_consistent répond-il True ou False, et entails(« ¬Le débat est vif ») ? Puis remplir la cellule pour vérifier.
# Exercice 3 : prédire, puis vérifier.kb_predit = KnowledgeBase()kb_predit.add_proposition(Proposition(content="Le débat est vif"))kb_predit.add_proposition(Proposition(content="¬¬Le débat est vif"))predictions = [] # TODO etudiant : [is_consistent attendu, entails('¬Le débat est vif') attendu]print(f"Vos prédictions : {predictions}")print(f"Réel : is_consistent = {kb_predit.is_consistent()}, "f"entails('¬...') = {kb_predit.entails(Proposition(content='¬Le débat est vif'))}")
add_argument est transitif : prémisses et conclusion entrent dans la base avec l’argument — la mémoire du débat se peuple par les actes eux-mêmes ;
support = conclusion identique ; attaque = conclusion préfixée ¬ — une convention lexicale, simple et vérifiable, qui impose une discipline de forme exacte ;
is_consistent détecte le conflit ouvert (P et ¬P cohabitants) — c’est lui qui signale qu’un débat a lieu, et lui seul ;
entails est un test d’appartenance : nommage honnête dans la docstring du tronc, attente déductive à ne pas y brancher — pas d’explosion depuis une contradiction, pas de chaînage ;
les limites mesurées, portées au grand jour : rules/preferences morts non portés, ¬¬P distinct de P, écrasement des propositions homonymes par ordre d’arrivée. Aucune n’est un défaut du port : chacune est une mesure du moteur d’origine.
Position dans la série : ce carnet est la mémoire du débat — la structure où Toulmin_Model dépose ses arguments structurés, où Schemes_Walton étiquette leurs schémas, et que les cadres de Dung_AF_Semantics lisent pour calculer ce qui survit aux attaques. Les trois niveaux de l’argumentation computationnelle : dire (les actes de langage formalisés), se souvenir (cette base), arbitrer (les sémantiques d’acceptabilité). Un moteur de débat complet boucle les trois — la distillation isole le second pour qu’il soit étudiable seul, déterministe, et sans dépendance.