diff --git a/docs/taxonomy/498-reconciliation-p1-annotations.csv b/docs/taxonomy/498-reconciliation-p1-annotations.csv new file mode 100644 index 00000000..cd2b17ee --- /dev/null +++ b/docs/taxonomy/498-reconciliation-p1-annotations.csv @@ -0,0 +1,15 @@ +fallacy_pk,family,fallacy_name,skos_signature,RA_scheme,attack_type,attacked_component,AIF_attackType,AIF_attackedNode,precedent_pk,confidence,justification +1198,Cheating,Essentialisme,Direct=- | Exception=Preference_Scheme | skos:broadMatch,Preference_Scheme (exception scheme),undermine,premise (I),undermine,I-node,953,HIGH,L'essentialisme pose une essence fixe comme premisse non justifiee — undermine. Precedent Preference_Scheme→undermine (pk953). +1083,Cheating,Apophénie,Direct=- | Exception=Sign_Inference | skos:broadMatch,Sign_Inference (exception scheme),undermine,premise (I),undermine,I-node,357,HIGH,L'apophenie voit un signe la ou il n'y en a pas : la premisse-signe est spurious — undermine. Precedent Sign_Inference→undermine (pk357). +1090,Cheating,Biais émotionnels,Direct=OppositeConsequences_Conflict | Exception=Preference_Scheme | skos:broadMatch,Preference_Scheme (exception) + OppositeConsequences_Conflict (CQ),undermine,premise (I),undermine,I-node,953,MED,"Les biais emotionnels substituent une preference affective a la justification (premisse) — undermine. Vote precedent depuis Preference_Scheme (pk953). Flag: CQ primaire OppositeConsequences_Conflict sans precedent direct ; undermine retenu par semantique (premisse affective), coherent avec la famille des biais." +1092,Cheating,Biais de négativité,Direct=- | Exception=NegativeConsequences_Inference | skos:broadMatch,NegativeConsequences_Inference (exception scheme),undermine,premise (I),undermine,I-node,322,HIGH,"Le biais de negativite sur-pese les consequences negatives : premisse deformee — undermine. Precedent NegativeConsequences_Inference→undermine (pk322 Repoussoir), meme token." +1104,Cheating,Biais d'autocomplaisance,Direct=- | Exception=PositiveConsequences_Inference | skos:broadMatch,PositiveConsequences_Inference (exception scheme),undermine,premise (I),undermine,I-node,300,HIGH,"Le biais d'autocomplaisance sur-pese les consequences positives pour soi : premisse deformee — undermine. Precedent PositiveConsequences_Inference→undermine (pk300 Connivence), meme token." +1,Insufficiency,Insuffisance,Direct=GeneralAcceptanceDoubt_Conflict | Exception=- | skos:broadMatch,GeneralAcceptanceDoubt_Conflict (CQ questioning that the premise is genuinely accepted),undermine,premise (I),undermine,I-node,953+177,HIGH,"Le doute sur l'acceptation generale attaque l'acceptabilite de la premisse (l'argument repose sur une adhesion non etablie) — undermine. Precedents pk953 Attention selective + pk177 Langage persuasif, memes tokens, undermine." +70,Insufficiency,Préjugé,"Direct=Bias_Inference | Exception=PopularOpinion_Inference, EstablishedRule_Inference, PracticalReasoning_Inference, PositionToKnow_Inference, Preference_Scheme, PresumptiveInference_Scheme | skos:broadMatch",Bias_Inference + 6 schemes (PopularOpinion/EstablishedRule/PracticalReasoning/PositionToKnow/Preference/PresumptiveInference),undermine,premise (I),undermine,I-node,177+340+953,HIGH,"Le prejuge attaque l'acceptabilite des premisses (schemes presomptifs appliques sans verification) — undermine. Six tokens de scheme, tous avec precedent undermine (pk177/340/953). Convergence forte." +133,Insufficiency,Surinterprétation,Direct=LackOfCompleteKnowledge_Conflict | Exception=Sign_Inference | skos:broadMatch,Sign_Inference (exception scheme) + LackOfCompleteKnowledge_Conflict (CQ),undermine,premise (I),undermine,I-node,357,HIGH,La surinterpretation traite un signe faible comme suffisant ; le CQ (connaissance incomplete) attaque la suffisance de la premisse-signe — undermine. Precedent Sign_Inference→undermine (pk357 Conditionnement). +3,Insufficiency,Argument vide,Direct=OppositeConsequences_Conflict | Exception=PopularOpinion_Inference | skos:broadMatch,PopularOpinion_Inference (exception scheme) + OppositeConsequences_Conflict (CQ),undermine,premise (I),undermine,I-node,177,MED,"L'argument vide s'appuie sur une premisse d'adhesion populaire sans contenu propre — undermine (premisse). Vote precedent depuis PopularOpinion_Inference (pk177). Flag: le CQ primaire OppositeConsequences_Conflict n'a pas de precedent direct ; retenu undermine par la semantique du sophisme (vacuite de la premisse), pas rebut." +4,Insufficiency,Appel à l'ignorance,Direct=Ignorance_Inference | Exception=- | skos:broadMatch,Ignorance_Inference (the argument-from-ignorance inference rule),undercut,inference (RA),undercut,RA-node,750,HIGH,"L'appel a l'ignorance : de « non prouve faux » on infere « vrai » — la regle d'inference elle-meme est invalide, pas une premisse — undercut. Precedent Ignorance_Inference→undercut (pk750 Erreur de modalite), meme token + semantique concordante." +799,Misleading language,Définition biaisée,"Direct=BiasedClassification_Conflict, ArbitraryVerbalClassification_Inference | Exception=VerbalClassification_Inference | skos:broadMatch",VerbalClassification_Inference (exception) + BiasedClassification/ArbitraryVerbalClassification CQ,undermine,premise (I),undermine,I-node,177,HIGH,La definition biaisee attaque la premisse de classification (la categorie est construite pour biaiser) — undermine. Precedent ArbitraryVerbalClassification_Inference→undermine (pk177). +846,Misleading language,Ambiguïté,"Direct=ArbitraryVerbalClassification_Inference, OppositeConsequences_Conflict, SignFromOtherEvents_Conflict | Exception=- | skos:broadMatch",ArbitraryVerbalClassification_Inference + OppositeConsequences/SignFromOtherEvents CQ,undermine,premise (I),undermine,I-node,177+357+1371,HIGH,"L'ambiguite exploite une classification arbitraire ; deux tokens a precedent (ArbitraryVerbalClassification pk177, SignFromOtherEvents pk357/1371) votent undermine. Convergence." +621,Mathematical error,Transfert illicite,Direct=PropertyNotExistant_Conflict | Exception=- | skos:broadMatch,PropertyNotExistant_Conflict (CQ that the inferred property does not transfer),undercut,inference (RA),undercut,RA-node,804,HIGH,"Le transfert illicite applique une propriete la ou elle ne se transfere pas : la regle d'inference ne s'applique pas — undercut. Precedent PropertyNotExistant_Conflict→undercut (pk804 Acception arbitraire), meme token." +1281,Obstruction,Refus du débat,Direct=- | Exception=Dialogue_Scheme | skos:broadMatch,Dialogue_Scheme (the rebutted dialogue-norm scheme),rebut,conclusion (CA),rebut,CA-node,1313,HIGH,"Le refus du debat oppose une contre-position qui bloque l'echange rationnel plutot que d'attaquer une premisse ou l'inference — rebut. Precedent Dialogue_Scheme→rebut (pk1313 Evasion), meme token, meme famille Obstruction (nucleus rebut)." diff --git a/docs/taxonomy/498-reconciliation-p1.md b/docs/taxonomy/498-reconciliation-p1.md new file mode 100644 index 00000000..6838cc84 --- /dev/null +++ b/docs/taxonomy/498-reconciliation-p1.md @@ -0,0 +1,140 @@ +# #498 — AIF two-layer reconciliation, P1 (skos-only → attack columns) + +**Worker** po-2024 · **Date** 2026-07-10 · **Base** master `e748735b` · **Statut** proposition **gated** (0 write prod CSV dans ce PR) · **Track** GO ai-01 `msg-20260710T180845-5i1v03` (réconciliation P1), couvert par le pilote GO #498 (jsboige 2026-06-17). + +> Scope de ce PR : **docs + apply-script (dry-run)**. Aucune cellule du CSV de production n'est modifiée dans le diff. La sérialisation prod suit le flow #753/#760 (gate ai-01), après revue de cette proposition. + +--- + +## 0. TL;DR + +`498-coverage-status.md` (#768) a établi que le mapping AIF a **deux couches jamais réconciliées** : la couche **attack** (`AIF_attackType`+`AIF_attackedNode`, 93 lignes) et la couche **skos** (`AIF_skosDirectRef`/`ExceptionRef`/`MappingType`, 70 lignes), avec **52 lignes skos-only** (skos vetté, colonnes attack vides). P1 = back-fill de la couche attack pour ces 52. + +Ce PR livre : + +1. **Une correction load-bearing** au cadrage #768 : il **n'existe pas** de sous-ensemble « inherit mécanique 0-risque » parmi les 52. La classification initiale « 19 inherit du sous-sous anchor » est un **artefact** — la vérification montre **0/19** dont la signature skos correspond à celle de son anchor (les anchors du même sous-sous sont eux-mêmes **skos-vides** : ils appartiennent à la couche attack-only, modélisée par #753/#760 sur des arguments **différents**). Hériter leur `attackType` serait une **fabrication** (§1). +2. **La méthode de dérivation** rigoureuse : l'`attackType` de chaque ligne skos-only se dérive de **sa propre** signature skos, ancrée sur les **18 lignes fully-modeled** (les seules qui portent skos **et** attack — ground truth « cette signature → ce type »). `attackedNode` suit déterministiquement (#707§4 (a)) (§2). +3. **Tranche 1 — 14 lignes PRECEDENT** (token exact d'un précédent fully-modeled + contrôle sémantique) : proposition ready-to-serialize, distribution **11 undermine / 2 undercut / 1 rebut** (§3–§4). +4. **Le reste (38) scopé** : 2 PREC-TIE (votes de tokens divergents) + 36 SUFFIX-ONLY (aucun précédent de token ; le « prior de suffixe » par défaut vers *undermine* est **démontrablement non fiable**) → modélisation Walton **au cas par cas**, pas d'auto-dérivation (§5). + +Compléter la tranche 1 porte le fully-modeled **18 → 32** lignes (1.3% → 2.3%). Le reste demande un vrai travail de modélisation, tranché en sous-lots. + +--- + +## 1. Correction : pas d'« inherit » mécanique (0/52) + +Le premier passage classait les 52 en « 19 inherit du sous-sous anchor » (le sous-sous porte une ligne attack-typée → hériter son type) vs « 33 à modéliser ». **Contrôle de rigueur** (`498-p1-inherit-verify.py`, code=truth) : pour chacune des 19, la signature skos de la feuille est-elle alignée sur celle de son anchor ? + +**Résultat : 0/19 alignées (19 DIVERGE).** Les anchors ont, dans la quasi-totalité des cas, une signature skos **vide** : ce sont des lignes de la couche **attack-only** (#753/#760), qui portent un `attackType` mais **pas** de skos, et modélisent des arguments **distincts** dans le même sous-sous. Exemples : + +| Feuille skos-only | son skos | anchor même sous-sous | skos de l'anchor | +|---|---|---|---| +| pk677 « Pente glissante » | `WeakestLink_Conflict` + SlipperySlope schemes | pk667 (undermine) | **vide** | +| pk705 « Pente glissante » | `RequiredSteps_Conflict` + SlipperySlope schemes | pk698 (undermine) | **vide** | +| pk337 « Appel à la terreur » | `IrrationalFearAppeal_Conflict` + `FearAppeal_Inference` | pk322 (undermine) | `NegativeConsequences_Inference` (**autre** scheme) | + +Hériter le type de l'anchor propagerait un type **non fondé sur la modélisation propre de la feuille** — et, pour pk677/705, probablement **faux** : leurs CQ (`WeakestLink`/`RequiredSteps`) contestent la **tenue de la chaîne causale** → penchent **undercut**, alors que l'anchor est typé undermine. **Conclusion : l'`attackType` est un jugement neuf** (les tokens existent = 0 fabrication de token ; mais le type d'attaque n'est **pas** dans le skos). Ceci raffine le « 0 fabrication risk » de #768 : 0-risque **token**, pas 0-risque **modélisation**. + +--- + +## 2. Méthode de dérivation (ancrée sur les 18 fully-modeled) + +Les **18 lignes fully-modeled** (skos **et** attack) sont le seul ground truth « signature skos → attackType assigné par le modélisateur ». On construit depuis elles une carte **token → attackType** (`498-p1-precedent.py`), puis pour chaque ligne skos-only on vote par ses tokens : + +- **PRECEDENT** — au moins un token de la ligne a un précédent, vote **unique et cohérent**. `attackType` = ce vote. Contrôle sémantique par ligne (§4). **14 lignes.** +- **PREC-TIE** — des tokens votent pour des types **différents** → jugement requis. **2 lignes** (777, 633). +- **SUFFIX-ONLY** — aucun token de la ligne n'a de précédent ; seul le **suffixe** (`_Conflict`/`_Inference`/`_Scheme`) donne un signal. Or la carte de suffixe est `_Conflict{undermine:5,undercut:1}`, `_Inference{undercut:6,undermine:9,rebut:2}`, `_Scheme{undermine:1,rebut:1}` : elle **défaut tout vers undermine** (pluralité), ce qui est un **prior**, pas une dérivation. **36 lignes.** + +`attackedNode` est **déterministe** (#707§4 Option a, ratifié) — la sérialisation prod le confirme exactement (90/93 lignes) : + +| attackType | attackedNode | composant attaqué | +|---|---|---| +| undercut | `RA-node` | inférence (la règle ne s'applique pas) | +| undermine | `I-node` | prémisse (acceptabilité contestée) | +| rebut | `CA-node` | conclusion (contre-conclusion opposée) | + +--- + +## 3. Tranche 1 — 14 lignes PRECEDENT (proposition) + +Machine-readable : [`498-reconciliation-p1-annotations.csv`](498-reconciliation-p1-annotations.csv) (12 colonnes, BOM+CRLF). Distribution **11 undermine / 2 undercut / 1 rebut**. + +| pk | famille | sophisme | skos (existant) | → type | node | précédent | conf. | +|---:|---|---|---|---|---|---|---| +| 1198 | Cheating | Essentialisme | `Preference_Scheme` | undermine | I | 953 | HIGH | +| 1083 | Cheating | Apophénie | `Sign_Inference` | undermine | I | 357 | HIGH | +| 1090 | Cheating | Biais émotionnels | `OppositeConsequences_Conflict`+`Preference_Scheme` | undermine | I | 953 | MED | +| 1092 | Cheating | Biais de négativité | `NegativeConsequences_Inference` | undermine | I | 322 | HIGH | +| 1104 | Cheating | Biais d'autocomplaisance | `PositiveConsequences_Inference` | undermine | I | 300 | HIGH | +| 1 | Insufficiency | Insuffisance | `GeneralAcceptanceDoubt_Conflict` | undermine | I | 953+177 | HIGH | +| 3 | Insufficiency | Argument vide | `OppositeConsequences_Conflict`+`PopularOpinion_Inference` | undermine | I | 177 | MED | +| 70 | Insufficiency | Préjugé | `Bias_Inference`+6 schemes | undermine | I | 177+340+953 | HIGH | +| 133 | Insufficiency | Surinterprétation | `LackOfCompleteKnowledge_Conflict`+`Sign_Inference` | undermine | I | 357 | HIGH | +| 4 | Insufficiency | Appel à l'ignorance | `Ignorance_Inference` | undercut | RA | 750 | HIGH | +| 799 | Misleading language | Définition biaisée | `ArbitraryVerbalClassification_Inference`+… | undermine | I | 177 | HIGH | +| 846 | Misleading language | Ambiguïté | `ArbitraryVerbalClassification`+`SignFromOtherEvents` | undermine | I | 177+357+1371 | HIGH | +| 621 | Mathematical error | Transfert illicite | `PropertyNotExistant_Conflict` | undercut | RA | 804 | HIGH | +| 1281 | Obstruction | Refus du débat | `Dialogue_Scheme` | rebut | CA | 1313 | HIGH | + +Tous les précédents (953, 177, 357, 322, 300, 750, 804, 340, 1371, 1313) sont dans le set des 18 fully-modeled. **2 lignes MED** (3, 1090) : le vote vient d'un token de scheme **secondaire** (le CQ primaire `OppositeConsequences_Conflict` n'a pas de précédent direct) mais `undermine` reste cohérent avec la sémantique du sophisme — flaggé, à confirmer par ai-01. + +--- + +## 4. Justification par ligne + +Détail complet en colonne `justification` du CSV. Synthèse : + +- **Undermine — biais/prémisse déformée** (1092 négativité←`NegativeConsequences` pk322 ; 1104 autocomplaisance←`PositiveConsequences` pk300 ; 1083 apophénie←`Sign` pk357 ; 1198 essentialisme←`Preference` pk953) : mêmes tokens que leurs précédents, la prémisse (conséquences/signe/préférence/essence) est déformée → I-node. +- **Undermine — insuffisance de prémisse** (1 insuffisance←`GeneralAcceptanceDoubt` ; 70 préjugé←6 schemes présomptifs ; 3 argument vide←`PopularOpinion` ; 133 surinterprétation←`Sign`+CQ connaissance incomplète) : le défaiteur conteste l'**acceptabilité/suffisance** d'une prémisse. +- **Undermine — classification biaisée** (799 définition biaisée, 846 ambiguïté ← `ArbitraryVerbalClassification` pk177) : la prémisse de catégorisation est construite pour biaiser. +- **Undercut — la règle d'inférence ne tient pas** (4 appel à l'ignorance ← `Ignorance_Inference` pk750 : « non-prouvé-faux ⇒ vrai » est une inférence invalide ; 621 transfert illicite ← `PropertyNotExistant_Conflict` pk804 : la propriété ne se transfère pas) → RA-node. +- **Rebut — contre-conclusion qui bloque l'échange** (1281 refus du débat ← `Dialogue_Scheme` pk1313 Évasion, même famille Obstruction, nucleus rebut) → CA-node. + +--- + +## 5. Reste (38) — modélisation au cas par cas (P1 tranche 2+) + +**Ne PAS auto-sérialiser.** Le prior de suffixe est non fiable (contre-exemple : pk705/677 « Pente glissante » ont des CQ qui contestent la chaîne → penchent **undercut**, pas le `~undermine` du prior). + +**PREC-TIE (2) — arbitrage de token requis :** +- pk777 « Inconsistance » : `OpposedCommitment_Conflict`→undermine vs `InconsistentCommitment_Inference`→rebut (précédent pk1361 Procès en incohérence = rebut). Nœud parent → probablement **rebut**, à trancher. +- pk633 « Relation infondée » : `PropertyNotExistant_Conflict`→undercut vs `Sign_Inference`→undermine. À trancher. + +**SUFFIX-ONLY (36) — modéliser depuis la sémantique du sophisme + son skos :** +- Cheating (8) : 1023, 888, 973, 1175, 1066, 1087, 1148, 1020 +- Insufficiency (5) : 2, 71, 33, 34, 43 +- Misleading language (7) : 833, 808, 814, 800, 876, 856, 839 +- Faulty logics (7) : 696, 697, 726, 758, 759, 719, 705 +- Mathematical error (4) : 595, 632, 677, 614 +- Influence (3) : 356, 432, 337 +- Obstruction (2) : 1280, 1360 + +Chacune demande la lecture de `desc_fr` + l'analyse « que défait le CQ » (prémisse → undermine, inférence → undercut, conclusion → rebut). Sous-lots par famille aux prochains ticks, chacun une proposition gated comme celle-ci. + +--- + +## 6. Sérialisation (flow #753/#760) + +`tools/498-p1-apply.py` — **gated, dry-run par défaut**, mirroir de `tools/498-phase13-apply.py` (#757) : +- lit `498-reconciliation-p1-annotations.csv` et **re-vérifie** que sa carte interne concorde 14/14 (assertion load-bearing) ; +- splitters byte-exact (guillemets doublés + LF encadrés), cell-fill des seules colonnes `AIF_attackType`/`AIF_attackedNode` des 14 PK ; +- preuve de **byte-preservation** (0 mismatch hors les 2 cellules × 14 lignes), well-formedness 104 cols, BOM+CRLF préservés ; +- `--write` **gaté** (ai-01), backup `tmp/` avant écriture pour vérif indépendante. + +``` +python tools/498-p1-apply.py # dry-run (0 write prod) — ce PR +python tools/498-p1-apply.py --write # APPLY 14 cellules (GATÉ — relais ai-01) +``` + +--- + +## 7. Bornes du gate + +- ✅ **0 write prod CSV** dans ce PR (docs + apply-script dry-run uniquement). +- ✅ **0 fabrication token #677** — aucun token AIF créé ; on **type** des lignes qui portent déjà un skos vetté. +- ✅ Code=truth : tous les chiffres/tokens/labels lus du CSV master `e748735b`. Précédents = les 18 fully-modeled. +- ✅ Discipline rigueur : correction du cadrage « inherit » avant toute sérialisation ; tranche limitée aux 14 défendables ; 38 restantes scopées, pas devinées. +- ❌ #674/#666/#596 non touchés. Pas de self-merge — verdict QA ai-01. +- ⏸️ Sérialisation prod = étape suivante gated (relais ai-01), pas dans ce PR. + +🤖 Worker po-2024 — réconciliation P1 tranche 1 (GO ai-01 `msg-20260710T180845-5i1v03`). diff --git a/tools/498-p1-apply.py b/tools/498-p1-apply.py new file mode 100644 index 00000000..f036a8cf --- /dev/null +++ b/tools/498-p1-apply.py @@ -0,0 +1,176 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""#498 P1 reconciliation tranche-1 APPLY — back-fill the 2 AIF attack columns +for the 14 PRECEDENT-derived skos-only rows (GATED — dry-run by default). + +Companion to `docs/taxonomy/498-reconciliation-p1.md` (proposition) and its +machine-readable `docs/taxonomy/498-reconciliation-p1-annotations.csv`. Mirrors +`tools/498-phase13-apply.py` (#757): the 2 columns already exist post-#753, so +this is byte-exact CELL FILL, not column insertion. + +These 14 rows are *skos-only* (they already carry vetted native skos tokens; only +the attack columns are empty). Their attackType is derived from their OWN skos +signature, anchored on the 18 fully-modeled rows (ground truth). NODE is the +deterministic ASPIC+ map (ratified #707§4 Option a): undercut→RA-node, +undermine→I-node, rebut→CA-node. No token is fabricated (#677): we TYPE rows that +already have a vetted skos. + +GATE (per ai-01 GO `msg-20260710T180845-5i1v03`, covered by pilote GO #498 +jsboige 2026-06-17): the proposition is reviewed by ai-01 before prod write. +`--write` is GATED until ai-01 relays the go. Dry-run (default) proves +byte-preservation without touching prod. + + python tools/498-p1-apply.py # dry-run, 0 prod write (this PR) + python tools/498-p1-apply.py --write # APPLY 14 cells (GATED — ai-01 relay) +""" +import csv, io, sys, glob +from collections import Counter + +PATH = "Cards/Fallacies/Argumentum Fallacies - Taxonomy.csv" +ANNOT = "docs/taxonomy/498-reconciliation-p1-annotations.csv" +BACKUP = "tmp/Fallacies-backup-pre-p1.csv" # saved before --write (independent verify) +NODE = {"undercut": "RA-node", "undermine": "I-node", "rebut": "CA-node"} +WRITE = "--write" in sys.argv + +# ── 14-row tranche-1 map (attackType). Load-bearing: re-verified below to match +# the annotation CSV 14/14 (the CSV is the human-reviewed source of truth). ── +P1_MAP = { + # Cheating (5) + "1198": "undermine", "1083": "undermine", "1090": "undermine", + "1092": "undermine", "1104": "undermine", + # Insufficiency (5) + "1": "undermine", "3": "undermine", "70": "undermine", "133": "undermine", + "4": "undercut", + # Misleading language (2) + "799": "undermine", "846": "undermine", + # Mathematical error (1) + "621": "undercut", + # Obstruction (1) + "1281": "rebut", +} +assert len(P1_MAP) == 14 +assert Counter(P1_MAP.values()) == {"undermine": 11, "undercut": 2, "rebut": 1} + +# ── byte-exact splitters (CSV-aware: doubled quotes + embedded LF) ───────────── +def split_logical_rows(text): + rows, cur, in_q = [], [], False + i, n = 0, len(text) + while i < n: + ch = text[i] + if ch == '"': + if in_q and i+1 < n and text[i+1] == '"': + cur.append('""'); i += 2 + else: + in_q = not in_q; cur.append(ch); i += 1 + elif ch == '\r' and not in_q and i+1 < n and text[i+1] == '\n': + rows.append(''.join(cur)); cur = []; i += 2 + else: + cur.append(ch); i += 1 + if cur: rows.append(''.join(cur)) + return rows + +def split_fields(row): + segs, cur, in_q = [], [], False + i, n = 0, len(row) + while i < n: + ch = row[i] + if ch == '"': + if in_q and i+1 < n and row[i+1] == '"': + cur.append('""'); i += 2 + else: + in_q = not in_q; cur.append(ch); i += 1 + elif ch == ',' and not in_q: + segs.append(''.join(cur)); cur = []; i += 1 + else: + cur.append(ch); i += 1 + segs.append(''.join(cur)) + return segs + +# ── load-bearing: re-verify P1_MAP vs the annotation CSV (human-reviewed source) ─ +annot_map = {} +with open(ANNOT, encoding="utf-8-sig") as fh: + for row in csv.DictReader(fh): + annot_map[row["fallacy_pk"].strip()] = row["AIF_attackType"].strip() + # node consistency in the annotation itself + assert row["AIF_attackedNode"].strip() == NODE[row["AIF_attackType"].strip()], \ + f"annotation node/type inconsistent at pk {row['fallacy_pk']}" +assert set(annot_map) == set(P1_MAP), ( + f"P1_MAP vs annotation-CSV PK mismatch\n" + f" only CSV: {set(annot_map)-set(P1_MAP)}\n only MAP: {set(P1_MAP)-set(annot_map)}") +assert all(annot_map[pk] == P1_MAP[pk] for pk in P1_MAP), "attack_type mismatch vs annotation CSV" + +# ── read current CSV ─────────────────────────────────────────────────────────── +raw = open(PATH, "rb").read() +bom = raw[:3] == b'\xef\xbb\xbf' +text = (raw[3:] if bom else raw).decode('utf-8') +ended_crlf = text.endswith('\r\n') +rows = split_logical_rows(text) +header = split_fields(rows[0]) +NCOL = len(header) +ATI = header.index('AIF_attackType') +ANI = header.index('AIF_attackedNode') +PKI = 0 # uppercase 'PK' +assert header[ATI-1] == 'AIF_skosMappingType', "AIF col block moved?" + +# ── pre-state: every target PK must be empty (fill, not overwrite) + carry skos ─ +DIR = header.index('AIF_skosDirectRef'); EXC = header.index('AIF_skosExceptionRef') +OTH = header.index('AIF_skosOther') +pre = {} +for r in rows[1:]: + s = split_fields(r); pk = s[PKI].strip() + if pk in P1_MAP: + pre[pk] = (s[ATI].strip(), s[ANI].strip(), + bool(s[DIR].strip() or s[EXC].strip() or s[OTH].strip())) +assert set(pre) == set(P1_MAP), f"target PKs missing in CSV: {set(P1_MAP)-set(pre)}" +not_empty = {pk for pk,(at,an,_) in pre.items() if at or an} +assert not not_empty, f"ABORT: target PKs not empty (would overwrite): {not_empty}" +no_skos = {pk for pk,(_,_,hs) in pre.items() if not hs} +assert not no_skos, f"ABORT: target PKs lack skos (not skos-only back-fill): {no_skos}" + +# ── apply: cell-fill byte-exact (only ATI/ANI of the 14 PK change) ──────────── +new_rows = [rows[0]] +filled = Counter() +for rtext in rows[1:]: + s = split_fields(rtext); pk = s[PKI].strip() + if pk in P1_MAP: + at = P1_MAP[pk]; s[ATI] = at; s[ANI] = NODE[at]; filled[at] += 1 + new_rows.append(",".join(s)) +new_text = "\r\n".join(new_rows) + ("\r\n" if ended_crlf else "") + +# ── byte-preservation proof (only ATI/ANI of the 14 PK may differ) ──────────── +new_rows2 = split_logical_rows(new_text) +mismatches = 0 +for i in range(len(rows)): + o = split_fields(rows[i]); n = split_fields(new_rows2[i]) + assert len(o) == len(n) == NCOL, f"row {i} col count drift" + for j in range(NCOL): + if o[j] != n[j] and not (i > 0 and j in (ATI, ANI) and o[PKI].strip() in P1_MAP): + mismatches += 1 + if mismatches <= 3: + print(f" MISMATCH row {i} pk {o[PKI].strip()!r} col {j}: {o[j]!r} -> {n[j]!r}") + +# re-parse well-formedness +chk = list(csv.reader(io.StringIO(new_text))) +assert len(chk) == len(rows) and all(len(r) == NCOL for r in chk), "well-formedness" + +# ── report ───────────────────────────────────────────────────────────────────── +total_now = sum(1 for r in rows[1:] if split_fields(r)[ATI].strip()) +print("="*72) +print(f"#498 P1 RECONCILIATION tranche-1 APPLY (write={WRITE})") +print("="*72) +print(f"annotation CSV re-verified 14/14 vs P1_MAP: OK") +print(f"pre-state: all 14 target PKs empty + carry skos (skos-only back-fill): OK") +print(f"apply_set: 14 PKs -> distribution: {dict(filled)}") +print(f"byte-preservation mismatches: {mismatches} (must be 0)") +print(f"well-formedness: {len(chk)} rows x {NCOL} cols, CRLF({ended_crlf})+BOM({bom}) preserved") +print(f"delta if written: {len(new_text)-len(text)} bytes") +print(f"CSV attack-typed total: {total_now} -> {total_now + 14}") +if WRITE: + import os + os.makedirs("tmp", exist_ok=True) + open(BACKUP, "wb").write(raw) + open(PATH, "wb").write((b'\xef\xbb\xbf' if bom else b'') + new_text.encode('utf-8')) + print(f">>> WRITTEN (14 cells filled). Backup saved to {BACKUP} for independent verify.") + print(f" GATE lifted by ai-01 relay. Two-layer fully-modeled: 18 -> 32.") +else: + print(">>> DRY-RUN (pass --write to APPLY; GATED until ai-01 relays go).")