Commit fa743269 authored by Jordan Grossemy's avatar Jordan Grossemy
Browse files

Les douze garanties orphelines ont enfin l'exigence qui les justifiait

Douze identifiants employés par la suite n'apparaissaient dans aucun des deux
cahiers des charges. Qui remontait d'un contrôle vers sa raison d'être tombait
sur un cul-de-sac — et dans l'autre sens c'est pire : une garantie peut survivre
à l'exigence qui la justifiait, et continuer d'exiger d'un produit un
comportement que plus personne n'a demandé.

── Rédigées d'après ce que les garanties vérifient, ligne par ligne

Et cela valait la peine de les lire plutôt que de recopier le résumé que j'avais
écrit dans le cahier de test : quatre y étaient mal décrites. EF-404a porte sur
la recherche et les filtres de la main courante, non sur ce que le journal y
verse. EF-707a et EF-707b sur la portée des dépêches temps réel et les
interpellations, non sur la recherche. EF-1002a sur la bibliothèque de modèles
portée par l'organisation, non sur des modèles qui posent des éléments. Un
résumé écrit de mémoire vaut ce qu'il vaut.

Neuf sous-exigences, placées dans la section où on les chercherait — 6.1
invitation multicanal et compte ouvert sur place, 6.4 recherche en main courante,
6.5 qui peut porter une action, 6.7 portée des dépêches et « @tous », 6.10
bibliothèque de modèles de tableau. La lettre en suffixe est déjà l'usage du
document, qui porte un EF-1103c ; elle ne signifie pas « cas particulier du
parent », et les textes le disent d'eux-mêmes.

── Une fonction entière n'était pas spécifiée

« Veille externe » n'apparaissait pas une seule fois dans le cahier des charges.
Ce n'était donc pas trois identifiants à recoller mais une sous-section à
écrire : 6.13, avec la lecture de flux sans bibliothèque tierce, le versement
d'un signal retenu en main courante — source, auteur, heure de publication
d'origine, adresse, numéro de séquence — et la purge à trente jours qui épargne
ce qu'on a retenu.

La garde sur la destination d'une source y est écrite en entier, parce qu'elle
n'a rien d'évident : l'adresse est saisie par l'organisation mais c'est le
serveur qui va la chercher, et l'inscription est en libre-service. Point de
métadonnées d'un hébergeur, boucle locale, nom qui y résout, plages privées en
IPv4 et IPv6, IPv4 encapsulée en IPv6, jeton en clair, flux qui ne s'arrête
jamais — et la borne inverse, qui interdit d'écarter les adresses publiques
voisines des plages privées.

── RG-292 n'avait besoin de rien de neuf

Il vivait au milieu de « RG-291 à RG-293 », une plage dont seuls les bouts
étaient écrits. Son comportement était déjà spécifié dans le texte d'EF-1108,
mot pour mot. La plage est énumérée.

── Ce qui les y maintient

Une garantie, EX-39, et une exigence pour la porter — c'est une treizième
exigence que j'ai écrite, au-delà des douze demandées, et je la signale comme
telle : sans elle, la garantie qui rattache les douze serait elle-même orpheline.

Elle est exécutée en dernier, ce qui est la condition de son sens : elle porte
sur tout ce que la course a touché. Et elle relève les identifiants à
l'exécution, dans le rapport, plutôt qu'au grep du source — un identifiant
calculé y figure, un contrôle jamais exécuté n'y figure pas, ce qui est le bon
comportement des deux côtés.

Éprouvée dans les deux sens, avec une garantie factice : elle signale
« EF-9999 » comme absent des cahiers, et se tait sur EF-1301, qui vient d'y
entrer.

1184 garanties au vert. Les cahiers nomment 296 exigences ; la suite en éprouve
210, toutes rattachées.

── Ce qui reste stale, et que je n'ai pas touché

Le relevé du §10 du cahier de test porte encore « onze étiquettes nomment un
groupe » comme écart ouvert, alors que quatorze sont corrigées, et ne mentionne
ni les barres d'onglets ni les états d'appui. C'est ma propre traîne, sur cinq
commits. Le corriger demande de reprendre les deux tableaux du relevé, ce qui
est un travail de rédaction à part.

Co-Authored-By: Claude (RCA)
parent 4e836903
Loading
Loading
Loading
Loading
Loading
+7 −0
Changes for apps/api/src/verif.ts: 7 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -42,6 +42,7 @@ import { verifierMobilisation } from "./verification/mobilisation.js";
import { verifierRestitution } from "./verification/restitution.js";
import { verifierDistribution } from "./verification/distribution.js";
import { installerMessagesFrancais } from "@kastell/shared";
import { verifierTracabilite } from "./verification/tracabilite.js";

/**
 * EX-32 — Vérification d'installation, et démonstration des garanties de la
@@ -394,6 +395,12 @@ await verifierRestitution(sql, app, rapport);
await verifierDistribution(sql, app, rapport);
await app.close();

// ── Traçabilité (EX-39) ───────────────────────────────────────────────────
//
// En dernier, et c'est la condition pour que ce contrôle ait un sens : il porte
// sur tout ce que la course a touché.
await verifierTracabilite(rapport);

const echecs = rapport.bilan();
await sql.end();
process.exit(echecs === 0 ? 0 : 1);
+13 −0
Changes for apps/api/src/verification/rapport.ts: 13 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -2,6 +2,7 @@
export class Rapport {
  private echecs = 0;
  private controles = 0;
  private readonly exigences = new Set<string>();

  titre(t: string): void {
    console.log(`\n${t} :`);
@@ -9,6 +10,7 @@ export class Rapport {

  verifie(exigence: string, libelle: string, ok: boolean, detail = ""): void {
    this.controles += 1;
    this.exigences.add(exigence);
    if (!ok) this.echecs += 1;
    console.log(`  ${ok ? "✓" : "✗"} ${exigence.padEnd(6)} ${libelle}${detail ? ` — ${detail}` : ""}`);
  }
@@ -28,6 +30,17 @@ export class Rapport {
    }
  }

  /**
   * Les exigences que la course a réellement touchées.
   *
   * Relevées à l'exécution, non au grep : une garantie dont l'identifiant se
   * calcule y figure quand même, et une garantie qui ne s'exécute jamais n'y
   * figure pas — ce qui est le bon comportement des deux côtés.
   */
  exigencesVues(): ReadonlySet<string> {
    return this.exigences;
  }

  bilan(): number {
    console.log(
      this.echecs === 0
+50 −0
Changes for apps/api/src/verification/tracabilite.ts: 50 added lines, 0 removed lines.
Original line number Diff line number Diff line
import { readFile } from "node:fs/promises";
import { fileURLToPath } from "node:url";
import type { Rapport } from "./rapport.js";

/**
 * Une garantie sans exigence est un contrôle que personne n'a demandé (EX-39).
 *
 * Douze identifiants employés par la suite n'apparaissaient dans aucun cahier
 * des charges : les deux moitiés de l'invitation multicanal, la recherche en
 * main courante, qui peut porter une action, la portée des dépêches et les
 * interpellations, les conversations flottantes, la bibliothèque de modèles de
 * tableau, et les trois de la veille externe — une fonction entière absente de
 * la spécification. Plus RG-292, qui vivait au milieu d'une plage dont seuls
 * les bouts étaient écrits.
 *
 * Qui remontait d'un contrôle vers sa raison d'être tombait donc sur un
 * cul-de-sac. Et dans l'autre sens, c'est pire : une garantie peut survivre à
 * l'exigence qui la justifiait, et continuer d'exiger d'un produit un
 * comportement que plus personne n'a demandé.
 *
 * Ce contrôle passe en dernier, parce qu'il a besoin de tout ce que la course a
 * touché. Il lit ce que le rapport a relevé à l'exécution plutôt que le source
 * au grep : un identifiant calculé y figure, un contrôle jamais exécuté n'y
 * figure pas.
 */

const CAHIERS = ["../../../../cahier-des-charges.html",
  "../../../../cahier-des-charges-mobile.html"];

const IDENTIFIANT = /\b(?:EF|EM|EX|EA|EB|RG)-\d+[a-z]?\b/g;

export async function verifierTracabilite(r: Rapport): Promise<void> {
  r.titre("Traçabilité des garanties");

  const nommees = new Set<string>();
  for (const relatif of CAHIERS) {
    const chemin = fileURLToPath(new URL(relatif, import.meta.url));
    const texte = await readFile(chemin, "utf8");
    for (const m of texte.matchAll(IDENTIFIANT)) nommees.add(m[0]);
  }
  r.verifie("EX-39", `les cahiers des charges nomment ${nommees.size} exigences`,
    nommees.size > 200, String(nommees.size));

  // Le sien compte aussi : il s'ajoute au relevé pendant qu'on le lit.
  const vues = new Set([...r.exigencesVues(), "EX-39"]);
  const orphelines = [...vues].filter((e) => !nommees.has(e)).sort();
  r.verifie("EX-39",
    `les ${vues.size} exigences éprouvées figurent toutes dans un cahier`,
    orphelines.length === 0, orphelines.join(", "));
}
+29 −32
Changes for cahier-de-test.html: 29 added lines, 32 removed lines.
Original line number Diff line number Diff line
@@ -1344,40 +1344,37 @@ pnpm arreter # arrête tout, sans détruire les volumes</code></pre>
  ils ne remplacent pas l’œil.
</p>

<h3>Douze garanties dont l’exigence n’existe pas</h3>
<h3>Les douze garanties orphelines sont rattachées</h3>
<p class="measure">
  Ces identifiants sont employés par la suite et n’apparaissent dans aucun des
  deux cahiers des charges. Un testeur qui remonte d’une garantie vers son
  exigence tombe donc sur un cul-de-sac. Constaté en rédigeant ce cahier, non
  supposé&nbsp;: c’est la différence entre les identifiants cités par
  <code>r.verifie</code> et ceux que les cahiers nomment.
  Douze identifiants employés par la suite n’apparaissaient dans aucun des deux
  cahiers des charges&nbsp;: un testeur qui remontait d’une garantie vers son
  exigence tombait sur un cul-de-sac. Les douze sont désormais écrits — neuf
  sous-exigences aux sections 6.1, 6.4, 6.5, 6.7 et 6.10, une sous-section 6.13
  entière pour la veille externe, qui n’était pas spécifiée du tout, et
  <span class="rid">RG-292</span> dont la plage «&nbsp;RG-291 à RG-293&nbsp;»
  n’écrivait que les bouts.
</p>
<p class="measure">
  Une garantie neuve — <span class="rid">EX-39</span> — refuse de passer si un
  identifiant employé par la suite manque aux cahiers. Elle est exécutée en
  dernier, parce qu’elle porte sur tout ce que la course a touché, et elle relève
  les identifiants à l’exécution plutôt qu’au grep&nbsp;: un identifiant calculé
  y figure, un contrôle jamais exécuté n’y figure pas. Éprouvée dans les deux
  sens&nbsp;: elle signale un identifiant absent des cahiers, et se tait sur ceux
  qui y sont.
</p>
<p class="measure">
  À noter pour qui comparera avec une version antérieure de ce cahier&nbsp;: le
  tableau qui listait ces douze identifiants décrivait mal quatre d’entre eux.
  <span class="rid">EF-404a</span> porte sur la recherche et les filtres de la
  main courante, non sur ce que le journal y verse&nbsp;;
  <span class="rid">EF-707a</span> et <span class="rid">EF-707b</span> sur la
  portée des dépêches temps réel et les interpellations, non sur la
  recherche&nbsp;; <span class="rid">EF-1002a</span> sur la bibliothèque de
  modèles portée par l’organisation, non sur des modèles qui posent des éléments.
  Un résumé écrit de mémoire vaut ce qu’il vaut&nbsp;: les exigences rédigées le
  sont d’après ce que les garanties vérifient, ligne par ligne.
</p>
<div class="scroll">
<table>
  <caption>À rattacher au cahier des charges, ou à en retirer</caption>
  <thead><tr><th>Identifiant</th><th>Ce que la garantie contrôle</th></tr></thead>
  <tbody>
    <tr><td><span class="rid">EF-107a</span> <span class="rid">EF-107b</span></td>
      <td>Invitation multicanal, et laissez-passer quand aucun canal ne part.</td></tr>
    <tr><td><span class="rid">EF-404a</span></td>
      <td>Ce que le journal verse de lui-même en main courante.</td></tr>
    <tr><td><span class="rid">EF-505a</span></td>
      <td>Qui peut porter une action, au-delà des membres d’une cellule.</td></tr>
    <tr><td><span class="rid">EF-707a</span> <span class="rid">EF-707b</span></td>
      <td>Recherche&nbsp;: portée et refus.</td></tr>
    <tr><td><span class="rid">EF-708a</span></td>
      <td>Conversations flottantes&nbsp;: réduire et fermer sont deux gestes.</td></tr>
    <tr><td><span class="rid">EF-1002a</span></td>
      <td>Modèles de tableau blanc qui posent des éléments.</td></tr>
    <tr><td><span class="rid">EF-1301</span> <span class="rid">EF-1302</span>
      <span class="rid">EF-1303</span></td>
      <td>Veille externe&nbsp;: lecture d’un flux, versement, refus de surveiller une
        personne nommée.</td></tr>
    <tr><td><span class="rid">RG-292</span></td>
      <td>Alertes multilingues&nbsp;: la variante non déclarée du cahier.</td></tr>
  </tbody>
</table>
</div>

<h2 id="s10"><span class="n">10</span>Relevé et suites</h2>

+20 −1
Changes for cahier-des-charges.html: 20 added lines, 1 removed line.
Original line number Diff line number Diff line
@@ -606,6 +606,8 @@ footer{
    <li><span class="rid">EF-105</span><span class="rtx">Codes de secours à usage unique générés à l&rsquo;activation du second facteur, présentés dans un format imprimable — la « carte de crise » que chaque membre range hors du SI.</span></li>
    <li><span class="rid">EF-106</span><span class="rtx">Aucune fédération d&rsquo;identité avec l&rsquo;annuaire du client. Ce refus est une fonctionnalité, documenté comme tel dans le produit.</span></li>
    <li><span class="rid">EF-107</span><span class="rtx">Invitation de membres par e-mail personnel comme professionnel, avec avertissement explicite recommandant une adresse hors du SI de l&rsquo;organisation.</span></li>
    <li><span class="rid">EF-107a</span><span class="rtx">Une invitation part par plusieurs canaux, et l&rsquo;échec de l&rsquo;un ne l&rsquo;annule pas&nbsp;: le relais de courriel injoignable, l&rsquo;invitation existe et reste en attente, et le motif de l&rsquo;échec est rapporté plutôt que tu. Le lien est rendu à qui invite, à transmettre autrement — un SMS, un appel, une feuille de papier.</span></li>
    <li><span class="rid">EF-107b</span><span class="rtx">Ouverture d&rsquo;un compte sur place, en pleine crise, avec un laissez-passer qui se dicte au téléphone&nbsp;: ni O ni 0, ni I ni 1. Il ouvre une session mais bloque tout le reste tant qu&rsquo;il n&rsquo;est pas remplacé, et le journal d&rsquo;administration retient qui l&rsquo;a ouvert et pourquoi. Un directeur de crise n&rsquo;ouvre pas ainsi un compte propriétaire, et une adresse déjà connue n&rsquo;est pas réouverte.</span></li>
    <li><span class="rid">EF-108</span><span class="rtx">Invitation d&rsquo;intervenants externes avec périmètre de cellules et date d&rsquo;expiration obligatoires, révocables en un geste depuis le cockpit.</span></li>
    <li><span class="rid">EF-109</span><span class="rtx">Liste des sessions actives par appareil, avec déconnexion unitaire ou globale immédiate.</span></li>
    <li><span class="rid">EF-110</span><span class="rtx">Un utilisateur peut appartenir à plusieurs organisations et bascule de l&rsquo;une à l&rsquo;autre sans se reconnecter — cas des consultants et des groupes multi-entités.</span></li>
@@ -701,6 +703,7 @@ donc immuable, et la règle est portée par le schéma.</span></li>
    <li><span class="rid">EF-402</span><span class="rtx">Les entrées sont immuables. Une correction crée une entrée liée à l&rsquo;originale par une relation de rectification&nbsp;; les deux restent visibles, l&rsquo;originale étant barrée et non supprimée.</span></li>
    <li><span class="rid">EF-403</span><span class="rtx">Consignation en un clic depuis n&rsquo;importe quel contexte&nbsp;: un message, un document, une décision, un tiers activé, un instant d&rsquo;une visio. L&rsquo;entrée conserve le lien vers l&rsquo;objet d&rsquo;origine.</span></li>
    <li><span class="rid">EF-404</span><span class="rtx">Versement automatique dans la main courante des événements structurants du journal&nbsp;: ouverture et clôture, création de cellule, changement de gravité, début et fin de visio, début d&rsquo;enregistrement, activation d&rsquo;un tiers, arrivée d&rsquo;un intervenant externe, envoi d&rsquo;une mobilisation.</span></li>
    <li><span class="rid">EF-404a</span><span class="rtx">Recherche et filtres sur la main courante&nbsp;: le texte se cherche sans accents et sans égard à la casse — « securite » trouve « Sécurité », car on ne tape pas d&rsquo;accents à trois heures du matin — et le caractère « % » se cherche littéralement au lieu de tout rendre. Les genres se filtrent ensemble, un genre inconnu est ignoré plutôt que de vider la liste, et le filtre par auteur ne rend que ses entrées. Chaque entrée porte le nom de qui l&rsquo;a consignée.</span></li>
    <li><span class="rid">EF-405</span><span class="rtx">Niveau de diffusion par entrée&nbsp;: cellule seule, toutes cellules, direction seulement. Les entrées non visibles n&rsquo;apparaissent pas, mais leur existence est comptabilisée dans l&rsquo;export destiné au directeur de crise.</span></li>
    <li><span class="rid">EF-406</span><span class="rtx">Horodatage relatif au déclenchement affiché à côté de l&rsquo;heure absolue&nbsp;: « H+04:12 ». C&rsquo;est ainsi que les cellules raisonnent réellement.</span></li>
    <li><span class="rid">EF-407</span><span class="rtx">Filtres combinables&nbsp;: cellule, type, auteur, période, mot-clé, criticité. Les filtres se partagent par lien.</span></li>
@@ -717,6 +720,7 @@ donc immuable, et la règle est portée par le schéma.</span></li>
    <li><span class="rid">EF-503</span><span class="rtx">Registre des actions&nbsp;: intitulé, responsable, échéance, cellule, statut, lien vers la décision ou l&rsquo;entrée qui l&rsquo;a engendrée.</span></li>
    <li><span class="rid">EF-504</span><span class="rtx">Deux vues sur les actions&nbsp;: liste triable et tableau par statut. Les actions en retard sont signalées dans le cockpit et dans la barre d&rsquo;état.</span></li>
    <li><span class="rid">EF-505</span><span class="rtx">L&rsquo;historique d&rsquo;une action ou d&rsquo;une décision est présenté comme une suite d&rsquo;événements — qui l&rsquo;a créée, réaffectée, reportée, close — et non comme un état final (EB-15).</span></li>
    <li><span class="rid">EF-505a</span><span class="rtx">Qui peut porter une action dépasse les membres d&rsquo;une cellule&nbsp;: celui qui a ouvert la crise figure dans la liste et y est signalé comme étant soi-même, les cellules se rendent séparément pour se lire groupées, et une personne de l&rsquo;annuaire sans compte est proposée avec son rôle de crise. Une action se confie à quelqu&rsquo;un qui n&rsquo;a pas de compte — un prestataire, un élu, un technicien d&rsquo;astreinte chez un tiers.</span></li>
    <li><span class="rid">EF-506</span><span class="rtx">Vue personnelle « ce qui m&rsquo;incombe », accessible en un geste depuis n&rsquo;importe quel écran.</span></li>
  </ul>

@@ -740,7 +744,10 @@ donc immuable, et la règle est portée par le schéma.</span></li>
    <li><span class="rid">EF-705</span><span class="rtx">Messages épinglés en tête de salon, pour les consignes en vigueur.</span></li>
    <li><span class="rid">EF-706</span><span class="rtx">Présence et indicateur de saisie, indispensables pour savoir qui est réellement mobilisé à 3 h du matin.</span></li>
    <li><span class="rid">EF-707</span><span class="rtx">Recherche plein texte sur l&rsquo;ensemble des salons accessibles à l&rsquo;utilisateur.</span></li>
    <li><span class="rid">EF-707a</span><span class="rtx">Portée des dépêches temps réel&nbsp;: un salon privé n&rsquo;adresse ses messages qu&rsquo;à ses seuls membres, un salon ouvert n&rsquo;adresse à personne en particulier puisque toute la crise peut le lire. La portée se décide au serveur, jamais au client.</span></li>
    <li><span class="rid">EF-707b</span><span class="rtx">« @tous » interpelle le salon entier, avec ses variantes usuelles — « @all », « @ici », « @here », « @canal », « @channel ». Un mot qui commence pareil n&rsquo;interpelle pas&nbsp;: « @allocation » est un mot. La mention est inscrite avec le message, non recalculée à la lecture.</span></li>
    <li><span class="rid">EF-708</span><span class="rtx">Messages directs entre membres, exclus du journal de crise et de l&rsquo;export du dossier, avec mention explicite de cette exclusion dans l&rsquo;interface (EB-10).</span></li>
    <li><span class="rid">EF-708a</span><span class="rtx">Conversations flottantes&nbsp;: réduire et fermer sont deux gestes distincts, et une conversation qu&rsquo;on ferme ne se réouvre pas d&rsquo;elle-même parce qu&rsquo;un message est arrivé entre-temps.</span></li>
    <li><span class="rid">EF-709</span><span class="rtx">Bandeau permanent dans tout salon comptant un intervenant externe, nommant les personnes concernées.</span></li>
  </ul>

@@ -778,6 +785,7 @@ donc immuable, et la règle est portée par le schéma.</span></li>
  <ul class="reqs">
    <li><span class="rid">EF-1001</span><span class="rtx">Tableaux collaboratifs en temps réel, plusieurs par cellule, avec curseurs nommés des participants.</span></li>
    <li><span class="rid">EF-1002</span><span class="rtx">Outils de base&nbsp;: formes, flèches, texte, notes adhésives, dessin libre, images déposées.</span></li>
    <li><span class="rid">EF-1002a</span><span class="rtx">Bibliothèque de modèles de tableau, portée par l&rsquo;organisation&nbsp;: vide pour une organisation neuve, et cela se dit sans erreur. Ce qu&rsquo;on y ajoute survit au rechargement et la crise suivante la trouve — c&rsquo;est une préparation, pas une préférence. Sa taille est bornée à 2&nbsp;Mio&nbsp;; au-delà, l&rsquo;ajout est refusé sans rien écraser de ce qui existait.</span></li>
    <li><span class="rid">EF-1003</span><span class="rtx">Modèles de crise fournis&nbsp;: chronologie d&rsquo;incident, cartographie du SI et des dépendances, arbre d&rsquo;attaque, matrice de décision, organigramme de cellule.</span></li>
    <li><span class="rid">EF-1004</span><span class="rtx">Instantanés périodiques et à la demande, versés en GED, référencés au journal, consignables en main courante — c&rsquo;est ainsi qu&rsquo;un tableau devient une pièce du dossier.</span></li>
    <li><span class="rid">EF-1005</span><span class="rtx">Le rembobinage d&rsquo;un tableau restitue l&rsquo;instantané en vigueur à l&rsquo;instant demandé, ce qui permet de rejouer l&rsquo;évolution de la compréhension de la situation.</span></li>
@@ -796,7 +804,7 @@ donc immuable, et la règle est portée par le schéma.</span></li>
    <li><span class="rid">EF-1106</span><span class="rtx"><span class="later">v2</span> Appel vocal automatisé avec message de synthèse et confirmation au clavier, pour les mobilisations nocturnes.</span></li>
    <li><span class="rid">EF-1103c</span><span class="rtx"><strong>Réponses personnalisées et typées</strong> <em>(RG-318)</em>&nbsp;: dix au plus par campagne, chacune <strong>positive</strong>, <strong>négative</strong> ou <strong>neutre</strong>. Le type, et non le libellé, est ce que le moteur lit&nbsp;: positive compte pour l&rsquo;objectif et arrête la recherche&nbsp;; négative cesse les relances et sollicite le suppléant tout de suite&nbsp;; neutre cesse les relances sans rien compter ni escalader. Une campagne peut donc inventer ses mots — «&nbsp;je prends l&rsquo;astreinte&nbsp;», «&nbsp;je me connecte à distance&nbsp;» — sans qu&rsquo;aucune règle change. Le jeu est préparé à froid, gelé au lancement, et proposé aussi bien sur l&rsquo;application que sur le lien reçu par SMS. Le texte vocal qui associe chaque touche à sa réponse est produit&nbsp;; le <em>recueil</em> de la touche frappée suppose la téléphonie entrante du lot L12, hors périmètre actuel.</span></li>
    <li><span class="rid">EF-1107</span><span class="rtx"><strong>Anti-doublon</strong> <em>(RG-221)</em>&nbsp;: une fenêtre en secondes, réglable par organisation, pendant laquelle un déclenchement identique est <em>refusé</em> et non rejoué. Un système de détection ne déclenche pas une fois&nbsp;: il déclenche tant que la condition dure — une sonde qui perd sa liaison réémet toutes les trente secondes, ce qui ferait cent vingt crises en une heure, chacune avec ses SMS et ses relances. Le rapprochement se fait sur la référence fournie par l&rsquo;émetteur, ou à défaut sur l&rsquo;intitulé et le type. La même règle vaut entre humains&nbsp;: deux personnes de la cellule qui lancent le même appel à trente secondes d&rsquo;écart. Zéro désactive, pour l&rsquo;organisation qui déduplique déjà en amont.</span></li>
    <li><span class="rid">EF-1108</span><span class="rtx"><strong>Alertes multilingues</strong> <em>(RG-291 à RG-293)</em>&nbsp;: jusqu&rsquo;à cinq versions par alerte, chacun recevant celle de sa langue déclarée. Sans langue déclarée, ou dans une langue qu&rsquo;on n&rsquo;a pas rédigée, on reçoit la version par défaut — <strong>jamais rien</strong>&nbsp;: un message qu&rsquo;on lit mal vaut infiniment mieux qu&rsquo;un silence. Les relances et les escalades suivent la langue de la personne, y compris celle du suppléant. La langue par défaut voyage jusqu&rsquo;à la passerelle vocale, dont le gabarit demande la voix de synthèse à son fournisseur&nbsp;: Kastell ne synthétise pas, et prétendre choisir la voix serait prétendre connaître un catalogue qu&rsquo;il ne connaît pas.</span></li>
    <li><span class="rid">EF-1108</span><span class="rtx"><strong>Alertes multilingues</strong> <em>(RG-291, RG-292 et RG-293)</em>&nbsp;: jusqu&rsquo;à cinq versions par alerte, chacun recevant celle de sa langue déclarée. Sans langue déclarée, ou dans une langue qu&rsquo;on n&rsquo;a pas rédigée, on reçoit la version par défaut — <strong>jamais rien</strong>&nbsp;: un message qu&rsquo;on lit mal vaut infiniment mieux qu&rsquo;un silence. Les relances et les escalades suivent la langue de la personne, y compris celle du suppléant. La langue par défaut voyage jusqu&rsquo;à la passerelle vocale, dont le gabarit demande la voix de synthèse à son fournisseur&nbsp;: Kastell ne synthétise pas, et prétendre choisir la voix serait prétendre connaître un catalogue qu&rsquo;il ne connaît pas.</span></li>
  </ul>

  <h3>6.12 &nbsp;Clôture et restitution</h3>
@@ -809,6 +817,16 @@ donc immuable, et la règle est portée par le schéma.</span></li>
    <li><span class="rid">EF-1206</span><span class="rtx">Les enseignements se convertissent en actions de préparation portées par l&rsquo;organisation, suivies hors crise et rattachées au dossier de préparation.</span></li>
    <li><span class="rid">EF-1207</span><span class="rtx">Crise archivée en lecture seule intégrale, rembobinable des années plus tard sans altération ni dégradation (EB-17).</span></li>
  </ul>

  <h3>6.13 &nbsp;Veille externe</h3>
  <p>Ce qui se dit dehors fait partie de la crise. Une cellule qui apprend par la presse
  ce qu&rsquo;elle aurait pu lire une heure plus tôt perd cette heure&nbsp;; et la question
  d&rsquo;un journaliste se prépare mieux avant qu&rsquo;il n&rsquo;appelle.</p>
  <ul class="reqs">
    <li><span class="rid">EF-1301</span><span class="rtx">Lecture de flux publics, sans bibliothèque tierce&nbsp;: titre et résumé réunis, balises retirées, date de publication d&rsquo;origine lue. Seul ce qui mentionne un terme surveillé est retenu, le signal porte le terme qui l&rsquo;a fait entrer, et un second passage ne duplique rien.<br>L&rsquo;adresse d&rsquo;une source est saisie par l&rsquo;organisation, mais c&rsquo;est le serveur qui va la chercher&nbsp;: elle vaut donc une requête émise depuis l&rsquo;intérieur du réseau, et l&rsquo;inscription étant en libre-service, quiconque peut créer un compte y a accès. Une source ne peut viser ni le point de métadonnées d&rsquo;un hébergeur, ni la boucle locale, ni un nom qui y résout, ni une adresse privée en IPv4 comme en IPv6, ni une IPv4 encapsulée en IPv6&nbsp;; elle ne transporte pas de jeton en clair&nbsp;; et un flux qui ne s&rsquo;arrête jamais est écarté. Le contrôle ne s&rsquo;élargit pas au point d&rsquo;écarter les adresses publiques voisines des plages privées.</span></li>
    <li><span class="rid">EF-1302</span><span class="rtx">Retenir un signal l&rsquo;inscrit à la crise et le verse en main courante avec sa source et son auteur, l&rsquo;heure de publication d&rsquo;origine conservée telle quelle, et l&rsquo;adresse d&rsquo;origine attachée. Le signal retenu porte le numéro de séquence de son inscription&nbsp;: ce qui a été versé se retrouve dans le journal, et réciproquement.</span></li>
    <li><span class="rid">EF-1303</span><span class="rtx">Un signal jamais retenu s&rsquo;efface au bout de trente jours&nbsp;; celui qu&rsquo;on a retenu survit à la purge, puisqu&rsquo;il est devenu une entrée de journal. La veille n&rsquo;est pas une archive&nbsp;: ce qu&rsquo;on garde, on l&rsquo;a gardé exprès.</span></li>
  </ul>
</section>

<!-- ============ 7 ============ -->
@@ -1044,6 +1062,7 @@ donc immuable, et la règle est portée par le schéma.</span></li>
    <li><span class="rid">EX-36</span><span class="rtx">L&rsquo;offre de code source exigée par la clause réseau de l&rsquo;AGPL est servie par le produit&nbsp;: adresse configurable par l&rsquo;exploitant, affichée sur les écrans publics et exposée par la sonde d&rsquo;état, lisible sans compte.</span></li>
    <li><span class="rid">EX-37</span><span class="rtx">La révision exacte de l&rsquo;artefact est inscrite à la construction et publiée avec l&rsquo;offre de code&nbsp;; à défaut, la console d&rsquo;administration signale que l&rsquo;offre ne désigne pas la version en cours d&rsquo;exécution.</span></li>
    <li><span class="rid">EX-38</span><span class="rtx">Un accord de contribution — personnes physiques et entités — encadre les apports externes&nbsp;: titularité conservée par le contributeur, licence large incluant le relicenciement, engagement réciproque de publication libre. Un registre tient lieu de signature et sa couverture de l&rsquo;historique est contrôlée par un outil.</span></li>
    <li><span class="rid">EX-39</span><span class="rtx">Chaque garantie de la suite de vérification porte l&rsquo;identifiant d&rsquo;une exigence de ce cahier, et la suite refuse de passer si l&rsquo;un d&rsquo;eux n&rsquo;y figure pas. Sans cela, une garantie peut survivre à l&rsquo;exigence qui la justifiait, et qui remonte d&rsquo;un contrôle vers sa raison d&rsquo;être tombe sur un cul-de-sac — ce qui s&rsquo;est produit pour douze d&rsquo;entre elles.</span></li>
  </ul>

  <div class="note">