Mesurer la dérive d’un copilot — le gate par étape ne protège pas la chaîne

Recycler la documentation du projet LivresAgités (en sommeil) vers le dépôt pédagogique. Ce notebook illustre le Copilot Gutenberg d’AI-Engine — la seconde et dernière fonctionnalité GenAI cœur du dossier qui n’avait pas de notebook compagnon (l’autre, AI Forms, a été traitée par le grain précédent). Il est le compagnon exécutable du Parcours 1 de AI-Engine-WordPress.

Thèse. Le Copilot propose six transformations de texte — résumé, enhancement, traduction, rewriting, image, alt text — et un gate humain (« valide ou rejette l’insertion ») à chaque étape. Ce gate donne l’illusion d’une sûreté pas-à-pas. Mais les six transformations n’ont pas le même effet informationnel : certaines ajoutent (image, alt text), d’autres réécrivent sans perdre (enhancement, rewriting), d’autres encore détruisent de l’information (résumé, traduction). Et l’effet d’une transformation dépend de ce sur quoi elle s’applique : un résumé d’un résumé collapse, une traduction d’un texte déjà résumé perd le peu qu’il restait. Le gate est local (étape par étape) ; la dérive est globale (chaîne). Un Copilot ne protège pas de la dérive — il la rend consentie.

Ce notebook ne dépend d’aucun service : pas de réseau, pas de modèle, pas de clé. Les transformations sont simulées par des fonctions déterministes sur des vecteurs (fréquences de termes) — ce qui est enseigné est la structure de la dérive, pas une mesure sur un modèle particulier. Le fixture est synthétique (« Maison Valmont »).

1. Le Copilot Gutenberg en une phrase

AI-Engine ajoute un panneau Copilot dans l’éditeur Gutenberg de WordPress. Le rédacteur écrit un brouillon ; le Copilot propose une transformation ; le rédacteur valide ou rejette. Six transformations sont offertes :

Transformation Ce qu’elle fait
Résumé Condense un texte long en un paragraphe
Enhancement stylistique Clarifie, reformule, ajuste le ton
Traduction Traduit en préservant le registre
Rewriting Propose plusieurs variantes d’un paragraphe
Image d’en-tête Génère une illustration depuis un prompt
Alt text Décrit une image en quelques mots

La pièce maîtresse, pédagogiquement, n’est aucune de ces transformations prise isolément — c’est le gate humain. « Aucune réécriture automatique, le brouillon reste sous contrôle humain. » C’est vrai à l’échelle d’une transformation. Ce notebook demande : et à l’échelle d’une chaîne de transformations ?

2. Quatre natures informationnelles — pas six

Les six transformations se classent en quatre natures selon leur effet sur l’information du document source. Cette classification n’est pas cosmétique : elle détermine ce que le gate humain peut, ou ne peut pas, protéger.

Nature Transformations Effet sur l’original Gate peut restaurer ?
Synthèse additive image, alt text Ajoute du contenu dérivé (axe nouveau) Oui (on retire l’ajout)
Réécriture réversible enhancement, rewriting Bruit de forme, fond préservé Oui (on garde l’original)
Projection partielle traduction Perte du registre stylistique Non (le registre est détruit)
Réduction destructive résumé Perte des thèmes mineurs, irréversible Non (l’info mineure est détruite)

La ligne qui sépare les deux premières des deux dernières est la frontière de réversibilité. En-deçà (synthèse, réécriture), le gate humain est une vraie garantie : on peut toujours revenir. Au-delà (projection, réduction), le gate est une approbation d’une perte : valider un résumé, c’est consentir à ne jamais revoir les nuances qu’il a tu — même si on rejette l’étape suivante.

3. Le fixture — un document et son vecteur

On représente un document par un vecteur de fréquences de termes sur un vocabulaire fixe. C’est une représentation rudimentaire (pas de syntaxe, pas d’ordre) mais suffisante pour mesurer la dérive : ce qui nous intéresse est combien de l’original survit, pas la qualité littéraire.

Le fixture est une page de catalogue Valmont : la fiche d’un livre, décrit par la fréquence de dix termes éditoriaux.

# Aucune dependance externe hors numpy.
import numpy as np

# Vocabulaire d'une page catalogue Valmont (10 termes editoriaux).
VOCABULAIRE = ["recit", "personnage", "style", "intrigue", "plume",
               "chapitre", "denouement", "voix", "rythme", "image"]

# Document source : fiche d'un livre (frequence de chaque terme).
# Note : 'image' est a 0 -- la page texte ne decrit pas d'image. Les axes
# d'ajout (image, alt_text) sont donc vraiment orthogonaux a l'original.
v0 = np.array([3.0, 2.0, 4.0, 2.0, 1.0, 2.0, 1.0, 3.0, 1.0, 0.0])

print("Document source (sommet de la fiche du livre) :")
for terme, freq in zip(VOCABULAIRE, v0):
    print("  " + terme + " : " + str(int(freq)))
print("  norme au carree : " + str(float(v0 @ v0)))
Document source (sommet de la fiche du livre) :
  recit : 3
  personnage : 2
  style : 4
  intrigue : 2
  plume : 1
  chapitre : 2
  denouement : 1
  voix : 3
  rythme : 1
  image : 0
  norme au carree : 49.0

La métrique : une projection normalisée — pas une fraction conservée

Pour mesurer la dérive, on calcule la projection normalisée du vecteur courant sur le vecteur original, plafonnée à 1 :

\[\text{projection}(v) = \min\!\left(1,\ \frac{v \cdot v_0}{v_0 \cdot v_0}\right)\]

Ce que cette quantité mesure — et ce qu’elle ne mesure pas :

  • Si \(v = v_0\) : projection = 1,0 (identité, rien n’a bougé).
  • Si \(v\) ajoute du contenu orthogonal (une image sur un axe nouveau) : projection = 1,0 (la projection ne voit pas l’ajout — l’original est intact, juste dilué).
  • Si \(v\) atténue des composantes de \(v_0\) sans en amplifier d’autres : projection < 1,0.
  • Contre-exemple : supprimer une composante et en amplifier une autre peut ramener la projection à 1,0. Sur \(v_0 = (1, 1)\), le vecteur \((2, 0)\) donne une projection de 1,0 — la deuxième composante a entièrement disparu, mais le doublement de la première compense exactement la perte dans le produit scalaire. La projection n’est donc pas une fraction d’information conservée : c’est un chevauchement directionnel, trompable par compensation (les quatre cas-limites sont testés ci-dessous).

Pourquoi l’utiliser quand même ? Parce qu’elle sépare l’ajout de la perte, là où le cosinus confond perte et dilution (les deux le font baisser). Une synthèse additive (image) et une réduction (résumé) baissent toutes deux le cosinus, mais pour des raisons opposées — seule la projection les distingue.

Pour une lecture en « fraction conservée », il faut un indicateur composante par composante — le recouvrement défini ci-dessous, valable pour des fréquences non négatives. Les deux indicateurs ne sont pas interchangeables, et ni l’un ni l’autre ne mesure la fidélité factuelle d’une réécriture textuelle réelle : pour cela, il faudrait des faits de référence et une vérification dédiée.

def rappel(v_courant, v_original):
    """Projection normalisee du vecteur courant sur l'original, plafonnee a 1.

    (v_courant . v_original) / (v_original . v_original), bornee par min(1, .).
    ATTENTION : ce n'est PAS une fraction d'information conservee -- une
    suppression compensee par une amplification (ex. (1,1) -> (2,0)) vaut 1.0
    bien qu'une composante ait entierement disparu. Vaut 1.0 si l'original est
    intact ou si seul un contenu orthogonal a ete ajoute ; < 1.0 si des
    composantes de l'original sont attenuees sans compensation. Le nom
    `rappel` est historique : pour une lecture "fraction conservee", voir
    `recouvrement` ci-dessous.
    """
    denom = float(v_original @ v_original)
    if denom == 0.0:
        return 0.0
    # Plafonne a 1.0 : la projection normalisee ne depasse pas son support.
    # Le bruit additif (enhancement, rewriting) pousse legerement au-dessus ;
    # c'est un artefact, pas une retention superieure a l'original.
    return min(1.0, float(v_courant @ v_original) / denom)


def cosinus(a, b):
    """Cosinus entre deux vecteurs (mesure de dilution, pas de perte)."""
    na, nb = np.linalg.norm(a), np.linalg.norm(b)
    if na == 0 or nb == 0:
        return 0.0
    return float(a @ b / (na * nb))


# Sanity check.
print("rappel(v0, v0) = " + str(rappel(v0, v0)) + "  (doit valoir 1.0)")
v_ajout = v0.copy()
v_ajout[9] += 4.0   # ajout orthogonal sur l'axe 'image'
print("rappel(v0 + ajout_image, v0) = " + str(round(rappel(v_ajout, v0), 3)) + "  (doit valoir 1.0 : ajout non penalise)")
print("cosinus(v0 + ajout_image, v0) = " + str(round(cosinus(v_ajout, v0), 3)) + "  (baisse : dilution)")
rappel(v0, v0) = 1.0  (doit valoir 1.0)
rappel(v0 + ajout_image, v0) = 1.0  (doit valoir 1.0 : ajout non penalise)
cosinus(v0 + ajout_image, v0) = 0.868  (baisse : dilution)

Les quatre cas-limites — projection contre recouvrement

La projection normalisée peut valoir 1,0 alors que de l’information a disparu. Pour le voir : quatre cas sur un jouet à deux axes occupés et un axe libre, comparés sur deux indicateurs — la projection, et le recouvrement composante par composante, valable pour des fréquences non négatives : la masse commune à chaque axe (le minimum des deux), rapportée à la masse totale de l’original. C’est lui — et seulement lui — qui se lit comme « fraction de la masse fréquencielle conservée ».

def recouvrement(v_courant, v_original):
    """Recouvrement composante par composante (frequences non negatives) :
    sum(min(v_courant, v_original)) / sum(v_original). Se lit comme la
    fraction de la masse frequentielle conservee axe par axe -- ce que la
    projection normalisee n'est PAS (voir la metrique plus haut).
    """
    return float(np.minimum(v_courant, v_original).sum() / v_original.sum())


# Jouet : deux axes occupes (1, 1) + un axe libre (0).
original_limites = np.array([1.0, 1.0, 0.0])
cas_limites = {
    "identite                (1, 1, 0)": np.array([1.0, 1.0, 0.0]),
    "suppression             (1, 0, 0)": np.array([1.0, 0.0, 0.0]),
    "ajout orthogonal        (1, 1, 2)": np.array([1.0, 1.0, 2.0]),
    "amplification compensee (2, 0, 0)": np.array([2.0, 0.0, 0.0]),
}

print("original = (1, 1, 0)     |  projection  recouvrement")
print("  " + "-" * 58)
for nom, v in cas_limites.items():
    p = rappel(v, original_limites)
    r = recouvrement(v, original_limites)
    print("  " + nom + "  |  " + str(round(p, 2)).ljust(10) + str(round(r, 2)))

print()
print("Derniere ligne : la projection vaut 1.0 -- le doublement de l'axe 1")
print("compense exactement la disparition de l'axe 2 dans le produit scalaire")
print("-- alors que la moitie de la masse frequentielle a disparu. Seul le")
print("recouvrement (0.5) revele la perte. Les deux indicateurs ne sont pas")
print("interchangeables.")
original = (1, 1, 0)     |  projection  recouvrement
  ----------------------------------------------------------
  identite                (1, 1, 0)  |  1.0       1.0
  suppression             (1, 0, 0)  |  0.5       0.5
  ajout orthogonal        (1, 1, 2)  |  1.0       1.0
  amplification compensee (2, 0, 0)  |  1.0       0.5

Derniere ligne : la projection vaut 1.0 -- le doublement de l'axe 1
compense exactement la disparition de l'axe 2 dans le produit scalaire
-- alors que la moitie de la masse frequentielle a disparu. Seul le
recouvrement (0.5) revele la perte. Les deux indicateurs ne sont pas
interchangeables.

4. Les six transformations, en vecteurs

Chaque transformation est modélisée par une fonction déterministe sur le vecteur. Ce ne sont pas de vraies transformations de texte — ce sont des fables vectorielles qui capturent la nature informationnelle de chacune. Le résumé seuille (garde les \(k\) plus grandes composantes) ; la traduction atténue les dimensions de registre ; l’image ajoute sur un axe nouveau. La reproductibilité est garantie par des graines fixes.

def resume(v, k=4):
    """REDUCTION destructive : garde les k plus grandes composantes,
    annule les autres. L'information mineure est detruite."""
    w = np.zeros_like(v)
    idx = np.argsort(v)[::-1][:k]
    w[idx] = v[idx]
    return w


def enhancement(v):
    """REECRITURE reversible : bruit leger proportionnel au poids.
    L'information principale est preservee."""
    rng = np.random.RandomState(7)
    return v + 0.10 * rng.randn(len(v)) * np.abs(v)


def rewriting(v):
    """REECRITURE plus marquee qu'enhancement. Reversible mais bruyante."""
    rng = np.random.RandomState(13)
    return v + 0.15 * rng.randn(len(v)) * np.abs(v)


def traduction(v):
    """PROJECTION partielle : attenuation des dimensions de registre
    (style, plume, voix) + leger bruit. Perte du registre, fond preserve."""
    w = v.copy()
    for i in [2, 4, 7]:   # style, plume, voix
        w[i] *= 0.5
    rng = np.random.RandomState(11)
    return w + 0.05 * rng.randn(len(v)) * np.abs(v)


def image(v):
    """SYNTHESE additive : ajoute une description image sur un axe orthogonal."""
    w = v.copy()
    w[9] += 4.0   # axe 'image'
    return w


def alt_text(v):
    """SYNTHESE additive courte : comme image, amplitude plus faible."""
    w = v.copy()
    w[9] += 2.5
    return w


TRANSFORMATIONS = {
    "resume":      resume,
    "enhancement": enhancement,
    "rewriting":   rewriting,
    "traduction":  traduction,
    "image":       image,
    "alt_text":    alt_text,
}
# Profil de chaque transformation appliquee SEULE au document source.
def nature_transformation(v_original, v_courant):
    """Classe une transformation par son effet informationnel.

    Le rappel seul ne distingue pas une reecriture (bruit, norme stable)
    d'une synthese additive (ajout, norme en hausse) -- les deux ont un
    rappel ~1.0. On ajoute donc le ratio de norme pour separer.
    """
    r = rappel(v_courant, v_original)
    if r < 0.85:
        return "perte (rappel < 0.85)"
    ratio_norme = np.linalg.norm(v_courant) / np.linalg.norm(v_original)
    if ratio_norme > 1.05:
        return "synthese additive (norme en hausse)"
    return "reecriture reversible (norme stable)"


print("Rappel de l'original apres une transformation unique :")
print()
print("  " + "transformation".ljust(14) + "rappel   cosinus   nature (rappel + norme)")
print("  " + "-" * 64)
for nom, f in TRANSFORMATIONS.items():
    w = f(v0)
    r = rappel(w, v0)
    c = cosinus(w, v0)
    print("  " + nom.ljust(14) + str(round(r, 3)).ljust(9) + str(round(c, 3)).ljust(10) + nature_transformation(v0, w))
Rappel de l'original apres une transformation unique :

  transformationrappel   cosinus   nature (rappel + norme)
  ----------------------------------------------------------------
  resume        0.776    0.881     perte (rappel < 0.85)
  enhancement   1.0      0.994     reecriture reversible (norme stable)
  rewriting     1.0      0.996     reecriture reversible (norme stable)
  traduction    0.732    0.941     perte (rappel < 0.85)
  image         1.0      0.868     synthese additive (norme en hausse)
  alt_text      1.0      0.942     synthese additive (norme en hausse)

Lecture

Le tableau rend la §2 lisible par les nombres. Trois groupes se séparent nettement sur le rappel :

  • Synthèse additive (image, alt_text) : rappel = 1,0 — l’original est intact, le vecteur a juste grossi sur un axe nouveau. Le cosinus baisse (dilution), mais c’est un artefact, pas une perte.
  • Réécriture réversible (enhancement, rewriting) : rappel élevé (~0,9+) — du bruit sur la forme, le fond préservé.
  • Perte (résumé, traduction) : rappel abaissé — de l’information de l’original a été effectivement détruite.

Une seule mesure (le rappel) fait le travail que la lecture du menu « six transformations » ne fait pas : elle révèle que le résumé et l’image, présentés côte à côte dans le panneau Copilot, n’ont rien à voir — l’un détruit, l’autre ajoute.

5. La chaîne — la dérive est cumulative

C’est ici que le gate humain montre sa limite. Appliquons non plus une transformation, mais une séquence — chacune validée par le rédacteur — et mesurons le rappel à la fin.

def rappel_apres_chaine(v_original, chaine):
    """Applique une chaine de transformations (par nom) a l'original,
    renvoie la projection normalisee finale.
    """
    v = v_original.copy()
    for nom in chaine:
        v = TRANSFORMATIONS[nom](v)
    return rappel(v, v_original)


# Trois chaines candidates, chacune plausible pour un redacteur.
chaines = {
    "A (destructrice)":      ["resume", "traduction", "enhancement"],
    "B (benigne)":           ["enhancement", "rewriting", "alt_text"],
    "C (collapse)":          ["resume", "resume"],
}

print("Verdict final de chaque chaine (les deux indicateurs, cf. la metrique) :")
print()
for nom, chaine in chaines.items():
    v_final = v0.copy()
    for t in chaine:
        v_final = TRANSFORMATIONS[t](v_final)
    p = rappel_apres_chaine(v0, chaine)
    r = recouvrement(v_final, v0)
    print("  " + nom.ljust(22) + " -> projection " + str(round(p, 3)).ljust(7) + " | recouvrement " + str(round(r, 3)))
Verdict final de chaque chaine (les deux indicateurs, cf. la metrique) :

  A (destructrice)       -> projection 0.544   | recouvrement 0.424
  B (benigne)            -> projection 1.0     | recouvrement 0.988
  C (collapse)           -> projection 0.776   | recouvrement 0.632

Lecture

Les trois chaînes ont la même longueur (deux ou trois étapes), et chaque étape individuelle passait le gate humain — aucune n’aurait été rejetée à la lecture, car chaque transformation prise seule est légitime. Et pourtant :

  • Chaîne A (résumé → traduction → enhancement) : projection 0,54, recouvrement 0,42 — plus de la moitié de la masse fréquentielle a disparu (lecture sur le recouvrement, seul indicateur qui se lit en fraction conservée ; la projection ne mesure qu’un chevauchement directionnel). Le résumé a retiré les nuances ; la traduction a écrasé le registre qu’il restait ; l’enhancement a bruité le tout. Le rédacteur a validé trois fois, et plus de la moitié du document est partie.
  • Chaîne B (enhancement → rewriting → alt text) : projection au plafond (1,0) et recouvrement 0,99 — les fréquences de l’original sont quasi intactes. Trois transformations, mais toutes réversibles ou additives : le fond tient.
  • Chaîne C (résumé → résumé) : projection 0,78, recouvrement 0,63 — et les mêmes valeurs qu’un seul résumé, sur les deux indicateurs à la fois. Ce n’est pas que la chaîne soit sûre — c’est qu’il n’y a plus rien à perdre : le premier résumé a déjà tué les nuances, et le second bute sur le même plancher (resume est idempotente dans ce modèle top-k : résumer le résumé redonne le même vecteur). La destruction a un point bas, et le résumé d’un résumé le confirme.

C’est l’enseignement central : le gate humain est local, la dérive est globale. La chaîne A perd deux fois (deux destructrices complémentaires détruisent des informations différentes), la chaîne C ne perd qu’une fois (la deuxième destructive est redondante avec la première). Dans les deux cas, aucune des étapes n’aurait été rejetée isolément — et dans les deux cas, le résultat global n’est pas la somme des résultats locaux. Un Copilot sans vision de la chaîne — sans afficher un indicateur de conservation muni de sa définition — laisse le rédacteur approuver une perte qu’il n’aurait pas acceptée d’un coup.

6. Où la dérive se produit — le profil étape par étape

Le rappel final est un verdict ; le profil (rappel après chaque étape) est le diagnostic. Où la dérive se produit-elle ? D’un seul tenant, ou répartie ?

def profil_chaine(v_original, chaine):
    """Renvoie la liste des rappels apres chaque etape (incluant l'etat
    initial 1.0 en tete).
    """
    profil = [1.0]
    v = v_original.copy()
    for nom in chaine:
        v = TRANSFORMATIONS[nom](v)
        profil.append(rappel(v, v_original))
    return profil


for nom, chaine in chaines.items():
    p = profil_chaine(v0, chaine)
    etiquettes = ["initial"] + chaine
    ligne = "  " + nom.ljust(22) + " "
    for e, r in zip(etiquettes, p):
        ligne += e + "=" + str(round(r, 2)) + "  "
    print(ligne)
  A (destructrice)       initial=1.0  resume=0.78  traduction=0.53  enhancement=0.54  
  B (benigne)            initial=1.0  enhancement=1.0  rewriting=1.0  alt_text=1.0  
  C (collapse)           initial=1.0  resume=0.78  resume=0.78  

Lecture

Le profil révèle ce que le verdict final masque : la dérive n’est pas uniforme, elle a une géométrie. Trois formes caractéristiques apparaissent :

  • Chaîne A : un escalier qui descend à chaque destructrice. Le résumé ampute (marche 1), la traduction finit le travail sur le registre (marche 2), et l’enhancement — réversible — n’ajoute qu’un plat. La dérive est complète avant la dernière étape : un Copilot qui afficherait le profil permettrait au rédacteur de voir que le dommage est fait dès la traduction.
  • Chaîne B : une ligne plate au plafond. Trois étapes validées, zéro perte. C’est le profil d’une chaîne qui ne franchit jamais la frontière de réversibilité — exactement ce qu’on veut pour des contenus où le fond compte.
  • Chaîne C : une seule marche, puis un plat. Le deuxième résumé ne descend pas plus que le premier. Le profil plat après la première marche est la signature d’une transformation idempotente — il n’y a plus rien à extraire.

C’est la contre-mesure pédagogique : si le Copilot affichait le profil — « après cette validation, la conservation mesurée du document est de X, selon l’indicateur affiché et sa définition » — le rédacteur verrait le point de rupture (chaîne A), la préservation (chaîne B) ou le plancher (chaîne C), et pourrait décider en connaissance de cause. Le gate humain pourrait protéger la chaîne, à la condition expresse de l’instrumenter avec une mémoire de la chaîne. Sans cette mémoire, le gate est une suite d’approbations locales qui ne voient pas la dérive globale.

7. Provenance et limites

Ce que ce notebook mesure. La structure de la dérive d’un document soumis à une chaîne de transformations : quelles transformations détruisent (rappel baisse), lesquelles préservent (rappel stable), et comment l’effet cumulé d’une chaîne validée étape par étape diffère de la somme des effets individuels. Tout est déterministe sur fixture synthétique.

Ce qu’il ne mesure pas. La qualité réelle des transformations (un résumé vrai peut être excellent et sa perte acceptable ; un enhancement vrai peut dégrader le style). Les transformations sont des fables vectorielles qui capturent la nature informationnelle, pas le rendu littéraire. La représentation par fréquences de termes ignore l’ordre, la syntaxe, le sens — elle ne mesure que la matière informationnelle.

La limite du propos. Le gate humain n’est pas inutile — il empêche les dérives les plus visibles (un résumé faux, une traduction absurde). Ce notebook ne dit pas « le gate ne sert à rien » ; il dit « le gate local ne voit pas la dérive globale, et cette limite appelle une instrumentation ». C’est une leçon de conception, pas un réquisitoire.

Pour aller plus loin. - auditer-un-formulaire-conditionnel.ipynb — autre angle sur le « comportement émergent non-lisible sur la définition statique » : un formulaire conditionnel comme machine à états. - livresagites-parcours.md Parcours 1 — la prose dont ce notebook est l’illustration exécutable, et sa leçon transposable (« cartographier les flux de texte réels »).

8. Exercices

Les trois exercices suivants manipulent le document source, les six transformations (TRANSFORMATIONS) et la métrique (rappel). Les stub sont à compléter — return None ou pass.

Exercice 1 — rappel après une chaîne

Écrire une fonction rappel_chaine qui, étant donné un vecteur original et une liste de noms de transformations, renvoie le rappel final après application de la chaîne complète. (Vous pouvez vous aider de rappel_apres_chaine défini plus haut, ou le réécrire.)

def rappel_chaine(v_original, chaine):
    """Renvoie le rappel de l'original apres application d'une chaine
    de transformations (liste de noms).
    """
    # TODO : appliquer chaque transformation de la chaine, puis mesurer le rappel.
    return None

Exercice 2 — la chaîne la plus destructrice

Étant donné un vecteur original et une liste de chaînes candidates (chacune une liste de noms de transformations), écrire chaine_la_plus_destructrice qui renvoie la chaîne dont le rappel final est le plus bas (celle qui détruit le plus d’information).

def chaine_la_plus_destructrice(v_original, candidates):
    """Parmi plusieurs chaines candidates, renvoie celle dont le rappel
    final est le plus bas (la plus destructrice).
    """
    # TODO : calculer le rappel de chaque candidate, garder le min.
    return None

Exercice 3 — le profil étape par étape

Écrire une fonction profil qui renvoie la liste des rappels après chaque étape d’une chaîne (en tête la valeur 1,0 pour l’état initial). Permet de localiser le point de rupture — là où la dérive se concentre.

def profil(v_original, chaine):
    """Renvoie la liste des rappels apres chaque etape, en tete 1.0
    (etat initial). Le point de rupture est la marche ou le rappel
    chute le plus fort.
    """
    # TODO : accumuler le rappel apres chaque transformation.
    return None

9. Ce que ce notebook enseigne, en une ligne

Un gate humain à chaque étape ne protège pas de la dérive d’une chaîne. Le résumé détruit, l’image ajoute, l’enhancement bruite — et le rédacteur qui valide pas-à-pas ne voit pas qu’il a traversé une frontière de réversibilité. Le remède n’est pas plus de gate, c’est un gate qui se souvienne de la chaîne.

Retour au sommet