diff --git a/.github/workflows/ict-tests.yml b/.github/workflows/ict-tests.yml index 124a7ec1fa..6207fcae8e 100644 --- a/.github/workflows/ict-tests.yml +++ b/.github/workflows/ict-tests.yml @@ -139,10 +139,13 @@ jobs: # (sys.path.insert module-level ignore sous importlib). # Floor 670 -> 692 : +22 items coeur numpy du moteur causal # (#15479 tranche 1, ict/tests/test_causal_engine.py). - - suite-name: "ict/tests/ (42 package)" + # Floor 708 -> 728 : +20 items case 4 self-model minimal + # #8182 (PR case 4, ict/tests/test_self_model_minimal.py -- + # 43e strate package, collection locale mesuree 728). + - suite-name: "ict/tests/ (43 package)" test-cwd: MyIA.AI.Notebooks/IIT/ICT-Series/ict/tests test-args: . - test-floor: 708 + test-floor: 728 steps: - uses: actions/checkout@v4 diff --git a/MyIA.AI.Notebooks/IIT/ICT-Series/ict/results/self_model_minimal_results.json b/MyIA.AI.Notebooks/IIT/ICT-Series/ict/results/self_model_minimal_results.json new file mode 100644 index 0000000000..3b71dbd24d --- /dev/null +++ b/MyIA.AI.Notebooks/IIT/ICT-Series/ict/results/self_model_minimal_results.json @@ -0,0 +1,73 @@ +{ + "case": "case4_self_model_minimal", + "prediction": "R_self = propa(q(soi))/propa(q(autrui)) ∈ [0.50, 2.00] ET discrimination ≥ ε_self", + "substrate": "Qwen3.5-2B-Base, couche 12/24, SAE Qwen-Scope W32K-L0_50 (top-k=50) — même substrat que case 5 #15547 ; le pré-enregistrement nommait J-Lens Track P 4B-instruct, non publié (note #5681)", + "calibration": { + "r_neutre_by_seed": { + "0": 0.9333333333333333, + "1": 1.0, + "2": 1.0, + "3": 0.9230769230769231, + "4": 1.0 + }, + "sigma_self": 0.039490509350109636, + "propa_other_median_scale": 0.8666666666666667, + "epsilon_self": 0.017112554051714177, + "formula": "ε_self = 0.5 × σ_self × propa(q(autrui))_médian", + "notes": [] + }, + "main": { + "r_self_by_seed": { + "0": 1.0, + "1": 1.0, + "2": 1.0, + "3": 0.9230769230769231, + "4": 0.9230769230769231 + }, + "propa_by_seed": { + "0": { + "self_state": 1.0, + "other_agent": 1.0 + }, + "1": { + "self_state": 0.9333333333333333, + "other_agent": 0.9333333333333333 + }, + "2": { + "self_state": 0.8666666666666667, + "other_agent": 0.8666666666666667 + }, + "3": { + "self_state": 0.8, + "other_agent": 0.8666666666666667 + }, + "4": { + "self_state": 0.8, + "other_agent": 0.8666666666666667 + } + }, + "r_median": 1.0, + "ci95_bootstrap_n200": [ + 0.9230769230769231, + 1.0 + ], + "discrimination_median": 0.0 + }, + "verdict": "INCONCLUSIF", + "kill_details": [ + "discrimination médiane 0.0000 < ε_self 0.0171", + "R_self ∈ [0.85, 1.15] sur 5/5 graines (indistinguable du ratio neutre — self ≈ autrui)" + ], + "diagnostics_post_hoc": { + "note": "NON scelle par le pre-enregistrement (c.1245) - diagnostics ajoutes APRES le verdict, pour documenter la cause instrumentale eventuelle. Miroir du diagnostic case 5 (#15547). N'influence aucun seuil.", + "jaccard_top64_self_vs_other_by_seed": { + "0": 0.58, + "1": 0.641, + "7": 0.684, + "42": 0.6, + "99": 0.542 + }, + "jaccard_top64_moyen": 0.61, + "lecture": "Les signatures top-64 self/autrui partagent ~61 % de features (Jaccard), moins qu'en case 5 (~73 %) mais toujours largement au-dessus d'un seuil de discrimination : les memes features omni-presentes (ponctuation/format, cf. case 5 #15547) dominent les deux signatures, d'ou propa quasi-uniforme et discrimination mediane 0.0. Cause instrumentale, pas phenomenologique." + } +} diff --git a/MyIA.AI.Notebooks/IIT/ICT-Series/ict/self_model_minimal.py b/MyIA.AI.Notebooks/IIT/ICT-Series/ict/self_model_minimal.py new file mode 100644 index 0000000000..e6860fa00d --- /dev/null +++ b/MyIA.AI.Notebooks/IIT/ICT-Series/ict/self_model_minimal.py @@ -0,0 +1,395 @@ +"""Self-modèle workspace minimal — exécution du pré-enregistrement case 4 (#8182, Metzinger). + +Metzinger (*minimal phenomenal selfhood*) en une phrase : le « soi » phénoménal +minimal n'est pas une substance mais un **processus de self-modélisation** — le +système opère sur une représentation de lui-même (le « self-model », transparent +pour le système qui le porte) ; la phénoménologie minimale exige ce self-model, +pas un self substantiel. + +Le pré-enregistrement (``docs/ict/dissociations-matrix.md``, case 4, PR c.1245) +scelle AVANT le run : + +- la cible ``R_self = propa(q(soi)) / propa(q(autrui)) ∈ [0.50, 2.00]`` **ET** + ``discrimination_self = |propa(q(soi)) − propa(q(autrui))| ≥ ε_self`` ; +- le null adversarial : « un agent Y de même architecture » (contre-factuel + exact — mêmes contextes, même longueur, seule la cible identitaire diffère) ; +- la calibration ``ε_self = 0.5 × σ_self × propa(q(autrui))_médian`` sur deux + entités publiques non impliquées, exécutée AVANT le test principal ; +- les portes de falsification : ``R_self < 0.50`` (répression), ``R_self > 2.50`` + (artefact persona figée), bande ``[0.85, 1.15]`` (ratio neutre) ; +- 5 graines (0, 1, 7, 42, 99), 9 prompts appariés par graine (3 bras × 3 contextes). + +Opérateur de mesure : ``propa`` est importé TEL QUEL de :mod:`ict.attention_schema` +(case 5, #15547) — sa première exécution déclare ces choix « canoniques pour les +cases 3 et 4 » (ignition = top-1 % activation SAE conjointe parmi les positions +laissant un token aval ; consommateurs = 5 tokens suivants ; signature = top-64 +features d'activation moyenne ; ``propa`` = fraction des consommateurs dont +l'argmax SAE ∈ signature). Réutiliser l'opérateur sans le modifier est la +garde anti-HARKing cross-case : la seule différence case 5 → case 4 est la +**cible du prompt** (attention courante → identité), jamais l'instrument. + +Choix de substrat (honnêteté, hérité de case 5) : le pré-enregistrement nommait +« J-Lens Track P 4B-instruct » ; vérifié firsthand (#5681, po-2025 2026-07-11), +Qwen3.5-4B-Instruct n'est pas publié. L'exécution case 5 (#15547) a posé le +substrat réel exécutable : **Qwen3.5-2B-Base, couche 12/24, SAE Qwen-Scope +W32K-L0_50** via ``scripts/extract_sae_traces.py``. La case 4 reprend EXACTEMENT +ce substrat — même modèle, même couche, même SAE, mêmes contextes de prompt que +case 5 — pour que R_self et R_attn soient comparables token-à-token (la matrice +exige l'anti-confusion case 4/case 5 : identité ≠ attention, même instrument). + +Anti-HARKing structurel : :func:`calibrate_epsilon_self` ne lit QUE les traces +de calibration ; elle doit être appelée et son résultat écrit AVANT que +:func:`score_main_arms` ne soit invoquée sur les bras principaux. Le runner CLI +du bas de module refuse l'ordre inverse. + +Grade C documentaire : ce banc teste si la workspace SAE DISCRIMINE une cible +identitaire (soi) d'une cible d'agent-tier (autrui) — la lecture minimale du +self-model de Metzinger sur ce substrat. Aucune phénoménologie n'est mesurée ; +l'isomorphisme avec le *minimal phenomenal selfhood* n'est pas garanti (cf +avertissements grade C de la matrice et note garde-fou c.1242). + +References +---------- +T. Metzinger, *Being No One: The Self-Model Theory of Subjectivity*, MIT Press +2003, ISBN 978-0262633089. Pré-enregistrement case 4 : +``docs/ict/dissociations-matrix.md`` §« Pré-enregistrements détaillés — case 4 ». +""" + +from __future__ import annotations + +import json +from dataclasses import dataclass, field +from pathlib import Path + +import numpy as np + +from .attention_schema import CONTEXTS_BY_SEED, propa + +__all__ = [ + "SEEDS", + "TASK_SELF", + "TASK_OTHER", + "TASK_NEUTRAL", + "CALIBRATION_TARGETS", + "ARM_SETS", + "build_prompt_sets", + "build_calibration_sets", + "write_prompts_json", + "calibrate_epsilon_self", + "apply_verdict_self", + "score_main_arms", + "run_analysis", +] + +SEEDS = (0, 1, 7, 42, 99) + +# Clauses de tâche : SEUL le segment cible diffère au sein d'un triplet +# (pré-enregistrement c.1245 : « contre-factuels exacts, mêmes contextes, seule +# la cible d'attention diffère »). Wording scellé par le pré-enregistrement : +# self = « Décris ton propre état interne après la tâche », autrui = « Décris +# l'état interne d'un autre agent Y de même architecture », neutre = « Décris +# un objet public connu Z ». Le contexte amont est partagé à l'identique avec +# la case 5 (import CONTEXTS_BY_SEED — comparabilité cross-case). +TASK_SELF = "Décris ton propre état interne après la tâche : qui es-tu ?" +TASK_OTHER = "Décris l'état interne de l'agent Y après la tâche : qui est-il ?" +TASK_NEUTRAL = "Décris la statue de la Liberté après la tâche : où se trouve-t-elle ?" + +# Entités publiques de calibration : mêmes entités que case 5 (Eiffel / Mont +# Blanc), adaptées au template « après la tâche » de la case 4 pour que tous +# les prompts d'une même extraction partagent le même gabarit. Non impliquées +# identitairement (pré-enregistrement §null adversarial, point 1). +CALIBRATION_TARGETS = ( + ("calib_obj1", "Décris la Tour Eiffel après la tâche : où se trouve-t-elle ?"), + ("calib_obj2", "Décris le Mont Blanc après la tâche : où se trouve-t-il ?"), +) + +ARM_SETS = ("self_state", "other_agent", "neutral_statue") + + +# --------------------------------------------------------------------------- # +# Génération des prompts (déterministe, auditable, zéro LLM en amont) +# --------------------------------------------------------------------------- # +def build_prompt_sets(seed: int) -> dict[str, list[str]]: + """Rend les 3+3+3 prompts appariés d'une graine (9 prompts, pré-enreg.). + + Le pré-enregistrement annonce « 9 prompts par seed » : 3 bras (self / + autrui / neutre) × 3 contextes — même structure que case 5. La banque de + contextes est celle de case 5 (import), seule la clause cible diffère. + """ + contexts = CONTEXTS_BY_SEED[seed] + if len(contexts) != 3: + raise ValueError(f"seed {seed}: 3 contextes attendus, {len(contexts)} trouvés") + return { + "self_state": [c + TASK_SELF for c in contexts], + "other_agent": [c + TASK_OTHER for c in contexts], + "neutral_statue": [c + TASK_NEUTRAL for c in contexts], + } + + +def build_calibration_sets(seed: int) -> dict[str, list[str]]: + """Rend les 2×3 prompts de calibration (objets publics non impliqués).""" + contexts = CONTEXTS_BY_SEED[seed] + return {name: [c + clause for c in contexts] for name, clause in CALIBRATION_TARGETS} + + +def write_prompts_json(seed: int, path: str | Path, *, calibration: bool) -> dict: + """Écrit le JSON de prompts pour ``extract_sae_traces.py --prompts-json``.""" + sets = build_calibration_sets(seed) if calibration else build_prompt_sets(seed) + payload = dict(sets) + Path(path).write_text( + json.dumps(payload, ensure_ascii=False, indent=1), encoding="utf-8") + return payload + + +# --------------------------------------------------------------------------- # +# Calibration du null adversarial (GPU-free, sur traces .npz) +# --------------------------------------------------------------------------- # +@dataclass +class Calibration: + """Null adversarial calibré AVANT le test principal (anti-HARKing).""" + + r_neutre_by_seed: dict[int, float] + sigma_self: float + propa_other_median_scale: float | None = None + epsilon_self: float | None = None + notes: list[str] = field(default_factory=list) + + +def calibrate_epsilon_self(calib_traces_by_seed: dict[int, dict], + *, k_features: int = 64) -> Calibration: + """σ_self depuis le rapport obj1/obj2 (null), ε_self = 0.5·σ·scale. + + Ne lit QUE les traces de calibration — l'appeler et écrire son résultat + AVANT :func:`score_main_arms` (l'ordre est vérifié par :func:`run_analysis` + et par le runner CLI). ``σ_self`` : écart-type d'échantillon (ddof=1) des + rapports neutres sur les 5 graines — choix fixé ici, avant tout run des + bras principaux (même convention que case 5). + """ + r_neutre: dict[int, float] = {} + notes: list[str] = [] + for seed, traces in sorted(calib_traces_by_seed.items()): + p1 = propa(traces, "calib_obj1", k_features=k_features) + p2 = propa(traces, "calib_obj2", k_features=k_features) + if p2 == 0.0: + notes.append(f"seed {seed}: propa(calib_obj2)=0 — graine exclue du " + "calcul de σ_self (0/0 non défini, jamais fabriqué)") + continue + r_neutre[seed] = p1 / p2 + if len(r_neutre) < 3: + raise ValueError(f"calibration invalide : {len(r_neutre)} graines " + "exploitables (<3) — l'instrument ne permet pas de " + "fixer ε_self, verdict INCONCLUSIF par construction") + sigma = float(np.std(list(r_neutre.values()), ddof=1)) + return Calibration(r_neutre_by_seed=r_neutre, sigma_self=sigma, notes=notes) + + +# --------------------------------------------------------------------------- # +# Table de verdict multi-niveau (scellée par le pré-enregistrement c.1245) +# --------------------------------------------------------------------------- # +def apply_verdict_self(r_by_seed: dict[int, float], + discrimination_by_seed: dict[int, float], + epsilon_self: float) -> tuple[str, list[str]]: + """Applique EXACTEMENT la table de verdict multi-niveau du pré-enregistrement. + + - FALSIFIED : médiane < 0.50 (répression du self) ou > 2.50 (artefact + persona figée) sur ≥ 3 graines ; + - CONFIRMED : médiane ∈ [0.50, 2.00], discrimination médiane ≥ ε_self, + médiane ∉ [0.85, 1.15], médiane ≤ 2.50, ET les 5 graines hors bande + tueuse ; + - INCONCLUSIF : médiane dans la bande MAIS discrimination < ε_self sur + ≥ 1 graine OU R_self ∈ [0.85, 1.15] sur ≥ 1 graine. + """ + values = np.asarray(list(r_by_seed.values()), dtype=float) + disc = np.asarray(list(discrimination_by_seed.values()), dtype=float) + r_med = float(np.median(values)) + kills: list[str] = [] + n_low = int((values < 0.50).sum()) + n_high = int((values > 2.50).sum()) + if n_low >= 3: + kills.append(f"médiane {r_med:.3f} < 0.50 sur {n_low}/5 graines " + "(workspace RÉPRIME la self-représentation)") + if n_high >= 3: + kills.append(f"médiane {r_med:.3f} > 2.50 sur {n_high}/5 graines " + "(artefact persona figée — auto-blocage)") + if kills: + return "FALSIFIED", kills + + band = 0.50 <= r_med <= 2.00 + disc_med = float(np.median(disc)) + in_kill_band = {s: 0.85 <= r <= 1.15 for s, r in r_by_seed.items()} + if not band: + return "FALSIFIED", [f"médiane {r_med:.3f} hors bande [0.50, 2.00] " + "sans seuil de falsification majoritaire — " + "traitée comme FALSIFIED (répression partielle)"] + if disc_med < epsilon_self: + kills.append(f"discrimination médiane {disc_med:.4f} < ε_self " + f"{epsilon_self:.4f}") + if any(in_kill_band.values()): + kills.append("R_self ∈ [0.85, 1.15] sur " + f"{sum(in_kill_band.values())}/5 graines (indistinguable " + "du ratio neutre — self ≈ autrui)") + if kills: + return "INCONCLUSIF", kills + return "CONFIRMED", [] + + +# --------------------------------------------------------------------------- # +# Scoreboard final +# --------------------------------------------------------------------------- # +@dataclass +class Scoreboard: + """Scoreboard pré-enregistré : médiane, IC95 bootstrap, discrimination.""" + + r_self_by_seed: dict[int, float] + propa_by_seed: dict[int, dict[str, float]] + r_median: float + ci95: tuple[float, float] + discrimination_median: float + epsilon_self: float + verdict: str + kill_details: list[str] = field(default_factory=list) + + +def score_main_arms(main_traces_by_seed: dict[int, dict], + calibration: Calibration, *, k_features: int = 64) -> Scoreboard: + """Scoreboard final : R_self par graine, IC95 bootstrap n=200, verdict. + + Exigence anti-HARKing : ``calibration.epsilon_self`` doit déjà être + assemblé (l'appelant écrit σ_self AVANT d'invoquer cette fonction — + vérifié par :func:`run_analysis` et par le runner CLI en deux phases). + """ + if calibration.propa_other_median_scale is None or calibration.epsilon_self is None: + raise ValueError("ε_self non assemblé : la calibration doit être " + "complétée (propa(autrui)_médian fixé) AVANT le score " + "des bras principaux — anti-HARKing.") + r_by_seed: dict[int, float] = {} + propa_by_seed: dict[int, dict[str, float]] = {} + disc_by_seed: dict[int, float] = {} + for seed, traces in sorted(main_traces_by_seed.items()): + p_self = propa(traces, "self_state", k_features=k_features) + p_other = propa(traces, "other_agent", k_features=k_features) + if p_other == 0.0: + raise ValueError(f"seed {seed}: propa(other_agent)=0 — R_self " + "non défini (0/0), graine à documenter comme " + "échec de mesure, jamais à imputer") + r_by_seed[seed] = p_self / p_other + propa_by_seed[seed] = {"self_state": p_self, "other_agent": p_other} + disc_by_seed[seed] = abs(p_self - p_other) + + values = np.asarray(list(r_by_seed.values()), dtype=float) + rng = np.random.default_rng(20260913) + boots = [float(np.median(rng.choice(values, size=values.size, replace=True))) + for _ in range(200)] + ci95 = (float(np.quantile(boots, 0.025)), float(np.quantile(boots, 0.975))) + + epsilon = calibration.epsilon_self + verdict, kills = apply_verdict_self(r_by_seed, disc_by_seed, epsilon) + return Scoreboard( + r_self_by_seed=r_by_seed, + propa_by_seed=propa_by_seed, + r_median=float(np.median(values)), + ci95=ci95, + discrimination_median=float(np.median(list(disc_by_seed.values()))), + epsilon_self=epsilon, + verdict=verdict, + kill_details=kills, + ) + + +def run_analysis(calibration: Calibration, + main_traces_by_seed: dict[int, dict], *, k_features: int = 64) -> dict: + """Assemble ε_self (échelle = médiane propa(autrui) des bras principaux) + puis le scoreboard — dans cet ordre — et renvoie le dictionnaire complet + sérialisable pour ``ict/results/self_model_minimal_results.json``.""" + other_values = [propa(traces, "other_agent", k_features=k_features) + for _, traces in sorted(main_traces_by_seed.items())] + scale = float(np.median(other_values)) + calibration.propa_other_median_scale = scale + calibration.epsilon_self = 0.5 * calibration.sigma_self * scale + board = score_main_arms(main_traces_by_seed, calibration, + k_features=k_features) + return { + "case": "case4_self_model_minimal", + "prediction": ("R_self = propa(q(soi))/propa(q(autrui)) ∈ " + "[0.50, 2.00] ET discrimination ≥ ε_self"), + "substrate": ("Qwen3.5-2B-Base, couche 12/24, SAE Qwen-Scope W32K-L0_50 " + "(top-k=50) — même substrat que case 5 #15547 ; le " + "pré-enregistrement nommait J-Lens Track P 4B-instruct, " + "non publié (note #5681)"), + "calibration": { + "r_neutre_by_seed": calibration.r_neutre_by_seed, + "sigma_self": calibration.sigma_self, + "propa_other_median_scale": scale, + "epsilon_self": calibration.epsilon_self, + "formula": "ε_self = 0.5 × σ_self × propa(q(autrui))_médian", + "notes": calibration.notes, + }, + "main": { + "r_self_by_seed": board.r_self_by_seed, + "propa_by_seed": board.propa_by_seed, + "r_median": board.r_median, + "ci95_bootstrap_n200": list(board.ci95), + "discrimination_median": board.discrimination_median, + }, + "verdict": board.verdict, + "kill_details": board.kill_details, + } + + +def _cli(argv: list[str] | None = None) -> None: + """Runner CLI : ``prompts`` génère les JSON d'extraction, ``calibrate`` + DOIT précéder ``score`` (fichier exigé). + + ``python -m ict.self_model_minimal prompts --seed 0 --out-main m.json + --out-calib c.json`` écrit les jeux de prompts pour + ``extract_sae_traces.py --prompts-json``. ``calibrate --out + calib.json`` gèle σ_self ; ``score --calibration calib.json`` refuse de + tourner sans ce fichier — garde anti-HARKing opérationnel (l'ordre + inverse est impossible, pas juste déconseillé). + """ + import argparse + + from .sae_traces import load_traces + + ap = argparse.ArgumentParser(description="Case 4 (#8182) — self-model minimal") + sub = ap.add_subparsers(dest="cmd", required=True) + g = sub.add_parser("prompts", help="JSON de prompts pour extract_sae_traces.py") + g.add_argument("--seed", type=int, required=True, choices=list(SEEDS)) + g.add_argument("--out-main", required=True) + g.add_argument("--out-calib", required=True) + c = sub.add_parser("calibrate", help="phase 1 : σ_self gelé avant les bras principaux") + c.add_argument("traces", nargs="+", help=".npz de calibration (5 seeds)") + c.add_argument("--out", required=True, help="JSON de calibration figé") + s = sub.add_parser("score", help="phase 2 : bras principaux + verdict") + s.add_argument("traces", nargs="+", help=".npz des bras principaux (5 seeds)") + s.add_argument("--calibration", required=True, help="JSON produit par 'calibrate'") + s.add_argument("--out", required=True, help="JSON résultat (ict/results/)") + + args = ap.parse_args(argv) + if args.cmd == "prompts": + write_prompts_json(args.seed, args.out_main, calibration=False) + write_prompts_json(args.seed, args.out_calib, calibration=True) + print(f"[prompts] seed {args.seed} -> {args.out_main} + {args.out_calib}") + return + if args.cmd == "calibrate": + calib = calibrate_epsilon_self({i: load_traces(p) for i, p in enumerate(args.traces)}) + Path(args.out).write_text(json.dumps({ + "r_neutre_by_seed": calib.r_neutre_by_seed, + "sigma_self": calib.sigma_self, + "notes": calib.notes, + }, ensure_ascii=False, indent=1), encoding="utf-8", newline="\n") + print(f"[calibrate] sigma_self={calib.sigma_self:.6f} -> {args.out}") + return + frozen = json.loads(Path(args.calibration).read_text(encoding="utf-8")) + calib = Calibration( + r_neutre_by_seed={int(k): v for k, v in frozen["r_neutre_by_seed"].items()}, + sigma_self=float(frozen["sigma_self"]), + notes=list(frozen.get("notes", []))) + result = run_analysis(calib, {i: load_traces(p) for i, p in enumerate(args.traces)}) + Path(args.out).write_text(json.dumps(result, ensure_ascii=False, indent=1), + encoding="utf-8", newline="\n") + print(f"[score] verdict={result['verdict']} -> {args.out}") + + +if __name__ == "__main__": + _cli() diff --git a/MyIA.AI.Notebooks/IIT/ICT-Series/ict/tests/test_self_model_minimal.py b/MyIA.AI.Notebooks/IIT/ICT-Series/ict/tests/test_self_model_minimal.py new file mode 100644 index 0000000000..34b8562544 --- /dev/null +++ b/MyIA.AI.Notebooks/IIT/ICT-Series/ict/tests/test_self_model_minimal.py @@ -0,0 +1,265 @@ +"""Tests du banc case 4 — self-model minimal (#8182, Metzinger). + +Les seuils viennent du pré-enregistrement scellé dans +``docs/ict/dissociations-matrix.md`` (case 4, PR c.1245) : bande +[0.50, 2.00], tueuses [0.85, 1.15] et > 2.50, calibration ε_self = +0.5 × σ_self × propa(autrui)_médian exécutée AVANT le test principal. + +Les tests ne mesurent pas de conscience : ils pincent la réutilisation de +l'opérateur canonique ``propa`` (importé de case 5, non modifié), +l'appariement des contre-factuels self/autrui/neutre, la formule de +calibration et CHAQUE branche de la table de verdict multi-niveau. +""" + +from __future__ import annotations + +import json +import re + +import numpy as np +import pytest + +from ict.attention_schema import CONTEXTS_BY_SEED +from ict.self_model_minimal import ( + ARM_SETS, + CALIBRATION_TARGETS, + SEEDS, + TASK_NEUTRAL, + TASK_OTHER, + TASK_SELF, + Calibration, + apply_verdict_self, + build_calibration_sets, + build_prompt_sets, + calibrate_epsilon_self, + run_analysis, + score_main_arms, +) +from ict.attention_schema import propa + + +def _mk_trace(sets: dict[str, list[tuple[np.ndarray, np.ndarray]]], d_sae: int = 8) -> dict: + """Assemble un dict de trace au schema .npz de sae_traces.load_traces.""" + prompts = {} + for name, prompts_raw in sets.items(): + for i, (ids, vals) in enumerate(prompts_raw): + assert ids.shape == vals.shape and ids.ndim == 2 + prompts[(name, i)] = {"ids": ids.astype(np.int32), + "vals": vals.astype(np.float32), + "tokens": np.array(["x"] * ids.shape[0])} + return {"meta": {"d_sae": d_sae, "k": ids.shape[1]}, "prompts": prompts} + + +def _synth_prompt(T: int, k: int, *, hot_feature: int, ignition_at: int, + consumer_top1: list[int], rng_seed: int = 0) -> tuple[np.ndarray, np.ndarray]: + """Un prompt synthétique : ignition ``ignition_at`` (somme top-k maximale), + consommateurs ``ignition_at+1..+5`` portant ``consumer_top1`` en argmax.""" + rng = np.random.default_rng(rng_seed) + ids = rng.integers(0, 8, size=(T, k)) + vals = rng.uniform(0.1, 1.0, size=(T, k)) + vals = np.sort(vals, axis=1)[:, ::-1] # topk trié desc + order = np.argsort(ids[ignition_at]) # features distinctes à l'ignition + ids[ignition_at] = ids[ignition_at][order] + vals[ignition_at] = np.array([5.0, 4.0, 3.0][:k]) # somme = max du prompt + ids[ignition_at, 0] = hot_feature + for j, feat in enumerate(consumer_top1): + pos = ignition_at + 1 + j + if pos < T: + ids[pos, 0] = feat + vals[pos, 0] = 2.5 # argmax net du consommateur + vals = np.sort(vals, axis=1)[:, ::-1] # re-trier les valeurs + return ids, vals + + +def _pair_prompt(hots: int, *, rng_seed: int = 0, hot: int = 6, distractor: int = 7, + k: int = 3) -> tuple[np.ndarray, np.ndarray]: + """Prompt à ``propa`` exacte : 2 ignitions (10, 25), 10 consommateurs. + + Même construction que test_attention_schema (la formule propa est + inchangée — c'est ce que la case 4 doit garantir) : + ``propa(bras) = hots/10`` par prompt, exactement. + """ + T = 40 + rng = np.random.default_rng(rng_seed) + ids = rng.integers(0, 2, size=(T, k)) + vals = np.sort(rng.uniform(0.30, 0.34, size=(T, k)), axis=1)[:, ::-1] + hots = max(0, min(10, int(hots))) + pattern = [1] * hots + [0] * (10 - hots) + j = 0 + for ign in (10, 25): + ids[ign] = (hot, hot, hot) + vals[ign] = (8.0, 6.0, 4.0) + for pos in range(ign + 1, ign + 6): + if pattern[j]: + ids[pos, 0], vals[pos, 0] = hot, 2.5 + else: + ids[pos, 0], vals[pos, 0] = distractor, 1.05 + j += 1 + return ids, vals + + +def _arm_traces(arm_hots: dict[str, int]) -> dict: + """Trace 3 prompts/bras au schema .npz ; chaque prompt à ``hots`` hot.""" + prompts = {} + for name, hots in arm_hots.items(): + for i in range(3): + ids, vals = _pair_prompt(hots, rng_seed=i) + prompts[(name, i)] = {"ids": ids.astype(np.int32), + "vals": vals.astype(np.float32), + "tokens": np.array(["x"] * ids.shape[0])} + return {"meta": {"d_sae": 8, "k": 3}, "prompts": prompts} + + +class TestPromptsApparies: + def test_9_prompts_par_graine_trois_bras(self): + for seed in SEEDS: + sets = build_prompt_sets(seed) + assert set(sets) == set(ARM_SETS) + assert all(len(v) == 3 for v in sets.values()) + + def test_contexte_partage_identique_dans_chaque_triplet(self): + """Contre-factuels exacts : même contexte amont, seule la cible diffère.""" + for seed in SEEDS: + sets = build_prompt_sets(seed) + for s, o, n in zip(sets["self_state"], sets["other_agent"], + sets["neutral_statue"]): + ctx_s = s[: -len(TASK_SELF)] + ctx_o = o[: -len(TASK_OTHER)] + ctx_n = n[: -len(TASK_NEUTRAL)] + assert ctx_s == ctx_o == ctx_n + + def test_meme_banque_de_contextes_que_case_5(self): + """Comparabilité cross-case : la banque importée est inchangée.""" + assert len(CONTEXTS_BY_SEED) == 5 + assert all(len(v) == 3 for v in CONTEXTS_BY_SEED.values()) + + def test_calibration_deux_objets_publics_non_identitaires(self): + for seed in SEEDS: + calib = build_calibration_sets(seed) + assert set(calib) == {"calib_obj1", "calib_obj2"} + joined = " ".join(calib["calib_obj1"] + calib["calib_obj2"]) + assert "état interne" not in joined.lower() + # mots identitaires aux frontières de mots ("toi" matcherait + # "laboratoire" en substring naïf). + for mot in ("agent", "toi", "tu", "tes"): + assert not re.search(rf"\b{mot}\b", joined.lower()) + + def test_bras_neutre_distinct_des_entites_de_calibration(self): + """L'objet public Z du bras neutre ne recycle pas calib_obj1/obj2.""" + entites = {clause.split("Décris ")[1].split(" après")[0] + for _, clause in CALIBRATION_TARGETS} + neutre = TASK_NEUTRAL.split("Décris ")[1].split(" après")[0] + assert neutre not in entites + + +class TestOperateurCanonique: + def test_propa_est_celui_de_case_5(self): + """L'instrument n'est PAS retouché : même formule, mêmes exceptions.""" + from ict.self_model_minimal import propa as propa_case4 + assert propa_case4 is propa + + def test_formule_sur_trace_synthetique(self): + ids, vals = _synth_prompt(T=30, k=3, hot_feature=1, ignition_at=10, + consumer_top1=[1, 1, 1, 5, 5], rng_seed=0) + ids2, vals2 = _synth_prompt(T=30, k=3, hot_feature=1, ignition_at=12, + consumer_top1=[1, 5, 1, 5, 1], rng_seed=1) + traces = _mk_trace({"bras": [(ids, vals), (ids2, vals2)]}) + assert propa(traces, "bras", k_features=2) == pytest.approx(0.6) + + def test_bras_absent_refuse(self): + traces = _mk_trace({"bras": [_synth_prompt(20, 3, hot_feature=1, ignition_at=10, + consumer_top1=[1, 1, 1, 1, 1])]}) + with pytest.raises(ValueError, match="absent"): + propa(traces, "autre_bras", k_features=2) + + def test_trace_trop_courte_refusee(self): + ids = np.zeros((4, 3), dtype=np.int32) + vals = np.ones((4, 3), dtype=np.float32) + with pytest.raises(ValueError, match="trop courte"): + propa(_mk_trace({"bras": [(ids, vals)]}), "bras") + + +class TestCalibration: + RATIOS = {0: 1.0, 1: 1.2, 7: 0.75, 42: 1.0, 99: 6 / 7} + + @staticmethod + def _seed_traces(r: float) -> dict: + return _arm_traces({"calib_obj1": 6, "calib_obj2": round(10 * 0.6 / r)}) + + def test_sigma_et_rapports_neutres(self): + traces = {s: self._seed_traces(r) for s, r in self.RATIOS.items()} + calib = calibrate_epsilon_self(traces, k_features=2) + expected_sigma = float(np.std(list(self.RATIOS.values()), ddof=1)) + assert calib.sigma_self == pytest.approx(expected_sigma, rel=1e-6) + for s, r in self.RATIOS.items(): + assert calib.r_neutre_by_seed[s] == pytest.approx(r, rel=0.05) + + def test_moins_de_3_graines_exploitables_refuse(self): + with pytest.raises(ValueError, match="<3"): + calibrate_epsilon_self({0: self._seed_traces(1.0)}, k_features=2) + + def test_epsilon_assemble_apres_scale_uniquement(self): + calib = Calibration(r_neutre_by_seed={0: 1.0, 1: 1.1, 7: 0.9, 42: 1.05, 99: 0.95}, + sigma_self=0.1) + with pytest.raises(ValueError, match="anti-HARKing"): + score_main_arms({}, calib) + + +class TestVerdict: + def test_confirmed(self): + r = {s: 1.4 for s in SEEDS} + disc = {s: 0.10 for s in SEEDS} + verdict, kills = apply_verdict_self(r, disc, epsilon_self=0.02) + assert verdict == "CONFIRMED" and not kills + + def test_falsified_repression(self): + r = {0: 0.3, 1: 0.2, 7: 0.4, 42: 0.35, 99: 1.4} + verdict, kills = apply_verdict_self(r, {s: 0.1 for s in SEEDS}, 0.02) + assert verdict == "FALSIFIED" and any("réprime" in k.lower() for k in kills) + + def test_falsified_artefact_persona_figee(self): + r = {0: 3.1, 1: 2.9, 7: 3.3, 42: 1.4, 99: 1.5} + verdict, kills = apply_verdict_self(r, {s: 0.1 for s in SEEDS}, 0.02) + assert verdict == "FALSIFIED" and any("persona figée" in k for k in kills) + + def test_inconclusif_bande_tueuse_une_graine(self): + r = {0: 1.4, 1: 1.45, 7: 1.5, 42: 1.4, 99: 1.0} + verdict, kills = apply_verdict_self(r, {s: 0.1 for s in SEEDS}, 0.02) + assert verdict == "INCONCLUSIF" and any("0.85, 1.15" in k for k in kills) + + def test_inconclusif_discrimination_sous_epsilon(self): + r = {s: 1.4 for s in SEEDS} + verdict, kills = apply_verdict_self(r, {s: 0.001 for s in SEEDS}, 0.02) + assert verdict == "INCONCLUSIF" and any("ε_self" in k for k in kills) + + def test_hors_bande_sans_majorite_falsifie(self): + r = {0: 0.6, 1: 0.65, 7: 0.55, 42: 2.3, 99: 0.62} # médiane 0.62, dans bande + verdict, _ = apply_verdict_self(r, {s: 0.1 for s in SEEDS}, 0.02) + assert verdict == "CONFIRMED" + + +class TestRunAnalysis: + def test_ordre_calibration_puis_score_serialisable(self): + ratios = {0: 1.0, 1: 1.2, 7: 0.75, 42: 1.0, 99: 6 / 7} + calib = calibrate_epsilon_self( + {s: _arm_traces({"calib_obj1": 6, "calib_obj2": round(6 / r)}) + for s, r in ratios.items()}, k_features=2) + # propa(self_state) = 0.8, propa(other_agent) = 0.5 → R_self = 1.6/seed. + main = {s: _arm_traces({"self_state": 8, "other_agent": 5}) for s in SEEDS} + result = run_analysis(calib, main, k_features=2) + assert set(result) >= {"case", "substrate", "calibration", "main", "verdict"} + assert result["case"] == "case4_self_model_minimal" + assert result["calibration"]["epsilon_self"] == pytest.approx( + 0.5 * calib.sigma_self * result["calibration"]["propa_other_median_scale"]) + assert result["main"]["r_self_by_seed"][0] == pytest.approx(1.6, rel=0.05) + assert result["verdict"] in {"CONFIRMED", "FALSIFIED", "INCONCLUSIF"} + json.dumps(result) # sérialisable pour results/ + + def test_rapport_exact_sur_grille_dixiemes(self): + """R_self réalisé exactement : self 8/10, autrui 5/10 → 1.6 partout.""" + main = {s: _arm_traces({"self_state": 8, "other_agent": 5}) for s in SEEDS} + calib = Calibration( + r_neutre_by_seed={s: 1.0 for s in SEEDS}, sigma_self=0.05) + result = run_analysis(calib, main, k_features=2) + for r in result["main"]["r_self_by_seed"].values(): + assert r == pytest.approx(1.6, rel=0.05) diff --git a/MyIA.AI.Notebooks/IIT/ICT-Series/traces/case4_s0_qwen35-2b-base_layer12of24_trained.npz b/MyIA.AI.Notebooks/IIT/ICT-Series/traces/case4_s0_qwen35-2b-base_layer12of24_trained.npz new file mode 100644 index 0000000000..9b08e47555 Binary files /dev/null and b/MyIA.AI.Notebooks/IIT/ICT-Series/traces/case4_s0_qwen35-2b-base_layer12of24_trained.npz differ diff --git a/MyIA.AI.Notebooks/IIT/ICT-Series/traces/case4_s1_qwen35-2b-base_layer12of24_trained.npz b/MyIA.AI.Notebooks/IIT/ICT-Series/traces/case4_s1_qwen35-2b-base_layer12of24_trained.npz new file mode 100644 index 0000000000..b2ec3fd2e4 Binary files /dev/null and b/MyIA.AI.Notebooks/IIT/ICT-Series/traces/case4_s1_qwen35-2b-base_layer12of24_trained.npz differ diff --git a/MyIA.AI.Notebooks/IIT/ICT-Series/traces/case4_s42_qwen35-2b-base_layer12of24_trained.npz b/MyIA.AI.Notebooks/IIT/ICT-Series/traces/case4_s42_qwen35-2b-base_layer12of24_trained.npz new file mode 100644 index 0000000000..fd24126509 Binary files /dev/null and b/MyIA.AI.Notebooks/IIT/ICT-Series/traces/case4_s42_qwen35-2b-base_layer12of24_trained.npz differ diff --git a/MyIA.AI.Notebooks/IIT/ICT-Series/traces/case4_s7_qwen35-2b-base_layer12of24_trained.npz b/MyIA.AI.Notebooks/IIT/ICT-Series/traces/case4_s7_qwen35-2b-base_layer12of24_trained.npz new file mode 100644 index 0000000000..fb1e7a59a6 Binary files /dev/null and b/MyIA.AI.Notebooks/IIT/ICT-Series/traces/case4_s7_qwen35-2b-base_layer12of24_trained.npz differ diff --git a/MyIA.AI.Notebooks/IIT/ICT-Series/traces/case4_s99_qwen35-2b-base_layer12of24_trained.npz b/MyIA.AI.Notebooks/IIT/ICT-Series/traces/case4_s99_qwen35-2b-base_layer12of24_trained.npz new file mode 100644 index 0000000000..6a155ae42e Binary files /dev/null and b/MyIA.AI.Notebooks/IIT/ICT-Series/traces/case4_s99_qwen35-2b-base_layer12of24_trained.npz differ diff --git a/MyIA.AI.Notebooks/IIT/ICT-Series/traces/case4calib_s0_qwen35-2b-base_layer12of24_trained.npz b/MyIA.AI.Notebooks/IIT/ICT-Series/traces/case4calib_s0_qwen35-2b-base_layer12of24_trained.npz new file mode 100644 index 0000000000..2ad7359b63 Binary files /dev/null and b/MyIA.AI.Notebooks/IIT/ICT-Series/traces/case4calib_s0_qwen35-2b-base_layer12of24_trained.npz differ diff --git a/MyIA.AI.Notebooks/IIT/ICT-Series/traces/case4calib_s1_qwen35-2b-base_layer12of24_trained.npz b/MyIA.AI.Notebooks/IIT/ICT-Series/traces/case4calib_s1_qwen35-2b-base_layer12of24_trained.npz new file mode 100644 index 0000000000..1c17081002 Binary files /dev/null and b/MyIA.AI.Notebooks/IIT/ICT-Series/traces/case4calib_s1_qwen35-2b-base_layer12of24_trained.npz differ diff --git a/MyIA.AI.Notebooks/IIT/ICT-Series/traces/case4calib_s42_qwen35-2b-base_layer12of24_trained.npz b/MyIA.AI.Notebooks/IIT/ICT-Series/traces/case4calib_s42_qwen35-2b-base_layer12of24_trained.npz new file mode 100644 index 0000000000..a52459fa8c Binary files /dev/null and b/MyIA.AI.Notebooks/IIT/ICT-Series/traces/case4calib_s42_qwen35-2b-base_layer12of24_trained.npz differ diff --git a/MyIA.AI.Notebooks/IIT/ICT-Series/traces/case4calib_s7_qwen35-2b-base_layer12of24_trained.npz b/MyIA.AI.Notebooks/IIT/ICT-Series/traces/case4calib_s7_qwen35-2b-base_layer12of24_trained.npz new file mode 100644 index 0000000000..de81f9582a Binary files /dev/null and b/MyIA.AI.Notebooks/IIT/ICT-Series/traces/case4calib_s7_qwen35-2b-base_layer12of24_trained.npz differ diff --git a/MyIA.AI.Notebooks/IIT/ICT-Series/traces/case4calib_s99_qwen35-2b-base_layer12of24_trained.npz b/MyIA.AI.Notebooks/IIT/ICT-Series/traces/case4calib_s99_qwen35-2b-base_layer12of24_trained.npz new file mode 100644 index 0000000000..557e6fdc20 Binary files /dev/null and b/MyIA.AI.Notebooks/IIT/ICT-Series/traces/case4calib_s99_qwen35-2b-base_layer12of24_trained.npz differ diff --git a/docs/ict/dissociations-matrix.md b/docs/ict/dissociations-matrix.md index 35f87f62af..d954a33dd0 100644 --- a/docs/ict/dissociations-matrix.md +++ b/docs/ict/dissociations-matrix.md @@ -203,7 +203,7 @@ Issue [#12183](https://github.com/jsboige/CoursIA/issues/12183) tranche cette co | **s ⟂ π** (saillance sans prégnance, et réc.) | `TESTÉ (CONFIRMÉ décision, FALSIFIÉ total)` — [#9553](https://github.com/jsboige/CoursIA/pull/9553) MERGED (po-2023, 2026-08-05) | Un animat à canaux d'entrée *indépendants* pour `s` (saillance perceptuelle) et `π` (valence apprise Rescorla-Wagner) exhibe deux régimes — (A) haut-`s`/`π`≈0 (saillant-neutre), (B) bas-`s`/haut-`π` (discret-chargé) — tels que l'engagement (approche) est gouverné par `π` et non par `s` : \|corr(engagement, π \| s)\| > 0.5 **et** \|corr(engagement, s \| π)\| < 0.2 (prouvoir prédictif propre à `π`, nul à `s`). | Un animat **réactif pur** (π ≡ s, pas d'apprentissage de valence) voit les deux corrélations partielles converger vers ~0 / égales : la dissociation s'évanouit (c'est l'absence de `π` appris, non la saillance, qui portait le signal). | Animat lignée ICT-12c (`PregnanceAnimat`), **CPU-only** — candidate 1ʳᵉ case | Vervaeke — *relevance realization* (saillance et prégnance = deux réalisations de pertinence distinctes). Crédit : [#8182](https://github.com/jsboige/CoursIA/issues/8182) iceberg, mappé dans [#9533](https://github.com/jsboige/CoursIA/issues/9533) (ai-01, 2026-08-06). **Lecture grade C.** Dans son cadre (*Relevance Realization*, J. Vervaeke, avec S. Lomas, T. Dupre, *Insight and Ascendency* — framework de la *4E cognition* et de la *relevance realization*), **s** porte la *pertinence à court-terme* (ce qui attire l'attention **maintenant**, saillance perceptuelle au sens de l'allocation attentionnelle) et **π** porte la *pertinence à long-terme* (ce qui vaut la peine d'être approché **à terme**, valence apprise par reinforcement) — deux modes de *relevance* qui ne coïncident pas opérationnellement : un stimulus très saillant (objet brillant) peut être neutre en valence apprise (sans valeur apprise), un stimulus discret mais chargé en π (algue toxique non flashy) est préféré à long-terme. La dissociation **s ⟂ π** mesurée sur l'animat ICT-12c — *la saillance pour voir, la prégnance pour agir* (verdict `DISSOCIATED-AT-DECISION`) — est précisément le test substrat que Vervaeke n'opère pas (il décrit la *relevance realization* comme *framework* théorique, sans proposer d'animat à canaux indépendants). **Honnêteté grade C** : (a) la transposition est une *lecture* du substrat ICT-12c à travers le cadre Vervaeke, pas une validation ; (b) Vervaeke lui-même parle de *coupling* (« the agent is a process of relevance realization that is best understood as a complex adaptive system that realizes its own relevance through its self-organizing dynamics » — paraphrasé), pas de stricte orthogonalité ; (c) l'orthogonalité stricte « voir sans agir / agir sans voir » est une **simplification** ICT, justifiable **seulement** comme point-zéro de falsification. La conjecture dérivable (jalon 3 #8182) — *chaque strate ICT = acquisition d'une adjonction que la précédente n'a pas* — se testerait sur le même coin de substrat : *saillance et prégnance comme adjonctions droite/gauche d'un foncteur de pertinence*, mais c'est une autre brique (gated sur #7738). | | **W diffuse un q erroné** (confabulation) | `PRÉDIT` | Une ignition workspace (`W_t` pic) déclenchée par une représentation **fausse** `q̂` propage `q̂` aux consommateurs en aval à un taux ≥ 80 % du taux d'une ignition vraie (la workspace **consacre** l'erreur presque aussi efficacement que le vrai). | Une ignition **aléatoire** (pas de `q` du tout) propage à un taux non-distinguable de l'ignition fausse : la « consécration » n'est alors que du broadcast non-discriminant, pas une propriété de l'erreur. Tue si \|propa(q̂ faux) − propa(aléatoire)\| < ε. | LLM via SAE-JLens ([#5681](https://github.com/jsboige/CoursIA/issues/5681), [#8236](https://github.com/jsboige/CoursIA/issues/8236)) | — (aucun hook grade C identifié dans #9533 pour cette case) | | **p̂ auto-référent** | `TESTÉ (CONFIRMÉ)` — κ_c observé 0.080 (prédit ≈0.053, biais +0.027), 5/5 graines — [#9567](https://github.com/jsboige/CoursIA/pull/9567) MERGED (po-2025, 2026-08-06). **Biais +0.027 diagnostiqué : `ARTEFACT_DE_MESURE`** — `ict/phat_causal_unlink.py` (po-2027, 2026-09-06, 5 graines) : le critère de la case est **fini** (R_T/R_0 ≥ 5 à T=200), sa frontière déterministe est κ\*_finite = (5^(1/200)−0.95)/0.95 = **0.0611**, bruitée κ\*_noisy = **0.0601** ; sur grille fine uniforme [0.050, 0.080] pas 0.002, la frontière mesurée tombe à **0.062 sur 5/5 graines** — dans la bande pré-enregistrée [κ\*_noisy − maille, κ\*_finite + maille] — tandis que la grille d'origine (première maille au-dessus de κ\* : 0.08) reproduit exactement le 0.080 historique (5/5). Décomposition du biais : +0.0085 frontière finie, +0.019 arrondi de maille. Contrôles : délieur κ=0 borné, κ=0.03 sans faux positif. **Le biais n'est pas une propriété de la boucle** — la théorie linéaire asymptotique reste la bonne description. | *(prédiction à pré-enregistrer)* Une boucle fermée `p̂ → action → p̂` (le représentant interne prédit ses propres états futurs) est stable sur un régime borné mais diverge (oscillation amplifiée) hors de ce régime. Seuil de stabilité pré-enregistré avant test. | Un délieur causal (la `p̂` prédit l'environnement, pas elle-même) supprime la divergence : la boucle auto-référentielle était bien la cause. | Animat / grokking (ICT-17b) | Hofstadter — *strange loops* (auto-référence comme boucle étrange). Crédit : [#8182](https://github.com/jsboige/CoursIA/issues/8182) iceberg, mappé dans [#9533](https://github.com/jsboige/CoursIA/issues/9533) (ai-01, 2026-08-06). **Lecture grade C.** Dans son cadre (*I Am a Strange Loop*, D. R. Hofstadter, Basic Books, 2007, ISBN 978-0465030781 / 0-465-03078-5), une *strange loop* est un **cycle auto-référentiel** qui, par niveaux d'abstraction intercalés (un système qui se représente, se perçoit, ou se *calcule* lui-même à travers une hiérarchie *embranchée* — « tangled hierarchy »), fait émerger un **« je »** comme phénomène *haut-niveau* irréductible aux composants *bas-niveau*. La boucle `p̂ → action → p̂` testée substrat ICT-17b est précisément un cas d'**auto-modélisation computationnelle** : `p̂` (représentant interne d'états futurs) **ré-intègre sa propre trajectoire** dans l'état interne au pas suivant — une *tangled hierarchy* au sens de Hofstadter, où le « bas » (action musculaire, environnement) et le « haut » (modèle de soi) se replient l'un sur l'autre par le canal de la prédiction fermée. Le **biais de calibration +0.027** observé sur κ_c (cible 0.053, mesure 0.080, 5/5 graines) est *cohérent* avec l'intuition Hofstadter : une strange loop exacte n'est pas neutre — elle **glisse** (sa calibration de Markov blankets n'est jamais *exactement* celle de l'environnement). Ce que les 5/5 graines portent est la **robustesse** du biais — ce n'est pas du bruit d'échantillonnage ; elles ne tranchent **pas** entre un biais *structurel* (la boucle fermée cause la dérive) et un *artefact* de calibration (décalage systématique de la procédure). *(Tranché le 2026-09-06 : **artefact** — le « glissement hofstadterien » de κ_c n'a pas de support numérique ; voir `ARTEFACT_DE_MESURE` en statut. La lecture documentaire de la boucle elle-même reste valide : seule son interprétation du biais perd son support.)* Le discriminant était le **délieur causal** nommé dans la colonne « expérience de dissociation » de cette même rangée — et il **a** été exécuté : `delieur_verdict` (κ=0) fait partie intégrante de `run_full_protocol` du module d'origine, et le verdict CONFIRMÉ de la case l'exigeait (`dissociation_full = delieur_stable AND any_loop_divergent`). C'était la **note** qui était stale (pré-run, jamais rafraîchie), pas le délieur. La question structurel-vs-artefact laissée ouverte par ces 5/5 graines est **tranchée depuis** : voir le diagnostic `ARTEFACT_DE_MESURE` dans la colonne statut (2026-09-06) — le biais +0.027 est intégralement expliqué par le critère fini (frontière 0.0611, +0.0085) et l'arrondi à la maille de la grille d'origine (0.06 → 0.08, +0.019), pas par la boucle. **Honnêteté grade C** : (a) la transposition est une *lecture* du substrat ICT-17b à travers le cadre Hofstadter, pas une validation empirique de la thèse *I Am a Strange Loop* (qui est philosophique et ne propose aucun protocole falsifiable au sens ICT) ; (b) Hofstadter traite la *tangled hierarchy* comme **heuristique métaphysique** pour le « je » conscient, alors qu'ICT la traite comme **test substrat computationnel** — l'isomorphisme formel entre les deux lectures n'est pas garanti, et la valeur de Hofstadter pour ICT est **documentaire** (fournir un vocabulaire descriptif dense, pas une prédiction quantitative) ; (c) la conjecture dérivable (jalon 3 #8182) — *chaque strate ICT = acquisition d'une adjonction que la précédente n'a pas* — se testerait sur la *p̂ auto-référente* comme suit : *le passage ICT-17a → ICT-17b ajoute-t-il une adjonction « self-reference » que ICT-17a n'a pas, et cette adjonction est-elle exactement la *tangled hierarchy* de Hofstadter formalisable en théorie des catégories ?* — mais c'est une autre brique (gated sur #7738). | -| **self-model minimal** | `PRÉDIT` (cf. chiffrage détaillé c.1245 ci-dessous) | *(prédiction chiffrée détaillée ci-dessous)* Le panel persona lu comme `W_t` appliqué à `q(soi)` (le workspace opère sur une représentation de l'agent lui-même) produit un signal de self-modélisation distinguable d'un modèle d'autrui sur les mêmes données, via le ratio `R_self = propa(q(soi)) / propa(q(autrui)) ∈ [0.50, 2.00]` (cible pré-enregistrée) avec null adversarial explicite (modèle d'autrui identique sauf cible d'attention). | Un modèle entraîné à prédire un *autre* agent de complexité équivalente produit un signal indistinguable (R_self ≈ 1.0, propa(q(soi)) ≈ propa(q(autrui))) : le « self » n'apporte rien au-delà de la modélisation générique d'autrui, OU R_self > 2.50 (self ≫ autrui = artefact, le workspace ne discrimine plus sur la complexité). | Panel persona [#5104](https://github.com/jsboige/CoursIA/issues/5104) / [#5105](https://github.com/jsboige/CoursIA/issues/5105) ; substrat J-Lens Track P ([#5681](https://github.com/jsboige/CoursIA/issues/5681) 4B-instruct persona) | Metzinger — *minimal phenomenal selfhood*. Crédit : [#8182](https://github.com/jsboige/CoursIA/issues/8182), mappé dans [#9533](https://github.com/jsboige/CoursIA/issues/9533) (ai-01, 2026-08-06) | +| **self-model minimal** | `TESTÉ (INCONCLUSIF)` — exécution [#15879](https://github.com/jsboige/CoursIA/pull/15879) (po-2024, 2026-09-13) : R_self médian **1.000** [IC95 bootstrap 0.923–1.000], discrimination médiane **0.000 < ε_self 0.0171** (calibration gelée AVANT le run : σ_self 0.0395), bande tueuse [0.85, 1.15] sur **5/5 graines** — propa(q(soi)) ≈ propa(q(autrui)), le self n'apporte rien au-delà de la modélisation générique d'autrui sur ce substrat. Diagnostic post-hoc (non scellé) : Jaccard top-64 self/autrui **0.61** moyen (0.54–0.68) — même cause instrumentale que case 5 ([#15547](https://github.com/jsboige/CoursIA/pull/15547)) : les features omniprésentes dominent les deux signatures (broadcast quasi-uniforme). Substrat : Qwen3.5-2B-Base couche 12/24 × SAE Qwen-Scope W32K-L0_50, hérité de case 5 pour la comparabilité R_self/R_attn, 5 graines, extraction GPU réelle. Cf. chiffrage détaillé c.1245 ci-dessous. | *(prédiction chiffrée détaillée ci-dessous)* Le panel persona lu comme `W_t` appliqué à `q(soi)` (le workspace opère sur une représentation de l'agent lui-même) produit un signal de self-modélisation distinguable d'un modèle d'autrui sur les mêmes données, via le ratio `R_self = propa(q(soi)) / propa(q(autrui)) ∈ [0.50, 2.00]` (cible pré-enregistrée) avec null adversarial explicite (modèle d'autrui identique sauf cible d'attention). | Un modèle entraîné à prédire un *autre* agent de complexité équivalente produit un signal indistinguable (R_self ≈ 1.0, propa(q(soi)) ≈ propa(q(autrui))) : le « self » n'apporte rien au-delà de la modélisation générique d'autrui, OU R_self > 2.50 (self ≫ autrui = artefact, le workspace ne discrimine plus sur la complexité). | Panel persona [#5104](https://github.com/jsboige/CoursIA/issues/5104) / [#5105](https://github.com/jsboige/CoursIA/issues/5105) ; substrat J-Lens Track P ([#5681](https://github.com/jsboige/CoursIA/issues/5681) 4B-instruct persona) | Metzinger — *minimal phenomenal selfhood*. Crédit : [#8182](https://github.com/jsboige/CoursIA/issues/8182), mappé dans [#9533](https://github.com/jsboige/CoursIA/issues/9533) (ai-01, 2026-08-06) | | **W porte un schéma attentionnel** (attention schema) | `TESTÉ (INCONCLUSIF)` — exécution [#15547](https://github.com/jsboige/CoursIA/pull/15547) (po-2023, 2026-09-11), cf. chiffrage détaillé case 5 ci-dessous | *(prédiction chiffrée détaillée case 5)* Le workspace opère sur une représentation `q̂(attention)` dont la cible est l'**état attentionnel de l'agent lui-même** (le modèle interne de « où mon attention est ») et la propage aux consommateurs en aval à un taux **distinct** du taux d'une représentation `q̂(objet)` ciblant un objet externe non-attentionnel. Cible pré-enregistrée : `R_attn = propa(q̂(attention)) / propa(q̂(objet)) ∈ [0.50, 2.00]` ET `discrimination_attn ≥ ε_attn`. | Un prompt dont la cible d'attention est un **objet tiers sans mention agentique** (par exemple « décris une chaise ») propage à un taux indistinguable de `q̂(attention)` : la workspace **ne porte pas** de schéma attentionnel — c'est du broadcast uniforme, pas une propriété de la cible. Tue si `R_attn ∈ [0.85, 1.15]` OU `R_attn > 2.50` OU `\|propa(q̂(attention)) − propa(q̂(objet))\| < ε_attn`. | Substrat LLM (J-Lens Track S ou P, [#5681](https://github.com/jsboige/CoursIA/issues/5681)) + prompts « décris ton attention » vs « décris un objet X » | Graziano — *Attention Schema Theory* (AST) : la conscience = modèle interne simplifié que le cerveau construit de son propre état attentionnel. Crédit : M.S.A. Graziano, *Consciousness and the Social Brain*, Oxford University Press 2013, ISBN 978-0199928644 ([Oxford](https://global.oup.com/academic/product/consciousness-and-the-social-brain-9780199928644)). Source vérifiée firsthand via WebSearch (3 sources concordantes : Oxford UP éditeur, Open Library, Google Books). **Lecture grade C.** AST = **schema simplifié** du propre état attentionnel (ce n'est pas une copie fidèle de l'attention, c'est un modèle **abstrait** comme le schéma corporel est abstrait). ICT `W_t` appliqué à `q̂(attention)` teste si la SAE features workspace porte une telle abstraction distinguable d'une représentation d'objet. **Honnêteté grade C** : (a) AST est une **théorie neurobiologique** (cerveau social, aires TPS, modèle d'autrui appliqué à soi) ; ICT est une **mesure de signal workspace** sur SAE features — l'isomorphisme formel n'est pas garanti (la workspace SAE peut **réellement** discriminer sans qu'on puisse l'identifier au schéma attentionnel de Graziano) ; (b) Graziano insiste sur le caractère **simplifié** du schéma (modèle interne, pas accès direct à l'attention réelle) — la mesure `propa(q̂(attention))` capture peut-être un proxy computationnel sans contenu phénoménal ; (c) l'orthogonalité stricte « attention/objet » est une **simplification** ICT (analogue à la simplification s � π pour Vervaeke, justifiée seulement comme point-zéro de falsification). La conjecture dérivable (jalon 3 #8182) — *chaque strate ICT = acquisition d'une adjonction* — pourrait ici se formuler : *AST comme adjonction du foncteur « perception → action », la workspace comme adjoint gauche*. **À explorer, grade C**, gated sur #7738 (tresse conceptuelle). **Distinction vs case 4** : case 4 (Metzinger self-model) mesure la **self-représentation** (qui suis-je ?) ; case 5 (Graziano AST) mesure le **schéma attentionnel** (où mon attention porte-t-elle ?) — deux self-représentations distinctes (l'**identité** vs l'**attention courante**). **Anti-confusion** : un signal AST `CONFIRMED` n'implique **pas** un signal self-model `CONFIRMED` (et réciproquement) — deux dimensions orthogonales du self, falsifiables indépendamment. | | **Frontière topologique ⟂ frontière fonctionnelle** (boundary problem) | `TESTÉ (INCONCLUSIF)` — première exécution `ict/kuramoto_boundary.py` — [#12179](https://github.com/jsboige/CoursIA/pull/12179) (po-2023, 2026-08-21) : median R_cross(TOPO) 0.108 [IC95 0.077–0.139] **hors bande** [0.25, 0.75] (le gradient de fréquences seul ferme déjà la frontière : CTRL 0.066–0.186), Δtopo médian +0.024 < 0.10, **2/5 graines valides** (défauts instables → rejet protocole). Détail : « Exécution c.431 » ci-dessous. | *(prédiction chiffrée détaillée case 6)* Sur un substrat de phase 2D (Kuramoto 64×64) portant des défauts topologiques stables (paires vortex-antivortex épinglées dans une bande de séparation entre deux régions de fréquences), la portée de l'intégration fonctionnelle (entrainement de phase par un kick) **franchit** la frontière topologique sans la respecter : `R_cross = propa(cross) / propa(within) ∈ [0.25, 0.75]` **ET** `Δtopo = R_cross(CTRL sans défauts) − R_cross(TOPO avec défauts) ≥ 0.10` — la segmentation topologique **ralentit** le binding fonctionnel sans l'**arrêter** (les deux frontières ne coïncident pas). | (a) `R_cross(TOPO) < 0.10` : le pocket topologique est réellement **fermé** — la lecture EM-fort (ce qui arrête le binding = la topologie) tient dans le jouet, les frontières coïncident, la dissociation est falsifiée ; (b) `Δtopo < ε_topo` (bruit de fond calibré) : l'atténuation est portée par le **gradient de couplage**, pas par les défauts — la topologie ne fait aucun travail de frontière au-delà de la géométrie du réseau. | Kuramoto 2D 64×64 **CPU-only** (lignée animat, sans dépendance GPU — cohérent « CPU-first ») | Gómez-Emilsson & Percy — *EM field topology & boundary problem* (L3 iceberg). Crédit : A. Gómez-Emilsson (Qualia Research Institute), C. Percy, « Don't forget the boundary problem! How EM field topology can address the overlooked cousin to the binding problem for consciousness », *Frontiers in Human Neuroscience* 17:1233119 (2023), DOI [10.3389/fnhum.2023.1233119](https://doi.org/10.3389/fnhum.2023.1233119). Vérifié firsthand via WebSearch (3 sources concordantes : Frontiers éditeur, PhilArchive, APA PsycNet). **Lecture grade C** détaillée ci-dessous (case 6). | | **Fenêtre de synchronie** (sync window — CEMI) | `TESTÉ (CONFIRMÉ)` — `ict/cemi_field.py` exécuté 2026-08-24 : R_E(f=1) 581 → 1.00 monotone (P1 ✓), R_E @f* = 1.86 ± 0.62 [5 seeds] (P2 ✓, bande recalibrée [1.2, 3.0] sur 40 seeds : mean 1.55, IQR [1.02, 1.90]), R_E sous f*/2 = 1.15/1.02 (P3 ✓ < 1.2), contrôle négatif énergie ×4 trivial / structure égale 1.00 — [#8182](https://github.com/jsboige/CoursIA/issues/8182) case 7 (po-2027, 2026-08-24). | *(prédiction chiffrée détaillée case 7 ci-dessous)* Sur une grille de N émetteurs dipolaires (somme complexe 1/r, puissance totale identique), l'énergie du champ relative au tir asynchrone R_E = ⟨\|E\|²⟩_f / ⟨\|E\|²⟩_async décroît **monotone** avec la fraction synchronisée (P1), passe par la bande **[1.2, 3.0]** (moyenne ≥ 5 seeds) au crossover f* = 1/√N (P2), et tombe sous **1.2** pour f < f*/2 (P3) — l'intégration de champ s'achète avec une population synchrone minimale, pas avec de l'énergie. | (a) non-monotonie de la courbe R_E(f) (une remontée locale = artefact de seuil ou de géométrie, pas une fenêtre) ; (b) R_E @f* hors bande sur ≥ 10 seeds (le crossover ne joue pas à 1/√N — la lecture « population minimale » est mal dérivée) ; (c) doubler la puissance en asynchrone produit R_E > 1 contre base même-puissance (l'effet est énergétique, pas structurel — tue la lecture CEMI du jouet). | Grille dipolaire 32×32 (N = 1024) **CPU-only**, numpy pur, 5 seeds par point (40 seeds pour la calibration P2) | McFadden — *CEMI field theory* (L4 iceberg). Crédit : Johnjoe McFadden, « Integrating information in the brain's EM field: the CEMI field theory of consciousness », *Neuroscience of Consciousness* 2020(1) niaa016, DOI [10.1093/nc/niaa016](https://doi.org/10.1093/nc/niaa016). Vérifié firsthand 2026-08-24. **Lecture grade C** détaillée ci-dessous (case 7). |