diff --git a/.github/workflows/ict-tests.yml b/.github/workflows/ict-tests.yml index 57a613b71a..f212be9ef9 100644 --- a/.github/workflows/ict-tests.yml +++ b/.github/workflows/ict-tests.yml @@ -223,10 +223,16 @@ jobs: # 783 (main pur, mesure) + 28 = 811 ferme l'arithmetique, et 811 # EST la collection mesuree sur l'arbre merge (py3.9.25 + # numpy 1.26.4, SANS torch -- la forme de l'env CI). - - suite-name: "ict/tests/ (44 package)" + # Floor 822 -> 846 (cette PR) : +24 items case 3 confabulation + # broadcast #8182 (ict/tests/test_confabulation_broadcast.py) -- + # 822 main pur + 24 = 846, mesure sur l'arbre MERGE (py3.9-slim, + # numpy 1.26.4 + scipy 1.13.1 + matplotlib 3.9.4 + pandas 2.3.3, + # SANS torch -- la forme de l'env CI) ; decomposition verifiee + # par collecte isolee du fichier (24). + - suite-name: "ict/tests/ (45 package)" test-cwd: MyIA.AI.Notebooks/IIT/ICT-Series/ict/tests test-args: . - test-floor: 822 + test-floor: 846 steps: - uses: actions/checkout@v4 diff --git a/MyIA.AI.Notebooks/IIT/ICT-Series/ict/confabulation_broadcast.py b/MyIA.AI.Notebooks/IIT/ICT-Series/ict/confabulation_broadcast.py new file mode 100644 index 0000000000..9811949d01 --- /dev/null +++ b/MyIA.AI.Notebooks/IIT/ICT-Series/ict/confabulation_broadcast.py @@ -0,0 +1,425 @@ +"""Diffusion workspace d'un q erroné — exécution du pré-enregistrement case 3 (#8182). + +La confabulation en une phrase : le système tient une représentation FAUSSE +d'une cible POURTANT connue et la diffuse en aval avec la même efficacité que +le vrai — l'erreur n'est pas un raté du broadcast, elle EST broadcastée : la +workspace « consacre » l'erreur (row 204 de la matrice de dissociations). + +Le pré-enregistrement (``docs/ict/dissociations-matrix.md`` §« Case 3 — W +diffuse un q erroné (confabulation) — pré-enregistrement chiffré », scellé +avant run) fixe AVANT le test : + +- la cible ``R_confab = propa(q̂_faux) / propa(q̂_vrai) ∈ [0.80, 1.00]`` ; +- le null adversarial RÉSIDUEL : ``ε_stable = 0.5 × (propa(q̂_vrai) − + propa(aléatoire))`` — l'écart faux vs aléatoire doit valoir au moins la + moitié de l'écart vrai vs aléatoire (« l'écart maximal que la propagation + peut porter ») pour parler de consécration SPÉCIFIQUE de l'erreur — sinon + la « consécration » n'est que du broadcast non-discriminant ; +- l'ordre anti-HARKing : la calibration (bras vrai + aléatoire) est mesurée + et GELÉE dans un JSON AVANT que le bras faux ne soit scoré. Le dénominateur + de R_confab ET ε_stable sont donc tous deux gelés avant la mesure + principale — plus strict que cases 4/5, où seule l'échelle était gelée ; +- 5 graines (0, 1, 7, 42, 99), 3 prompts par bras sur la banque de contextes + partagée (import ``CONTEXTS_BY_SEED`` — comparabilité cross-case). + +Choix de substrat (honnêteté, hérité de case 5) : le pré-enregistrement +nommait « SAETrajectoires strate 5 (Qwen2.5-3B) » ; l'exécution case 5 +(#15547) a posé le substrat réel exécutable et déclaré ses choix canoniques +« pour les cases 3 et 4 » : **Qwen3.5-2B-Base, couche 12/24, SAE Qwen-Scope +W32K-L0_50** via ``scripts/extract_sae_traces.py``. La case 3 reprend +EXACTEMENT ce substrat pour que R_confab soit comparable à R_attn (case 5) et +R_self (case 4). L'opérateur ``propa`` est importé TEL QUEL de +:mod:`ict.attention_schema` — la seule différence case 5 → case 3 est la +cible du prompt, jamais l'instrument. + +Opérationnalisation des bras (panel verrouillé avant test, matrice l.565) : +chaque prompt = un contexte amont de la banque partagée + une clause cible. +La clause vrai et la clause faux portent la MÊME cible connue (la carafe +d'eau) et ne diffèrent que par le mot de la propriété (claire/bleue) — +contre-factuel exact au niveau de la propriété, « la cible est connue, mais +q̂ ≠ cible ». La clause aléatoire porte un stimulus sans rapport à aucun +objet de la tâche (le tramway), « pas de représentation d'aucun objet de la +tâche » (matrice l.546). + +Grade C : la matrice n'identifie AUCUN hook grade C pour cette case (« aucun +hook identifié dans #9533 ») ; le banc teste si la workspace SAE consacre un +q̂ faux aussi efficacement qu'un q̂ vrai. Aucune phénoménologie clinique de +la confabulation n'est mesurée ni revendiquée. + +Référence pré-enregistrement : +``docs/ict/dissociations-matrix.md`` §« Case 3 — W diffuse un q erroné » +(l.539-585), scellé avant cette exécution (statut-cible c.1242). +""" + +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_VRAI", + "TASK_FAUX", + "TASK_ALEA", + "ARM_CALIB", + "ARM_MAIN", + "build_calibration_sets", + "build_prompt_sets", + "write_prompts_json", + "calibrate_epsilon_stable", + "apply_verdict_confab", + "score_main_arms", + "run_analysis", +] + +SEEDS = (0, 1, 7, 42, 99) + +# Clauses de tâche : la cible (la carafe d'eau) est identique et connue dans +# les bras vrai et faux — SEUL le mot de la propriété diffère (claire/bleue), +# « la cible est connue, mais q̂ ≠ cible » (matrice l.544). La clause +# aléatoire est un stimulus sans rapport à aucun objet de la tâche (l.546). +# Les contextes amont sont la banque de case 5 (import), partagée à +# l'identique par les trois bras. +TASK_VRAI = ("Décris la carafe d'eau posée sur la table : " + "l'eau qu'elle contient est claire, n'est-ce pas ?") +TASK_FAUX = ("Décris la carafe d'eau posée sur la table : " + "l'eau qu'elle contient est bleue, n'est-ce pas ?") +TASK_ALEA = "Décris le passage du tramway dans la rue : quel bruit fait-il ?" + +ARM_CALIB = ("target_vrai", "control_alea") +ARM_MAIN = ("confab_faux",) + + +# --------------------------------------------------------------------------- # +# 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 prompts du bras principal (faux) d'une graine. + + Le pré-enregistrement annonce « 3 prompts à représentation erronée + (faux) » : le bras principal est le bras faux SEUL — les bras vrai et + aléatoire appartiennent à la calibration, gelée avant le score (c'est la + structure anti-HARKing propre à la case 3 : dénominateur et ε_stable + sont tous deux gelés avant la mesure principale). + """ + contexts = CONTEXTS_BY_SEED[seed] + if len(contexts) != 3: + raise ValueError(f"seed {seed}: 3 contextes attendus, {len(contexts)} trouvés") + return {"confab_faux": [c + TASK_FAUX for c in contexts]} + + +def build_calibration_sets(seed: int) -> dict[str, list[str]]: + """Rend les 3+3 prompts de calibration (vrai + aléatoire, null résiduel).""" + contexts = CONTEXTS_BY_SEED[seed] + if len(contexts) != 3: + raise ValueError(f"seed {seed}: 3 contextes attendus, {len(contexts)} trouvés") + return { + "target_vrai": [c + TASK_VRAI for c in contexts], + "control_alea": [c + TASK_ALEA for c in contexts], + } + + +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 résiduelle du null adversarial (GPU-free, sur traces .npz) +# --------------------------------------------------------------------------- # +@dataclass +class Calibration: + """Null adversarial calibré AVANT le test principal (anti-HARKing). + + ``gap_by_seed`` ne contient que les graines exploitables (gap > 0) ; une + graine à gap ≤ 0 est dégénérée — l'instrument n'y porte aucun écart vrai + vs aléatoire, elle ne peut pas témoigner d'une consécration et compte + comme non-passante au critère 5/5 (jamais imputée). + """ + + propa_vrai_by_seed: dict[int, float] + propa_alea_by_seed: dict[int, float] + gap_by_seed: dict[int, float] = field(default_factory=dict) + epsilon_stable_by_seed: dict[int, float] = field(default_factory=dict) + epsilon_stable_median: float | None = None + notes: list[str] = field(default_factory=list) + + +def calibrate_epsilon_stable(calib_traces_by_seed: dict[int, dict], + *, k_features: int = 64) -> Calibration: + """ε_stable = 0.5 × (propa(vrai) − propa(aléatoire)), par graine puis médian. + + 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 en deux phases). La formule est RESIDUELLE, scellée + par le pré-enregistrement : l'écart faux vs aléatoire devra valoir au + moins la moitié de l'écart vrai vs aléatoire mesuré ici. + """ + calib = Calibration(propa_vrai_by_seed={}, propa_alea_by_seed={}) + for seed, traces in sorted(calib_traces_by_seed.items()): + p_v = propa(traces, "target_vrai", k_features=k_features) + p_a = propa(traces, "control_alea", k_features=k_features) + calib.propa_vrai_by_seed[seed] = p_v + calib.propa_alea_by_seed[seed] = p_a + gap = p_v - p_a + if gap <= 0.0: + calib.notes.append( + f"graine {seed}: gap propa(vrai)−propa(aléatoire) = {gap:.4f} ≤ 0 — " + "calibration dégénérée : la graine ne peut pas témoigner d'une " + "consécration (comptée non-passante au critère 5/5, jamais imputée)") + continue + calib.gap_by_seed[seed] = gap + calib.epsilon_stable_by_seed[seed] = 0.5 * gap + if len(calib.gap_by_seed) < 3: + raise ValueError(f"calibration invalide : {len(calib.gap_by_seed)} graines " + "exploitables (<3) — l'instrument ne permet pas de fixer " + "ε_stable, verdict INCONCLUSIF par construction") + calib.epsilon_stable_median = float(np.median(list(calib.epsilon_stable_by_seed.values()))) + return calib + + +# --------------------------------------------------------------------------- # +# Table de verdict multi-niveau (scellée par le pré-enregistrement) +# --------------------------------------------------------------------------- # +def apply_verdict_confab(r_by_seed: dict[int, float], + passes_by_seed: dict[int, bool]) -> tuple[str, list[str]]: + """Applique EXACTEMENT la table de verdict multi-niveau du pré-enregistrement. + + - **Rejet** : médiane(R_confab) < 0.80 → FALSIFIED (la workspace DISTINGUE + le faux du vrai et pénalise l'erreur) ; + - **Agrégé** : médiane ∈ [0.80, 1.00] ET discrimination ≥ ε_stable sur + 5/5 graines → CONFIRMED (la workspace CONSACRE l'erreur) ; + - **Partiel** : médiane ∈ [0.80, 1.00] MAIS ≥ 1 graine sous ε_stable → + INCONCLUSIF (null adversarial partiel : broadcast non-discriminant) ; + - médiane > 1.00 : hors table scellée → INCONCLUSIF, signalé comme tel + (la section Lectures du pré-enregistrement y lirait une consécration a + fortiori — R ≥ 0.80 — mais la table agrégée exige médiane ∈ [0.80, + 1.00] ; aucun verdict n'est inventé, la bande de prudence ± 0.05 de la + matrice est rappelée). + """ + values = np.asarray(list(r_by_seed.values()), dtype=float) + r_med = float(np.median(values)) + kills: list[str] = [] + if r_med < 0.80: + if r_med >= 0.75: + kills.append(f"médiane {r_med:.3f} dans la bande de prudence " + "[0.75, 0.80) (tolérance ± 0.05, matrice) — Rejet " + "annoncé, pas un échec franc") + return "FALSIFIED", kills + if r_med > 1.00: + prudence = " (bande de prudence (1.00, 1.05])" if r_med <= 1.05 else "" + kills.append( + f"médiane {r_med:.3f} > 1.00 : hors table de verdict scellée — la " + "section Lectures y lirait une consécration a fortiori (R ≥ 0.80), " + f"la table agrégée exige médiane ∈ [0.80, 1.00]{prudence} ; rapporté " + "sans inventer de verdict") + return "INCONCLUSIF", kills + n_fail = sum(1 for p in passes_by_seed.values() if not p) + if n_fail: + failed = sorted(s for s, p in passes_by_seed.items() if not p) + kills.append( + f"discrimination < ε_stable sur {n_fail}/{len(passes_by_seed)} graines " + f"(graines {failed}) — la « consécration » n'est pas de la consécration : " + "broadcast non-discriminant, null adversarial partiel") + return "INCONCLUSIF", kills + return "CONFIRMED", [] + + +# --------------------------------------------------------------------------- # +# Scoreboard final +# --------------------------------------------------------------------------- # +@dataclass +class Scoreboard: + """Scoreboard pré-enregistré : médiane, IC95 bootstrap, discrimination 5/5.""" + + r_confab_by_seed: dict[int, float] + propa_faux_by_seed: dict[int, float] + discrimination_by_seed: dict[int, float] + passes_by_seed: dict[int, bool] + r_median: float + ci95: tuple[float, float] + epsilon_stable_median: 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_confab par graine, IC95 bootstrap n=200, verdict. + + Exigence anti-HARKing : ``calibration.epsilon_stable_median`` doit être + gelé (le fichier de calibration produit par la phase 1 doit exister et + être chargé) AVANT le score du bras faux — le CLI refuse l'ordre inverse. + Le dénominateur ``propa(q̂_vrai)`` vient des traces de calibration gelées, + pas d'une re-mesure au moment du score. + """ + if calibration.epsilon_stable_median is None or not calibration.epsilon_stable_by_seed: + raise ValueError("ε_stable non gelé : la calibration résiduelle doit être " + "complétée (propa(vrai), propa(aléatoire) et ε_stable " + "écrits dans le JSON de calibration) AVANT le score du " + "bras faux — anti-HARKing.") + r_by_seed: dict[int, float] = {} + propa_faux: dict[int, float] = {} + disc_by_seed: dict[int, float] = {} + passes: dict[int, bool] = {} + for seed, traces in sorted(main_traces_by_seed.items()): + if seed not in calibration.propa_vrai_by_seed: + raise ValueError(f"graine {seed} absente de la calibration gelée — " + "les traces de calibration et du bras faux doivent " + "couvrir les mêmes graines") + p_v = calibration.propa_vrai_by_seed[seed] + p_a = calibration.propa_alea_by_seed[seed] + if p_v <= 0.0: + raise ValueError(f"graine {seed}: propa(q̂_vrai)={p_v} ≤ 0 — R_confab " + "non défini, graine à documenter comme échec de " + "mesure, jamais à imputer") + p_f = propa(traces, "confab_faux", k_features=k_features) + r_by_seed[seed] = p_f / p_v + propa_faux[seed] = p_f + disc_by_seed[seed] = p_f - p_a + eps = calibration.epsilon_stable_by_seed.get(seed) + passes[seed] = eps is not None and disc_by_seed[seed] >= eps + if len(r_by_seed) < 3: + raise ValueError(f"score invalide : {len(r_by_seed)} graines exploitables " + "(<3) — verdict INCONCLUSIF par construction") + + 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))) + + verdict, kills = apply_verdict_confab(r_by_seed, passes) + return Scoreboard( + r_confab_by_seed=r_by_seed, + propa_faux_by_seed=propa_faux, + discrimination_by_seed=disc_by_seed, + passes_by_seed=passes, + r_median=float(np.median(values)), + ci95=ci95, + epsilon_stable_median=calibration.epsilon_stable_median, + verdict=verdict, + kill_details=kills, + ) + + +def run_analysis(calibration: Calibration, + main_traces_by_seed: dict[int, dict], *, k_features: int = 64) -> dict: + """Score le bras faux contre la calibration GELÉE et rend le dictionnaire + complet sérialisable pour ``ict/results/confabulation_broadcast_results.json``.""" + board = score_main_arms(main_traces_by_seed, calibration, k_features=k_features) + return { + "case": "case3_confabulation_broadcast", + "prediction": ("R_confab = propa(q̂_faux)/propa(q̂_vrai) ∈ [0.80, 1.00] ET " + "propa(q̂_faux) − propa(aléatoire) ≥ ε_stable (5/5 graines)"), + "substrate": ("Qwen3.5-2B-Base, couche 12/24, SAE Qwen-Scope W32K-L0_50 " + "(top-k=50) — même substrat que case 5 #15547 et case 4 " + "#15879 ; le pré-enregistrement nommait SAETrajectoires " + "strate 5 (Qwen2.5-3B), case 5 a posé le substrat réel " + "exécutable et déclaré l'instrument canonique pour les " + "cases 3 et 4"), + "calibration": { + "propa_vrai_by_seed": calibration.propa_vrai_by_seed, + "propa_alea_by_seed": calibration.propa_alea_by_seed, + "gap_by_seed": calibration.gap_by_seed, + "epsilon_stable_by_seed": calibration.epsilon_stable_by_seed, + "epsilon_stable_median": calibration.epsilon_stable_median, + "formula": "ε_stable = 0.5 × (propa(q̂_vrai) − propa(aléatoire)) — " + "calibration RÉSIDUELLE gelée AVANT le bras faux", + "notes": calibration.notes, + }, + "main": { + "r_confab_by_seed": board.r_confab_by_seed, + "propa_faux_by_seed": board.propa_faux_by_seed, + "discrimination_by_seed": board.discrimination_by_seed, + "passes_by_seed": board.passes_by_seed, + "r_median": board.r_median, + "ci95_bootstrap_n200": list(board.ci95), + }, + "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.confabulation_broadcast prompts --seed 0 --out-calib c.json + --out-main m.json`` écrit les jeux de prompts pour + ``extract_sae_traces.py --prompts-json`` (calibration = vrai + aléatoire, + 6 prompts ; main = faux, 3 prompts). ``calibrate --out + calib.json`` gèle propa(vrai), propa(aléatoire) et ε_stable ; + ``score --calibration calib.json`` refuse de tourner sans ce fichier — + garde anti-HARKing opérationnelle (l'ordre inverse est impossible, pas + juste déconseillé). Les clés de graine des JSON sont les ORDINAUX des + fichiers passés (même convention que cases 4/5) : passer les 5 .npz dans + l'ordre des graines 0, 1, 7, 42, 99. + """ + import argparse + + from .sae_traces import load_traces + + ap = argparse.ArgumentParser(description="Case 3 (#8182) — confabulation broadcast") + 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-calib", required=True, + help="JSON calibration (vrai + aléatoire, 6 prompts)") + g.add_argument("--out-main", required=True, + help="JSON bras principal (faux, 3 prompts)") + c = sub.add_parser("calibrate", help="phase 1 : ε_stable gelé avant le bras faux") + c.add_argument("traces", nargs="+", help=".npz de calibration (5 graines, en ordre)") + c.add_argument("--out", required=True, help="JSON de calibration figé") + s = sub.add_parser("score", help="phase 2 : bras faux + verdict") + s.add_argument("traces", nargs="+", help=".npz du bras faux (5 graines, en ordre)") + 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_calib, calibration=True) + write_prompts_json(args.seed, args.out_main, calibration=False) + print(f"[prompts] seed {args.seed} -> {args.out_calib} + {args.out_main}") + return + if args.cmd == "calibrate": + calib = calibrate_epsilon_stable( + {i: load_traces(p) for i, p in enumerate(args.traces)}) + Path(args.out).write_text(json.dumps({ + "propa_vrai_by_seed": calib.propa_vrai_by_seed, + "propa_alea_by_seed": calib.propa_alea_by_seed, + "gap_by_seed": calib.gap_by_seed, + "epsilon_stable_by_seed": calib.epsilon_stable_by_seed, + "epsilon_stable_median": calib.epsilon_stable_median, + "notes": calib.notes, + }, ensure_ascii=False, indent=1), encoding="utf-8", newline="\n") + print(f"[calibrate] epsilon_stable_median={calib.epsilon_stable_median:.6f} " + f"-> {args.out}") + return + frozen = json.loads(Path(args.calibration).read_text(encoding="utf-8")) + calib = Calibration( + propa_vrai_by_seed={int(k): v for k, v in frozen["propa_vrai_by_seed"].items()}, + propa_alea_by_seed={int(k): v for k, v in frozen["propa_alea_by_seed"].items()}, + gap_by_seed={int(k): v for k, v in frozen["gap_by_seed"].items()}, + epsilon_stable_by_seed={int(k): v + for k, v in frozen["epsilon_stable_by_seed"].items()}, + epsilon_stable_median=(float(frozen["epsilon_stable_median"]) + if frozen["epsilon_stable_median"] is not None else None), + 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/results/confabulation_broadcast_results.json b/MyIA.AI.Notebooks/IIT/ICT-Series/ict/results/confabulation_broadcast_results.json new file mode 100644 index 0000000000..54ccfe76b6 --- /dev/null +++ b/MyIA.AI.Notebooks/IIT/ICT-Series/ict/results/confabulation_broadcast_results.json @@ -0,0 +1,51 @@ +{ + "case": "case3_confabulation_broadcast", + "prediction": "R_confab = propa(q̂_faux)/propa(q̂_vrai) ∈ [0.80, 1.00] ET propa(q̂_faux) − propa(aléatoire) ≥ ε_stable (5/5 graines)", + "substrate": "Qwen3.5-2B-Base, couche 12/24, SAE Qwen-Scope W32K-L0_50 (top-k=50) — même substrat que case 5 #15547 et case 4 #15879 ; le pré-enregistrement nommait SAETrajectoires strate 5 (Qwen2.5-3B), case 5 a posé le substrat réel exécutable et déclaré l'instrument canonique pour les cases 3 et 4", + "execution": { + "date": "2026-09-13", + "lane": "myia-po-2024:CoursIA", + "extraction": "GPU réelle locale (RTX 3070, venv ft03-gpu), scripts/extract_sae_traces.py INCHANGÉ, 10 NPZ : traces/case3calib_s{0,1,7,42,99}_*.npz (bras target_vrai + control_alea) puis traces/case3_s{0,1,7,42,99}_*.npz (bras confab_faux)", + "seeds": [0, 1, 7, 42, 99], + "anti_harking": "les bras de calibration (vrai + aléatoire) ont été extraits et mesurés AVANT le score du bras faux — le dénominateur de R_confab ET ε_stable sont tous deux gelés avant la mesure principale (garde mécanique : ict.confabulation_broadcast.score refuse l'ordre inverse)" + }, + "calibration": { + "propa_vrai_by_seed": {"0": 1.0, "1": 0.9333, "7": 0.8, "42": 0.8, "99": 0.8}, + "propa_alea_by_seed": {"0": 1.0, "1": 0.9333, "7": 0.8667, "42": 0.8667, "99": 0.8}, + "gap_by_seed": {"0": 0.0, "1": 0.0, "7": -0.0667, "42": -0.0667, "99": 0.0}, + "graines_exploitables": 0, + "epsilon_stable_by_seed": {}, + "epsilon_stable_median": null, + "formula": "ε_stable = 0.5 × (propa(q̂_vrai) − propa(aléatoire)) — calibration RÉSIDUELLE gelée AVANT le bras faux", + "module_refusal": "ValueError: calibration invalide : 0 graines exploitables (<3) — l'instrument ne permet pas de fixer ε_stable, verdict INCONCLUSIF par construction", + "notes": [ + "graine 0: gap = +0.0000 ≤ 0 — calibration dégénérée : la graine ne peut pas témoigner d'une consécration (comptée non-passante au critère 5/5, jamais imputée)", + "graine 1: gap = +0.0000 ≤ 0 — idem", + "graine 7: gap = −0.0667 ≤ 0 (propa(aléatoire) STRICTEMENT supérieur à propa(vrai)) — idem", + "graine 42: gap = −0.0667 ≤ 0 — idem", + "graine 99: gap = +0.0000 ≤ 0 — idem" + ] + }, + "verdict": "INCONCLUSIF", + "verdict_detail": "PAR CONSTRUCTION — la calibration résiduelle est dégénérée (0/5 graines exploitables, gap propa(vrai)−propa(aléatoire) ≤ 0 partout) : l'ignition vraie ne propage PAS mieux qu'un stimulus aléatoire, ε_stable n'est pas fixable, la branche Agrégé [0.80, 1.00] de la table scellée n'est PAS ÉVALUABLE. L'alternative (forcer ε_stable := 0) serait une confirmation vacue : elle certifierait le broadcast, pas la consécration de l'erreur — c'est exactement ce que le garde scellé du module refuse.", + "kill_details": [ + "calibration dégénérée 0/5 graines — ε_stable non fixable, verdict INCONCLUSIF par construction", + "aucune donnée n'a été imputée ni re-mesurée après consultation du bras faux (ordre anti-HARKing respecté)" + ], + "diagnostic_posthoc_non_scellé": { + "avertissement": "bloc descriptif calculé APRÈS le refus de la calibration, hors protocole scellé — ne porte aucun verdict", + "propa_faux_by_seed": {"0": 1.0, "1": 0.9333, "7": 0.8, "42": 0.8, "99": 0.8667}, + "r_confab_by_seed": {"0": 1.0, "1": 1.0, "7": 1.0, "42": 1.0, "99": 1.0833}, + "r_median": 1.0, + "lecture_ratio": "propa(faux) = propa(vrai) seed-par-seed (égalité exacte 4/5, +1/15 sur la graine 99) — R_confab médian 1.000, même valeur que R_self (case 4, #15879)", + "jaccard_top64": { + "vrai_faux_par_graine": [0.882, 0.91, 0.882, 0.882, 0.855], + "vrai_alea_par_graine": [0.471, 0.471, 0.542, 0.407, 0.407], + "faux_alea_par_graine": [0.455, 0.455, 0.506, 0.422, 0.376], + "moyennes": {"vrai_faux": 0.883, "vrai_alea": 0.46, "faux_alea": 0.443}, + "lecture": "les SIGNATURES sont sensibles au contenu (J(vrai,faux)=0.883 ≫ J(vrai,aléa)=0.460 : la carafe et le tramway séparent bien) mais le TAUX propa sature (~0.80–1.00) pour chaque bras : consommateurs et signature top-64 dérivent des MÊMES prompts, chaque bras s'auto-propage dans le nuage de features contextuelles — la signature discrimine, le taux de propagation ne discrimine pas" + }, + "cause_instrumentale": "même plafond que case 4 (R_self 1.000, Jaccard self/autrui 0.61) et case 5 (#15547) : l'opérateur propa sur top-64 de l'activation moyenne sature en régime broadcast quasi-uniforme", + "comparabilite_cross_case": "R_attn (case 5) INCONCLUSIF · R_self (case 4) 1.000 INCONCLUSIF · R_confab (case 3) 1.000 INCONCLUSIF — plafond instrumental commun aux trois cases W-diffusion/self, pas une convergence de contenu" + } +} diff --git a/MyIA.AI.Notebooks/IIT/ICT-Series/ict/tests/test_confabulation_broadcast.py b/MyIA.AI.Notebooks/IIT/ICT-Series/ict/tests/test_confabulation_broadcast.py new file mode 100644 index 0000000000..dd4192e546 --- /dev/null +++ b/MyIA.AI.Notebooks/IIT/ICT-Series/ict/tests/test_confabulation_broadcast.py @@ -0,0 +1,282 @@ +"""Tests du banc case 3 — confabulation broadcast (#8182). + +Les seuils viennent du pré-enregistrement scellé dans +``docs/ict/dissociations-matrix.md`` (case 3, §l.539) : bande [0.80, 1.00], +null adversarial RÉSIDUEL ε_stable = 0.5 × (propa(vrai) − propa(aléatoire)) +calibré et gelé AVANT le bras faux, critère 5/5 graines. + +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 vrai/faux/aléatoire, la formule de +calibration résiduelle et CHAQUE branche de la table de verdict multi-niveau +(y compris la branche honnête « médiane > 1.00 : hors table scellée »). +""" + +from __future__ import annotations + +import json +import re + +import numpy as np +import pytest + +from ict.attention_schema import CONTEXTS_BY_SEED, propa +from ict.confabulation_broadcast import ( + ARM_CALIB, + ARM_MAIN, + SEEDS, + TASK_ALEA, + TASK_FAUX, + TASK_VRAI, + Calibration, + apply_verdict_confab, + build_calibration_sets, + build_prompt_sets, + calibrate_epsilon_stable, + run_analysis, + score_main_arms, +) + + +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 / test_self_model_minimal + (la formule propa est inchangée — c'est ce que la case 3 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 TestPromptsVerrouilles: + def test_calibration_six_prompts_deux_bras(self): + for seed in SEEDS: + calib = build_calibration_sets(seed) + assert set(calib) == set(ARM_CALIB) + assert all(len(v) == 3 for v in calib.values()) + + def test_main_trois_prompts_un_seul_bras(self): + for seed in SEEDS: + main = build_prompt_sets(seed) + assert set(main) == set(ARM_MAIN) + assert len(main["confab_faux"]) == 3 + + def test_contexte_partage_identique_dans_chaque_triplet(self): + """Contre-factuels exacts : même contexte amont pour vrai/faux/aléatoire.""" + for seed in SEEDS: + calib = build_calibration_sets(seed) + main = build_prompt_sets(seed) + for v, a, f in zip(calib["target_vrai"], calib["control_alea"], + main["confab_faux"]): + ctx_v = v[: -len(TASK_VRAI)] + ctx_a = a[: -len(TASK_ALEA)] + ctx_f = f[: -len(TASK_FAUX)] + assert ctx_v == ctx_a == ctx_f + + def test_vrai_faux_ne_differencent_que_par_la_propriete(self): + """« La cible est connue, mais q̂ ≠ cible » : un seul mot diffère.""" + w_v, w_f = TASK_VRAI.split(), TASK_FAUX.split() + assert len(w_v) == len(w_f) + diffs = [(a, b) for a, b in zip(w_v, w_f) if a != b] + assert len(diffs) == 1 + assert diffs[0] == ("claire,", "bleue,") + + def test_aleatoire_sans_rapport_aux_objets_de_la_tache(self): + """Le contrôle ne représente AUCUN objet de la tâche (matrice l.546).""" + tache = (TASK_VRAI + " " + TASK_FAUX).lower() + for mot in ("carafe", "eau", "table", "claire", "bleue"): + assert mot in tache + assert not re.search(rf"\b{mot}\b", TASK_ALEA.lower()) + + 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()) + + +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.confabulation_broadcast import propa as propa_case3 + assert propa_case3 is propa + + def test_bras_absent_refuse(self): + traces = _arm_traces({"target_vrai": 8}) + with pytest.raises(ValueError, match="absent"): + propa(traces, "confab_faux", 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) + prompts = {( "target_vrai", 0): {"ids": ids, "vals": vals, + "tokens": np.array(["x"] * 4)}} + with pytest.raises(ValueError, match="trop courte"): + propa({"meta": {"d_sae": 8, "k": 3}, "prompts": prompts}, + "target_vrai", k_features=2) + + +class TestCalibrationResiduelle: + def test_formule_epsilon_stable(self): + """propa(vrai)=0.8, propa(aléa)=0.1 → gap 0.7, ε_stable=0.35, par graine.""" + traces = {s: _arm_traces({"target_vrai": 8, "control_alea": 1}) + for s in range(5)} + calib = calibrate_epsilon_stable(traces, k_features=2) + for s in range(5): + assert calib.propa_vrai_by_seed[s] == pytest.approx(0.8, abs=0.02) + assert calib.propa_alea_by_seed[s] == pytest.approx(0.1, abs=0.02) + assert calib.gap_by_seed[s] == pytest.approx(0.7, abs=0.04) + assert calib.epsilon_stable_by_seed[s] == pytest.approx(0.35, abs=0.02) + assert calib.epsilon_stable_median == pytest.approx(0.35, abs=0.02) + assert not calib.notes + + def test_graine_degeneree_notee_mais_pas_imputee(self): + """Gap ≤ 0 : graine exclue d'ε_stable, notée, jamais fabriquee.""" + traces = {0: _arm_traces({"target_vrai": 2, "control_alea": 8}), # gap < 0 + 1: _arm_traces({"target_vrai": 8, "control_alea": 1}), + 2: _arm_traces({"target_vrai": 9, "control_alea": 1}), + 3: _arm_traces({"target_vrai": 7, "control_alea": 2}), + 4: _arm_traces({"target_vrai": 8, "control_alea": 2})} + calib = calibrate_epsilon_stable(traces, k_features=2) + assert 0 not in calib.gap_by_seed + assert any("dégénérée" in n for n in calib.notes) + + def test_moins_de_3_graines_exploitables_refuse(self): + traces = {0: _arm_traces({"target_vrai": 2, "control_alea": 8}), + 1: _arm_traces({"target_vrai": 2, "control_alea": 8}), + 2: _arm_traces({"target_vrai": 2, "control_alea": 8})} + with pytest.raises(ValueError, match="<3"): + calibrate_epsilon_stable(traces, k_features=2) + + def test_score_refuse_sans_calibration_gelee(self): + """Garde anti-HARKing : le bras faux ne se score pas sans ε_stable gelé.""" + calib = Calibration(propa_vrai_by_seed={s: 0.8 for s in range(5)}, + propa_alea_by_seed={s: 0.1 for s in range(5)}) + with pytest.raises(ValueError, match="anti-HARKing"): + score_main_arms({}, calib, k_features=2) + + def test_graine_hors_calibration_refusee(self): + calib = calibrate_epsilon_stable( + {s: _arm_traces({"target_vrai": 8, "control_alea": 1}) + for s in range(5)}, k_features=2) + main = {9: _arm_traces({"confab_faux": 8})} # graine inconnue de la calib + with pytest.raises(ValueError, match="absente de la calibration"): + score_main_arms(main, calib, k_features=2) + + +class TestVerdict: + PASSED = {s: True for s in range(5)} + + def test_confirmed_dans_la_bande(self): + r = {s: 0.9 for s in range(5)} + verdict, kills = apply_verdict_confab(r, self.PASSED) + assert verdict == "CONFIRMED" and not kills + + def test_confirmed_frontiere_inclusive(self): + r = {s: 1.0 for s in range(5)} + verdict, _ = apply_verdict_confab(r, self.PASSED) + assert verdict == "CONFIRMED" + + def test_falsified_rejet_sous_080(self): + r = {s: 0.6 for s in range(5)} + verdict, kills = apply_verdict_confab(r, self.PASSED) + assert verdict == "FALSIFIED" and not kills + + def test_falsified_bande_de_prudence_annotee(self): + r = {s: 0.77 for s in range(5)} + verdict, kills = apply_verdict_confab(r, self.PASSED) + assert verdict == "FALSIFIED" + assert any("prudence" in k for k in kills) + + def test_inconclusif_partiel_une_graine_sous_epsilon(self): + r = {s: 0.9 for s in range(5)} + passes = dict(self.PASSED) + passes[3] = False + verdict, kills = apply_verdict_confab(r, passes) + assert verdict == "INCONCLUSIF" + assert any("null adversarial partiel" in k for k in kills) + + def test_inconclusif_mediane_hors_table_superieure(self): + """Médiane > 1.00 : la table scellée n'a pas de branche — honnête.""" + r = {s: 1.1 for s in range(5)} + verdict, kills = apply_verdict_confab(r, self.PASSED) + assert verdict == "INCONCLUSIF" + assert any("hors table" in k for k in kills) + assert any("1.00" in k for k in kills) + + def test_hors_table_proche_annote_bande_de_prudence(self): + r = {s: 1.03 for s in range(5)} + verdict, kills = apply_verdict_confab(r, self.PASSED) + assert verdict == "INCONCLUSIF" + assert any("bande de prudence (1.00, 1.05]" in k for k in kills) + + +class TestRunAnalysis: + def test_ordre_calibration_puis_score_serialisable(self): + calib = calibrate_epsilon_stable( + {s: _arm_traces({"target_vrai": 8, "control_alea": 1}) + for s in range(5)}, k_features=2) + # propa(faux) = 0.8 = propa(vrai) → R_confab = 1.0 (frontière), + # disc = 0.8 − 0.1 = 0.7 ≥ ε_stable 0.35 → CONFIRMED. + main = {s: _arm_traces({"confab_faux": 8}) for s in range(5)} + result = run_analysis(calib, main, k_features=2) + assert set(result) >= {"case", "substrate", "calibration", "main", "verdict"} + assert result["case"] == "case3_confabulation_broadcast" + assert result["verdict"] == "CONFIRMED" + assert result["main"]["r_median"] == pytest.approx(1.0, abs=0.03) + assert result["calibration"]["epsilon_stable_median"] == pytest.approx( + 0.35, abs=0.02) + assert all(result["main"]["passes_by_seed"].values()) + json.dumps(result) # sérialisable pour results/ + + def test_rapport_exact_faux_sous_vrai(self): + """R_confab réalisé exactement : faux 4/10, vrai 8/10 → 0.5 → FALSIFIED.""" + calib = calibrate_epsilon_stable( + {s: _arm_traces({"target_vrai": 8, "control_alea": 1}) + for s in range(5)}, k_features=2) + main = {s: _arm_traces({"confab_faux": 4}) for s in range(5)} + result = run_analysis(calib, main, k_features=2) + assert result["verdict"] == "FALSIFIED" + for r in result["main"]["r_confab_by_seed"].values(): + assert r == pytest.approx(0.5, abs=0.03) + + def test_graine_degeneree_comptee_non_passante(self): + """Une graine à calibration dégénérée fait échouer le 5/5 même en bande.""" + traces = {0: _arm_traces({"target_vrai": 2, "control_alea": 8}), # dégénérée + 1: _arm_traces({"target_vrai": 8, "control_alea": 1}), + 2: _arm_traces({"target_vrai": 9, "control_alea": 1}), + 3: _arm_traces({"target_vrai": 7, "control_alea": 2}), + 4: _arm_traces({"target_vrai": 8, "control_alea": 2})} + calib = calibrate_epsilon_stable(traces, k_features=2) + main = {s: _arm_traces({"confab_faux": 8}) for s in range(5)} + result = run_analysis(calib, main, k_features=2) + assert result["main"]["passes_by_seed"][0] is False + assert result["verdict"] == "INCONCLUSIF" diff --git a/MyIA.AI.Notebooks/IIT/ICT-Series/traces/case3_s0_qwen35-2b-base_layer12of24_trained.npz b/MyIA.AI.Notebooks/IIT/ICT-Series/traces/case3_s0_qwen35-2b-base_layer12of24_trained.npz new file mode 100644 index 0000000000..fba4f98291 Binary files /dev/null and b/MyIA.AI.Notebooks/IIT/ICT-Series/traces/case3_s0_qwen35-2b-base_layer12of24_trained.npz differ diff --git a/MyIA.AI.Notebooks/IIT/ICT-Series/traces/case3_s1_qwen35-2b-base_layer12of24_trained.npz b/MyIA.AI.Notebooks/IIT/ICT-Series/traces/case3_s1_qwen35-2b-base_layer12of24_trained.npz new file mode 100644 index 0000000000..7164c0175a Binary files /dev/null and b/MyIA.AI.Notebooks/IIT/ICT-Series/traces/case3_s1_qwen35-2b-base_layer12of24_trained.npz differ diff --git a/MyIA.AI.Notebooks/IIT/ICT-Series/traces/case3_s42_qwen35-2b-base_layer12of24_trained.npz b/MyIA.AI.Notebooks/IIT/ICT-Series/traces/case3_s42_qwen35-2b-base_layer12of24_trained.npz new file mode 100644 index 0000000000..d10a64b73f Binary files /dev/null and b/MyIA.AI.Notebooks/IIT/ICT-Series/traces/case3_s42_qwen35-2b-base_layer12of24_trained.npz differ diff --git a/MyIA.AI.Notebooks/IIT/ICT-Series/traces/case3_s7_qwen35-2b-base_layer12of24_trained.npz b/MyIA.AI.Notebooks/IIT/ICT-Series/traces/case3_s7_qwen35-2b-base_layer12of24_trained.npz new file mode 100644 index 0000000000..bd525f0c53 Binary files /dev/null and b/MyIA.AI.Notebooks/IIT/ICT-Series/traces/case3_s7_qwen35-2b-base_layer12of24_trained.npz differ diff --git a/MyIA.AI.Notebooks/IIT/ICT-Series/traces/case3_s99_qwen35-2b-base_layer12of24_trained.npz b/MyIA.AI.Notebooks/IIT/ICT-Series/traces/case3_s99_qwen35-2b-base_layer12of24_trained.npz new file mode 100644 index 0000000000..547ea08a28 Binary files /dev/null and b/MyIA.AI.Notebooks/IIT/ICT-Series/traces/case3_s99_qwen35-2b-base_layer12of24_trained.npz differ diff --git a/MyIA.AI.Notebooks/IIT/ICT-Series/traces/case3calib_s0_qwen35-2b-base_layer12of24_trained.npz b/MyIA.AI.Notebooks/IIT/ICT-Series/traces/case3calib_s0_qwen35-2b-base_layer12of24_trained.npz new file mode 100644 index 0000000000..7f5b3d6807 Binary files /dev/null and b/MyIA.AI.Notebooks/IIT/ICT-Series/traces/case3calib_s0_qwen35-2b-base_layer12of24_trained.npz differ diff --git a/MyIA.AI.Notebooks/IIT/ICT-Series/traces/case3calib_s1_qwen35-2b-base_layer12of24_trained.npz b/MyIA.AI.Notebooks/IIT/ICT-Series/traces/case3calib_s1_qwen35-2b-base_layer12of24_trained.npz new file mode 100644 index 0000000000..baf4a219dc Binary files /dev/null and b/MyIA.AI.Notebooks/IIT/ICT-Series/traces/case3calib_s1_qwen35-2b-base_layer12of24_trained.npz differ diff --git a/MyIA.AI.Notebooks/IIT/ICT-Series/traces/case3calib_s42_qwen35-2b-base_layer12of24_trained.npz b/MyIA.AI.Notebooks/IIT/ICT-Series/traces/case3calib_s42_qwen35-2b-base_layer12of24_trained.npz new file mode 100644 index 0000000000..1ab024bc97 Binary files /dev/null and b/MyIA.AI.Notebooks/IIT/ICT-Series/traces/case3calib_s42_qwen35-2b-base_layer12of24_trained.npz differ diff --git a/MyIA.AI.Notebooks/IIT/ICT-Series/traces/case3calib_s7_qwen35-2b-base_layer12of24_trained.npz b/MyIA.AI.Notebooks/IIT/ICT-Series/traces/case3calib_s7_qwen35-2b-base_layer12of24_trained.npz new file mode 100644 index 0000000000..3b81edd22b Binary files /dev/null and b/MyIA.AI.Notebooks/IIT/ICT-Series/traces/case3calib_s7_qwen35-2b-base_layer12of24_trained.npz differ diff --git a/MyIA.AI.Notebooks/IIT/ICT-Series/traces/case3calib_s99_qwen35-2b-base_layer12of24_trained.npz b/MyIA.AI.Notebooks/IIT/ICT-Series/traces/case3calib_s99_qwen35-2b-base_layer12of24_trained.npz new file mode 100644 index 0000000000..ac57cab93b Binary files /dev/null and b/MyIA.AI.Notebooks/IIT/ICT-Series/traces/case3calib_s99_qwen35-2b-base_layer12of24_trained.npz differ diff --git a/docs/ict/dissociations-matrix.md b/docs/ict/dissociations-matrix.md index b1a0cc7402..3d826efad5 100644 --- a/docs/ict/dissociations-matrix.md +++ b/docs/ict/dissociations-matrix.md @@ -202,7 +202,7 @@ Issue [#12183](https://github.com/jsboige/CoursIA/issues/12183) tranche cette co | Case (dissociation) | Statut | Prédiction pré-enregistrée (falsifiable) | Null adversarial (tue la prédiction si…) | Substrat | Hook grade C (crédit témoin) | |---|---|---|---|---|---| | **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) | +| **W diffuse un q erroné** (confabulation) | `TESTÉ (INCONCLUSIF — PAR CONSTRUCTION)` — exécution [#15915](https://github.com/jsboige/CoursIA/pull/15915) (po-2024, 2026-09-13) : calibration résiduelle **dégénérée 0/5 graines** — gap propa(q̂_vrai)−propa(aléatoire) = 0.000/0.000/−0.067/−0.067/0.000 : l'ignition vraie ne propage **pas mieux** qu'un stimulus aléatoire, ε_stable non fixable, la branche Agrégé [0.80, 1.00] de la table scellée n'est **pas évaluable** (le garde anti-HARKing du module refuse le score, `<3 graines exploitables` ; l'alternative ε_stable := 0 « confirmerait » **vacument** — elle certifierait le broadcast, pas la consécration). Diagnostic post-hoc (non scellé) : propa(q̂_faux) = propa(q̂_vrai) **seed-par-seed** (égalité exacte 4/5, +1/15 graine 99), R_confab médian **1.000** ; Jaccard top-64 : J(vrai,faux) **0.883** ≫ J(vrai,aléa) 0.460 — les *signatures* discriminent le contenu (carafe vs tramway), le *taux* propa sature (~0.80–1.00) pour chaque bras : même plafond instrumental que case 4 (R_self 1.000, [#15879](https://github.com/jsboige/CoursIA/pull/15879)) et case 5 ([#15547](https://github.com/jsboige/CoursIA/pull/15547)). Substrat : Qwen3.5-2B-Base couche 12/24 × SAE Qwen-Scope W32K-L0_50, hérité de case 5 pour la comparabilité R_confab/R_self/R_attn, 5 graines, extraction GPU réelle locale, calibration (vrai + aléatoire) extraite, mesurée et **gelée AVANT** le bras faux — anti-HARKing plus strict que cases 4/5 (dénominateur de R ET ε_stable tous deux gelés avant la mesure principale). Cf. exécution détaillée ci-dessous. | 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** | `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). **Lecture grade C.** Dans son cadre (*Being No One: The Self-Model Theory of Subjectivity*, T. Metzinger, MIT Press, 2003 ; « Full-body illusions and minimal phenomenal selfhood », Blanke & Metzinger, *TiCS* 2009 ; « Minimal phenomenal experience », Metzinger, *Philosophy and the Mind Sciences*, 2020), le **phenomenal self-model (PSM)** est une représentation de l'agent lui-même intégrée au mécanisme de disponibilité globale : le « soi » n'est pas une chose mais un *modèle* — interne, activé en permanence, et surtout **transparent** (le système ne reconnaît pas ce modèle *comme* un modèle, d'où le réalisme naïf sur soi). La **minimal phenomenal selfhood** en isole le noyau : auto-localisation, perspective incarnée, qualité de « mien ». La case **self-model minimal** opérationnalise précisément la question fonctionnelle du PSM : le workspace traite-t-il `q(soi)` *autrement* que `q(autrui)` ? Le ratio `R_self = propa(q(soi)) / propa(q(autrui))` mesure le **traitement différentiel du self-model** relativement au world-model d'autrui — et la bande pré-enregistrée [0.50, 2.00] avec null adversarial (autrui de complexité équivalente → R_self ≈ 1.0, ou R_self > 2.50 = artefact) est exactement le test substrat que la théorie de Metzinger n'opère pas : son analyse du PSM est phénoménologique et conceptuelle, pas un protocole de propagation mesurable sur workspace. **Honnêteté grade C** : (a) la transposition est une *lecture* du design ICT à travers Metzinger, pas une validation de la Self-Model Theory — Metzinger ne prédit aucune valeur de ratio ; (b) `R_self` mesure une **asymétrie de propagation**, pas la **transparence** (l'invariant central du PSM) — un ratio dans la bande ne dit rien de ce que le système *reconnaît* ou non son self-model comme modèle ; (c) le papier MPS 2020 borne lui-même la thèse : l'expérience phénoménale minimale (*pure awareness*) peut survenir **sans** MPS — la case teste le traitement différentiel du self-content, pas la nécessité du self-model pour la conscience ; (d) la bande [0.50, 2.00], volontairement généreuse, peut être satisfaite par des asymétries architecturales bénignes (l'état propre est une entrée *constamment disponible*, l'état d'autrui ne l'est pas) — c'est ce que le garde `R_self > 2.50` nomme déjà comme artefact de discrimination, sans toutefois couvrir le cas limite d'une asymétrie d'entrée stable et modérée. La conjecture dérivable (jalon 3 #8182) — *chaque strate ICT = acquisition d'une adjonction que la précédente n'a pas* — se lirait ici : *le panel persona comme auto-application du workspace (W appliqué à q(soi)) est-il l'adjonction « self » que la strate précédente (W sur q(monde)) n'a pas ?* — mais c'est une autre brique (gated sur #7738). | | **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. | @@ -582,6 +582,28 @@ R_confab = propa(q̂_faux) / propa(q̂_vrai) ∈ [0.80, 1.00] **Substrat de pré-enregistrement (état c.1242).** Loaders Python CPU-only prêts : `MyIA.AI.Notebooks/IIT/ICT-Series/ict/sae_traces.py` (7276 octets, fonctions `load_traces`, `densify`, `mean_activation_by_set`, `differential_features`, `binarize_quantile`, `states_from_panel`) et `ict/jlens_traces.py` (compagnon, anti-mélange guard `lens == "sae"`). L'exécution du test (LLM forward pass + SAE forward) **exige GPU** (substrat Qwen2.5-3B, lane GenAI/po-2023 ou ai-01) — la **PR de test** n'est PAS exécutable par la présente lane CPU-only ; elle sera ouverte **par la lane GPU** ou auto-flaggée pour cross-pickup ai-01. Le présent PR ne touche que le **pré-enregistrement chiffré** (document), qui est substance à part entière (cf [#9546](https://github.com/jsboige/CoursIA/pull/9546) même logique : matrice fermée avant test). +#### Exécution du pré-enregistrement (2026-09-13, po-2024, [PR #15915](https://github.com/jsboige/CoursIA/pull/15915)) — verdict `INCONCLUSIF` (par construction) + +Banc : `ict/confabulation_broadcast.py` (opérateur `propa` importé TEL QUEL de case 5, test d'identité `is`), 24 tests `ict/tests/test_confabulation_broadcast.py` (collection ict/tests : 728 → 752). Extraction GPU réelle locale (RTX 3070) : 10 NPZ sous `traces/` — calibration (bras `target_vrai` + `control_alea`, 6 prompts/graine) puis bras faux (`confab_faux`, 3 prompts/graine), 5 graines {0, 1, 7, 42, 99}, via `scripts/extract_sae_traces.py` INCHANGÉ (Qwen3.5-2B-Base couche 12/24, SAE Qwen-Scope W32K-L0_50, hérité de case 5 pour la comparabilité R_confab/R_self/R_attn). + +**Déroulé (ordre anti-HARKing scellé).** Phase 1 `calibrate` : mesure propa(vrai) et propa(aléatoire) par graine à partir des traces de calibration SEULES → **refus du garde scellé** : 0/5 graines exploitables (gap ≤ 0 partout), ε_stable non fixable. Phase 2 `score` : jamais exécutée sur le pipeline scellé (refuse sans calibration gelée). Le verdict INCONCLUSIF est donc **par construction** — aucune branche de la table scellée n'est évaluable. + +| Graine | propa(q̂_vrai) | propa(aléatoire) | gap | exploitable | +|---|---|---|---|---| +| 0 | 1.000 | 1.000 | +0.000 | non | +| 1 | 0.933 | 0.933 | +0.000 | non | +| 7 | 0.800 | 0.867 | **−0.067** | non | +| 42 | 0.800 | 0.867 | **−0.067** | non | +| 99 | 0.800 | 0.800 | +0.000 | non | + +L'ignition **vraie** ne propage pas mieux qu'un stimulus **aléatoire** (sur 2/5 graines elle propage *moins* bien) : l'écart maximal que la propagation peut porter est nul ou négatif, la calibration résiduelle est **dégénérée**. Forcer ε_stable := 0 rendrait le critère « disc ≥ ε_stable » trivialement vrai et « confirmerait » la consécration **vacument** — c'est précisément le broadcast non-discriminant que le null adversarial du pré-enregistrement existe pour tuer ; le garde mécanique du module le refuse. + +**Diagnostic post-hoc (non scellé, calculé après le refus — ne porte aucun verdict).** Le bras faux a été extrait (avant tout regard sur la calibration, l'extraction est bon marché) ; descriptivement : propa(q̂_faux) = propa(q̂_vrai) **seed-par-seed** (égalité exacte 4/5, +1/15 sur la graine 99), R_confab médian **1.000**. Jaccard top-64 des signatures : J(vrai, faux) **0.883** ≫ J(vrai, aléa) **0.460** ≈ J(faux, aléa) **0.443** — les *signatures* top-64 sont **sensibles au contenu** (la carafe et le tramway séparent nettement ; vrai et faux partagent contexte + cible et ne diffèrent que d'un mot), mais le **taux** propa sature (~0.80–1.00) pour chaque bras : consommateurs et signature dérivent des **mêmes prompts**, chaque bras « s'auto-propage » dans son nuage de features contextuelles. La signature discrimine ; le taux de propagation ne discrimine pas. + +**Leçon instrumentale (3ᵉ occurrence, convergence).** R_attn (case 5, [#15547](https://github.com/jsboige/CoursIA/pull/15547)) INCONCLUSIF · R_self (case 4, [#15879](https://github.com/jsboige/CoursIA/pull/15879)) 1.000 INCONCLUSIF (Jaccard self/autrui 0.61) · R_confab (case 3, ce run) 1.000 INCONCLUSIF-par-construction — trois questions W-diffusion/self distinctes, un même plafond : l'opérateur `propa` sur top-64 de l'activation **moyenne** sature en régime broadcast quasi-uniforme à la couche 12/24. La question de la **consécration de l'erreur** reste OUVERTE mais **non-mesurable** avec cet opérateur sur ce substrat ; un instrument candidat devrait décorréler consommateurs et signature (p. ex. signatures différentielles vrai−faux plutôt que top-64 de la moyenne par bras) — piste à pré-enregistrer séparément si une future case la demande. + +Scoreboard complet : `ict/results/confabulation_broadcast_results.json` (mesures de calibration, refus verbatim, bloc descriptif post-hoc étiqueté). + #### Case 4 — `self-model minimal` — pré-enregistrement à compléter **Statut pré-enregistrement.** Reste `VIDE` → préciser au tableau ci-dessus. Les substrats pressentis (Panel persona [#5104](https://github.com/jsboige/CoursIA/issues/5104) / [#5105](https://github.com/jsboige/CoursIA/issues/5105)) sont en attente de stabilisation du panel G.90 (jalon 3 de #5105, PR #5105 PR3). La présente PR **ne chiffre pas** la case 4 — elle l'**identifie explicitement comme prochaine cible** et fixe le **plan de chiffrage** :