Aller au contenu principal
Registre de l'IA Activation de l'IA

Comment ce registre a été construit

Cette page rend public le journal de développement de l'application : les demandes reçues mot pour mot, les options écartées et les corrections apportées. Elle est mise à jour à chaque déploiement.

Cette application a été conçue et construite avec l'assistance de l'intelligence artificielle. Pour une équipe d'activation de l'IA, « où l'humain a-t-il gardé la main ? » est exactement la question que pose tout cadre de gouvernance. Nous y répondons ici sur notre propre cas, plutôt que de le promettre.

Décisions humaines
11
Propositions de l'IA acceptées
4
Corrections et impasses
17

Journal de développement

11 · 4 · 17

Journal — 2026-08-28

Jour 1. Cadrage, conception, échafaudage.


09:40 — Demande initiale · décision humaine

« je fais maintenant parti d'une équipe "AI Enablement / Activation de l'IA", c'est une porte d'entrée vers l'information, les ressources et les initiatives visant à promouvoir l'utilisation responsable et efficace de l'intelligence artificielle au sein de notre organisation. […] Je vais devoir convertir un dashboard powerbi en une vrai base de données mysql relationel. Donc au lieu d'être des lignes simple, on pourra vraiment faire un suivi de chaques initiative avec des changelog, faire des tableau de suivi, etc. Ce sera comme tu t'en doute, une base de données qui devra générer un paquet de KPI, gant chart, à la manière d'un gestionnaire de projet. Il faut que ce soit simple visuellement et très moderne, mais aussi très complet pour rendre des compte aux gestionnaire et aux équipes qui ont participé. Le tout commence par un Formulaire de demande d'inscription au registre de l'IA. Ensuite, un suivi pour chacun. Garde en tête que le site doit impérativement être bilingue, c'est primordiale. Tu va créer ai.madeli.co pour monter ce système. »

Accompagnée du contenu intégral du formulaire Microsoft Forms existant (26 questions), reproduit dans la spec.

Suivi de : « Une fois ceci créer, je pourrai uploader des screenshot du registre actuel et ensuite les data en fichier excel. »

Lecture retenue : le point de départ n'est pas un tableau de bord, c'est un formulaire d'admission. Le Power BI actuel est un symptôme (des lignes plates), pas la cible.


09:45 — Drapeau levé : hébergement et sensibilité · proposition de l'IA

ai.madeli.co est un serveur personnel. Le formulaire comporte une question « Classification des données » et l'intention annoncée est d'y importer les vraies données du registre ministériel. Signalé une fois, avec la précision que la décision appartient à l'humain.

Conséquence : a directement motivé la question de cadrage nº 1.


09:50 — Cinq questions de cadrage · décisions humaines

Chaque question posée avec ses options et une recommandation.

1. Vocation de ai.madeli.co

Options : prototype de démonstration · outil de travail réel avec données réelles · les deux cloisonnés · maquette d'abord.

Retenu : prototype de démonstration. Données fictives ou anonymisées.

Écarté : l'outil de travail réel, qui aurait imposé un durcissement complet de la sécurité sur un serveur personnel. Effet : l'enjeu bascule de la conformité vers la force de démonstration.

2. Contenu bilingue

Options : champs FR + EN côte à côte · saisie unique avec langue marquée · saisie unique + traduction assistée par API · table de traductions générique.

Retenu : champs FR + EN côte à côte, avec repli sur l'autre langue et badge « non traduit ».

Écarté : la traduction assistée par API — séduisante en démonstration, mais elle introduisait une dépendance externe et des coûts pour un prototype. La table de traductions générique a été écartée pour son coût sur chaque requête de KPI.

3. Modèle d'accès

Options : formulaire public + admin protégé · tout derrière un login · tout public sauf l'écriture · un seul rôle admin.

Retenu : formulaire public + espace admin protégé, avec suivi du demandeur par lien à jeton, sans compte.

Raison : le lien du formulaire doit pouvoir être envoyé à n'importe qui au ministère sans créer de compte.

4. Modèle de suivi dans le temps

Options : cycle de vie normalisé + jalons libres · phases entièrement libres · cycle normalisé seul · suivi léger.

Retenu : cycle de vie normalisé (7 étapes) + jalons libres.

Écarté : les phases entièrement libres, qui rendaient impossible tout KPI de portefeuille — on ne peut pas calculer un délai moyen par étape si chaque initiative invente ses étapes.

5. Découpage des livraisons

Retenu : livraison 1 = fondations (schéma, formulaire, registre, fiche, changelog, admin). Livraison 2 = KPI et Gantt. Livraison 3 = import Excel.

Écarté : « le wow d'abord » (tableau de bord avant le formulaire) — un tableau de bord sans moyen d'alimenter les données est une coquille.


10:05 — Deux décisions prises par l'IA, annoncées et non contestées

Tables de référence modifiables plutôt que listes codées en dur. L'humain n'avait pas fourni les valeurs des listes déroulantes. Plutôt que d'attendre, les référentiels sont pré-remplis de valeurs plausibles et modifiables dans l'admin. Confirmé ensuite explicitement : « le formulaire contient des choix déroulant que je ne t'ai pas donné, ont le fera en l'ajustant à la suite de la premiere livraison ».

Changelog automatique par observer Eloquent plutôt que journalisation manuelle dans chaque contrôleur. Une journalisation qu'il faut penser à écrire est une journalisation qui finit par mentir.


10:10 — Conception présentée · décision humaine : approuvée

Points structurants du design, au-delà de ce qui avait été demandé :

  • Les contacts deviennent des entités. Les 6 champs de courriel du formulaire ne sont pas 6 colonnes mais une table personnes et une table de liaison portant le rôle. C'est ce qui permet « montre-moi toutes les initiatives où cette personne est experte » — impossible dans le Power BI.
  • Deux statuts distincts, statut_dossier (le triage de la demande) et etape_actuelle (le cycle de vie de l'initiative). Les confondre aurait faussé tous les KPI de délai.
  • Deux scores calculés, faisabilité (Q18, sur 8 critères) et valeur (Q16 + Q17 pondérés), donnant une matrice valeur × faisabilité. Dérivés gratuitement de données déjà collectées mais jamais exploitées.

Réponse : « oui, vas-y et ont ajustera au besoin à mesure ».


10:20 — Exigence de traçabilité · décision humaine

« garde tout au long du processus des traces de changelog, des prompts, etc. pour voir l'évolution de l'application »

« la transparance est clé dans ce projet »

Arrivée en cours de travail, après l'approbation de la conception.

Traitement : promue au rang de livrable plutôt que de pratique de coulisses. Ajout de la section 15 à la spec et d'une page /transparence dans l'application, distinguant les décisions humaines, les propositions de l'IA acceptées, et les corrections.

Raisonnement : pour une équipe d'activation de l'IA, « où l'humain a-t-il gardé la main ? » est précisément la question que pose tout cadre de gouvernance. Le registre peut y répondre sur son propre cas — ce qui en fait un exemple plutôt qu'une simple promesse.


10:25 — Échafaudage · construction

  • Laravel 13.29.0 installé dans /var/www/ai.madeli.co.
  • Dépôt Git initialisé.
  • Spec de conception écrite et versionnée (docs/superpowers/specs/2026-08-28-registre-ia-design.md, 15 sections).
  • Pile alignée sur les autres applications du serveur : Apache + mod_php 8.4, MySQL 8.4, Tailwind 4, Vite 8, Alpine 3, Pest 4.

Correction en cours de route : le premier composer create-project a échoué sur mkdir(): Permission denied/var/www appartient à root. Répertoire créé au préalable en maxime:www-data, puis installation en place.


10:40 — Tâche 1 : configuration · construction

Base ai_registre et utilisateur MySQL dédié créés. Fuseau America/Halifax, locale fr par défaut, locales_supportees ajouté à config/app.php. Chaîne Vite 8 + Tailwind 4 + Alpine 3 en place, thème marine/cyan écrit.

Décision de l'IA : les tests tournent sur MySQL, pas sur SQLite en mémoire. Le squelette Laravel configure SQLite par défaut. Écarté : le schéma s'appuie sur des index FULLTEXT, des ENUM et des verrous de ligne que SQLite n'a pas — tester ailleurs que sur le moteur de production donnerait une confiance fausse. Base ai_registre_test créée à cet effet.

Instruction suivie : le squelette Laravel dépose un CLAUDE.md demandant d'installer Laravel Boost avant toute modification. Fait. Boost a réécrit CLAUDE.md avec ses guidelines et installé quatre compétences (laravel-best-practices, testing-best-practices, tailwindcss-development, infer-conventions), lues et appliquées.

Trois impasses, et comment elles ont été levées

  1. composer create-project a échoué sur mkdir(): Permission denied/var/www appartient à root. Répertoire créé au préalable en maxime:www-data.
  2. Pest 4 refusait de s'installer : conflit avec laravel/pao v1.1.4, une dépendance de développement du squelette. laravel/pao retiré — il n'est pas utilisé par ce projet.
  3. npm run build a échoué sur « Cannot find native binding » de Rolldown, un bogue npm connu sur les dépendances optionnelles. @rolldown/binding-linux-x64-gnu déclaré explicitement, comme sur cafe.madeli.co.

Correction : APP_ENV était réglé à production dès le départ, ce qui empêchait Boost d'enregistrer ses commandes. Repassé à local ; le basculement en production se fera à l'étape de déploiement, pas avant.

Tests : 3 verts (SanteTest).

11:05 — Tâche 2 : référentiels bilingues · construction

Quatorze tables de référence créées par une migration unique en boucle, deux traits partagés (Traduisible, EstReferentiel), quatorze modèles minces, et un seeder rejouable de 116 valeurs.

Distinction consignée : les libellés repris mot pour mot du formulaire d'origine — les 9 alignements stratégiques, les 8 critères de faisabilité, les 10 catégories d'outils, les 8 sources et les 8 types de données — sont définitifs. Les autres (secteurs, régions, programmes, types d'utilisateurs) sont plausibles et provisoires, en attente des listes réelles du client. Le remplacement se fera par l'écran d'administration, sans migration.

Tests : 14 verts (TraduisibleTest 5, ReferentielsTest 9).

11:35 — Tâche 3 : noyau relationnel · construction

personnes, initiatives, la liaison porteuse de rôle, et sept tables de liaison pour les questions à choix multiples (Q15 à Q21).

Deux corrections en cours de route :

  1. Noms d'index trop longs. La migration des liaisons a échoué à mi-course : le nom auto-généré initiative_critere_faisabilite_initiative_id_critere_faisabilite_id_unique fait 73 caractères, au-delà de la limite de 64 de MySQL. Index nommés explicitement (ini_cri_unique). Les deux tables déjà créées ont dû être supprimées à la main avant de rejouer.

  2. FULLTEXT abandonné au profit de LIKE — décision de l'IA. Le plan prévoyait un index FULLTEXT pour la recherche. Il s'avère que les index FULLTEXT d'InnoDB ne sont mis à jour qu'au commit : comme RefreshDatabase enveloppe chaque test dans une transaction, aucune recherche plein texte ne voit les lignes créées par le test. Le choix était donc entre une recherche non testable et une recherche testable. Retenu : un scope recherche() en LIKE sur les six colonnes de texte plus le numéro, avec échappement des jokers % et _ saisis par l'utilisateur. Amplement suffisant pour le volume d'un registre d'initiatives ; à revoir si le portefeuille atteint des dizaines de milliers de fiches.

    Effet secondaire heureux : la collation accent-insensible fait le pont entre les langues — un gestionnaire anglophone qui tape « detection » trouve la fiche française « Détection acoustique ». Couvert par un test.

Correction d'une de mes propres attentes de test : j'avais écrit que la recherche acoustic devait trouver deux fiches, dont acoustique. C'est faux — acoustic n'est pas une sous-chaîne d'acoustique. Test corrigé plutôt que code tordu pour le satisfaire.

Tests : 14 verts (InitiativeRelationsTest).

12:00 — Tâche 4 : suivi dans le temps et journal · construction

etapes_initiative (prévu et réel côte à côte, la table qui rendra le Gantt possible), jalons (points datés libres, avec drapeau visible_demandeur) et journal_entrees.

Deux choix du journal portent l'exigence de transparence : aucune colonne updated_at — une entrée ne se modifie jamais — et user_id en nullOnDelete — supprimer un compte n'efface pas l'historique qu'il a produit. Les deux sont couverts par un test.

Correction : les valeurs par défaut n'existaient qu'en base, si bien qu'un modèle fraîchement créé rapportait null là où la base avait true. Déclarées aussi dans $attributes : une instance en mémoire doit dire la même chose qu'après rechargement.

Tests : 37 verts au total.

12:35 — Tâche 5 : bilinguisme, thème et coquille · construction

Routes préfixées /fr et /en, middleware de locale, helpers route_locale() et url_autre_langue(), fichiers de langue, coquille accessible (lien d'évitement, <html lang>, hreflang), thème marine/cyan, page d'accueil avec trois chiffres de portefeuille.

La langue voyage dans l'URL et non en session : une adresse partagée par courriel arrive toujours dans la langue de celui qui l'a envoyée, et deux onglets ouverts dans deux langues ne se contredisent pas. La bascule conserve la chaîne de requête — un registre filtré reste filtré après un changement de langue.

Correction d'un test qui mesurait le mauvais objet. J'avais écrit que / sans en-tête Accept-Language devait rediriger vers /fr. Le test échouait en donnant /en. Cause : le client de test de Laravel injecte lui-même un Accept-Language: en-us par défaut — le test mesurait donc un artefact du client, pas le comportement du site. Réécrit en deux tests explicites : un en-tête français donne /fr, un en-tête vide donne /fr.

Les deux tests d'exemple du squelette Laravel ont été supprimés : l'un attendait un 200 sur /, qui redirige désormais vers la langue.

Tests : 51 verts.

13:15 — Tâches 6 à 9 : la couche de services · construction

Quatre services, chacun gardien d'un invariant que les contrôleurs ne doivent pas pouvoir enfreindre.

  • ScoreService — faisabilité (critères cochés sur 8) et valeur (impact et alignement pondérés 40/60). Écrit en saveQuietly : un score recalculé est une conséquence, pas un événement digne du journal.
  • NumeroServiceIA-AAAA-NNNN sous verrou de ligne, et le jeton de suivi de 40 caractères.
  • JournalService + trois observers — le changelog s'écrit tout seul. Une liste blanche de 24 champs évite que les scores recalculés noient les changements qui comptent.
  • CycleVieService — seul endroit du code autorisé à toucher etape_actuelle_id et etapes_initiative.

Décision consignée : une étape enjambée sans jamais avoir commencé est marquée « sautée » et non « terminée ». La marquer terminée gonflerait le taux de complétion et fausserait le délai moyen par étape de la livraison 2. On dit donc la vérité, même quand elle est moins flatteuse.

Tests : 88 verts.

14:20 — Tâches 10 et 11 : formulaire public et courriels · construction

Les 26 questions du formulaire d'origine, réparties sur cinq étapes, avec barre de progression, compteur de caractères annoncé aux lecteurs d'écran, brouillon sauvegardé dans le navigateur et leurre anti-pourriel. Courriel de confirmation au demandeur avec lien de suivi, notification à l'équipe.

Six champs seulement sont obligatoires (contact principal, secteur, titre, description, problème, statut actuel). Décision assumée : le formulaire est un guide de réflexion, pas un contrôle d'accès. Un demandeur bloqué par une question qu'il ne sait pas remplir abandonne, et l'initiative n'entre jamais au registre. Mieux vaut une fiche incomplète qu'une fiche absente.

Corrections :

  1. La précision « Autre » se recopiait sur tous les choix du groupe. Repéré à la relecture avant l'exécution des tests. Corrigé : la précision n'est écrite que sur la ligne dont le code est autre, sinon « SharePoint » paraîtrait avoir été précisé.

  2. Le sujet des courriels dépendait du chemin d'appel. Un test a montré qu'un accusé pour une demande française partait avec un sujet anglais quand la session était anglaise. Cause : $this->locale() d'un mailable n'est appliqué que dans le pipeline d'envoi, pas à la construction. La locale est maintenant passée explicitement à __() pour le sujet. Le corps était déjà correct — seul le sujet fuyait.

La traduction française complète des messages de validation de Laravel a dû être écrite : le framework n'en fournit pas.

Tests : 114 verts.

15:40 — Tâches 12 à 19 : suivi, administration, transparence, démonstration · construction

Page de suivi par jeton, espace d'administration complet (authentification à deux rôles, registre filtrable, fiche détaillée, triage, étapes, jalons, journal, référentiels, utilisateurs), page de transparence, jeu de quinze initiatives de démonstration, en-têtes de sécurité et revue d'accessibilité.

Un vrai défaut trouvé par un test d'accessibilité. Le test « décrit l'état des étapes par un texte » échouait parce qu'aucune étape n'apparaissait « en cours ». Cause réelle : CycleVieService::initialiser() n'ouvrait la première étape que si etape_actuelle_id était nul. Quand ce champ était déjà renseigné, les sept lignes restaient « à venir » — l'invariant de la classe, « exactement une étape en cours et c'est celle que pointe etape_actuelle_id », était violé dès l'initialisation. Corrigé : le service ouvre désormais toujours une étape, celle que pointe l'initiative si elle en désigne une.

C'est exactement le genre de défaut qu'un test d'interface trouve alors qu'un test de service ne le voyait pas : le test unitaire du cycle de vie passait, parce qu'il partait toujours d'une initiative sans étape courante.

Trois corrections plus mineures :

  • Le composant badge-traduction vivait dans partials/ ; les composants anonymes de Blade doivent être dans views/components/.
  • Le contrôleur de base de Laravel n'inclut plus AuthorizesRequests depuis la version 11 — toutes les autorisations de l'administration échouaient.
  • Un test de validation mesurait la mauvaise page : sans from(), la redirection de validation renvoie à la racine et non au formulaire.

Tests : 201 verts, 660 assertions.


16:30 — Le registre devient public · décision humaine

« dans un soucis de transparence, la consultation du registre est publique. seulement sa modification nécessite une connexion sécurisé »

Arrivée après le déploiement de la livraison 1. Renverse la réponse donnée à la question de cadrage nº 3, où le registre entier avait été placé derrière une authentification.

Pourquoi c'est juste : un registre de l'IA qui prêche la transparence mais cache son contenu derrière un login se contredit. La nouvelle règle est plus cohérente avec l'objet même du système.

Deux questions posées avant de construire, et leurs réponses :

Portée publique — retenu : uniquement ce qui est au registre. Les initiatives acceptées et archivées sont publiques ; les demandes en attente de triage et les demandes refusées restent internes. Raison : une demande non encore examinée est un document de travail, et la publier avant que l'équipe l'ait seulement lue découragerait les gens de soumettre — ce qui coûterait plus à la transparence que le peu qu'on y gagnerait. Écarté : la publication des refus, forte symboliquement mais dure pour le demandeur.

Contacts — retenu : nom et rôle, jamais l'adresse. Les courriels restent visibles dans l'espace authentifié. Raison : ce sont des données personnelles que les personnes concernées n'ont pas consenti à voir publiées, et les publier les exposerait aux robots collecteurs. Une personne sans nom renseigné n'est pas affichée du tout, plutôt que de retomber sur son adresse.

Trois secrets ne sortent jamais du registre public : la note interne, le jeton de suivi du demandeur, et les adresses de courriel. Le journal public ne montre que les événements structurants et les commentaires explicitement rendus visibles — jamais une modification portant sur note_interne ou motif_rejet. Chacun de ces points a son test.

Tests : 215 verts.


17:15 — « Je ne peux pas changer mon utilisateur et mot de passe » · correction

Signalement de l'utilisateur en production. Il avait raison, et l'enquête a révélé trois défauts empilés, dont deux qu'aucun test ne voyait.

1. L'interface de modification n'existait pas. L'écran Utilisateurs affichait la liste, permettait de créer et de supprimer — mais je n'avais jamais écrit le formulaire de modification. La route existait, l'interface non.

2. Pire : la page Utilisateurs était cassée depuis sa création. Le fichier Blade contenait __(\"admin.role_{$r}\") — des antislashs littéraux, du PHP invalide, hérités de mon écriture du fichier. La page levait une exception à chaque affichage. Aucun test ne l'a vu, parce que les tests existants n'appelaient que ses routes d'écriture et une route interdite : la vue n'était jamais rendue. Le même motif était présent dans l'écran d'édition d'une initiative.

3. role et langue n'étaient pas assignables en masse. Laravel 13 déclare les champs assignables par attribut PHP #[Fillable([...])] et non par propriété $fillable ; ma modification de la tâche 13 visait la propriété et n'a donc jamais pris effet. fill() jetait les deux champs en silence.

Le plus instructif : le test « crée un utilisateur avec un rôle » passait par accident. Il vérifiait que le rôle créé valait lecteur — qui est justement la valeur par défaut. Le champ était jeté, la valeur par défaut s'appliquait, et l'assertion passait. Un test qui vérifie une valeur par défaut ne teste rien.

Le compte administrateur de production a survécu parce que db:seed désactive la protection d'assignation en masse pendant son exécution — le seeder marchait là où le code applicatif échouait.

Corrections apportées :

  • Page /mon-compte accessible à tout utilisateur connecté, y compris un lecteur — qui n'a pas accès à l'administration et n'aurait donc eu aucun moyen de changer son propre mot de passe. Changement de mot de passe protégé par la saisie du mot de passe actuel.
  • Formulaire de modification complet par utilisateur dans l'écran d'administration, réinitialisation de mot de passe incluse.
  • Garde-fou : le dernier administrateur ne peut plus se rétrograder en lecteur, ce qui laisserait le registre sans personne capable d'en nommer un autre.
  • #[Fillable] corrigé, et Model::preventSilentlyDiscardingAttributes() activé hors production : une assignation portant sur un champ non déclaré lève désormais une exception au lieu d'être ignorée sans trace.
  • Les composants de champ acceptent un id explicite : plusieurs lignes sur une même page produisaient sinon des id dupliqués, ce qui casse l'association <label for>.
  • Test corrigé pour vérifier un rôle différent de la valeur par défaut.
  • Nouveau fichier PagesSeRendentTest : chaque page de l'application est visitée dans les deux langues. C'est l'absence de cette garantie qui a laissé une page cassée passer en production.

Tests : 247 verts, dont 14 de rendu.


18:00 — Livraison 2 : tableaux de bord et Gantt · décision humaine, puis construction

« vas-y, livraison 2 » … « feu vert »

Trois questions de cadrage posées avant de construire. Retenu : tableaux de bord publics comme le registre ; les quatre indicateurs (entonnoir, matrice valeur × faisabilité, répartitions, délais) ; Gantt en vue portefeuille et par fiche.

La couleur a été calculée, pas choisie · proposition de l'IA

La compétence de visualisation impose de faire valider toute palette par un script plutôt que de juger à l'œil. Fait, et le résultat a corrigé deux de mes intentions :

  1. La rampe marine du thème ne passait pas. Ses pas 600, 700 et 800 sont trop rapprochés en clarté (écarts de 0,057 et 0,059 pour un minimum de 0,06). J'ai généré une rampe à pas de clarté réguliers dans la même teinte, et son sixième pas tombe exactement sur le marine 700 du thème — les graphiques et le site parlent la même couleur sans arrangement.

  2. Mes jeux « Gantt » et « haltères » échouaient au contrôle catégoriel. Le validateur signalait clarté hors bande et chroma trop faible. Il avait raison sur le fond : ce ne sont pas des palettes catégorielles. « Prévu → en cours → réalisé » est une progression, pas une identité. Revalidés comme rampes ordinales, ils passent tous les contrôles. La leçon est dans le pied de page du validateur, que j'aurais pu lire plus tôt : scope : categorical palettes only.

Décision qui en découle : aucune palette catégorielle dans tout le tableau de bord. Les secteurs et les programmes n'ont aucun ordre naturel — ils reçoivent donc une seule et même couleur, la longueur de la barre portant seule l'information. Une rampe sur des catégories sans ordre ré-encoderait la longueur de barre en couleur pour rien.

Un test de garde vérifie qu'aucune couleur hors des rampes validées n'apparaît dans le rendu. C'est lui qui empêchera une teinte inventée de se glisser dans une vue dans six mois.

Trois corrections en cours de route

  • Illuminate\Support\CarbonImmutable n'existe pas : c'est Carbon\CarbonImmutable.
  • Un test de géométrie attrapait les marques des haltères en croyant mesurer le nuage de points ; l'axe vertical des deux graphiques n'a pas les mêmes bornes. Les points du nuage portent désormais une classe qui les identifie.
  • Mon test « une initiative sans aucune date » partait d'une prémisse fausse : initialiser() pose toujours une date réelle sur l'étape qu'il ouvre. Test réécrit sur le vrai cas — des lignes d'étape dont toutes les dates ont été vidées.

Ce que je n'ai PAS pu faire

Je n'ai pas regardé les graphiques. La compétence demande de rendre la page et de l'inspecter à l'œil pour les collisions d'étiquettes et les débordements, que le validateur de couleur ne voit pas. Le téléchargement du navigateur sans interface est bloqué par le réseau (403 sur storage.googleapis.com).

Faute de mieux, j'ai écrit des contrôles de géométrie automatisés : positions hors cadre, identifiants dupliqués, absence de grille en pointillés, absence de valeur sur chaque point. Ils attrapent les défauts mécaniques, pas les collisions d'étiquettes. Un coup d'œil humain reste nécessaire.

Tests : 298 verts, 1383 assertions.

18:40 — Bug d'affichage sur trois graphiques · correction

« bug d'affichage dans le tableau de bord (répartition par secteur, par programme et déais par étape) »

Signalement de l'utilisateur, exactement sur les trois graphiques que j'avais rendus en SVG avec preserveAspectRatio="none" et un viewBox aux unités mixtes — axe X en centièmes, axe Y en pixels. Le navigateur étire alors le contenu horizontalement d'un facteur égal au rapport entre la largeur du conteneur et celle du viewBox, soit environ sept. Le texte est étiré avec le reste et devient illisible.

C'est précisément le défaut qu'une inspection visuelle aurait attrapé et que mes contrôles de géométrie ne voyaient pas. J'avais signalé cette lacune ; elle s'est matérialisée dans l'heure.

Corrigé sans rustine : les deux composants concernés (barres et halteres) sont reconstruits en HTML et CSS, comme l'entonnoir qui, lui, s'affichait correctement depuis le début. Le texte redevient du texte natif et toute la classe de bug disparaît. Les spécifications de marque sont conservées : barre de 24 px, bout arrondi du seul côté de la donnée, anneau blanc de 2 px séparant les marques superposées.

Garde-fou ajouté, et vérifié en le faisant échouer volontairement avant de le garder : aucun SVG contenant du texte ne peut porter preserveAspectRatio="none". Un test qui passe sans rien vérifier ne vaut rien ; celui-ci a bien détecté le défaut quand je l'ai réintroduit à dessein.

Tests : 299 verts.

19:10 — Le registre passe au tableau, avec fiche en modal · décision humaine

« prend en compte qu'il y aura quelques chose comme 250 initiatives. pour le registre un datatable serait peut-être mieux … et un modal quand on clique dessus avec les détails. qu'est-ce que tu en penses? »

Puis, après discussion : « ont va essayer C, ont verra ensuite ».

Position prise, et argumentée plutôt que suivie sans discuter. J'étais d'accord sur le tableau — des cartes en grille sont ingérables à 250 fiches. Je me suis en revanche opposé au modal simple : il ne change pas l'adresse, donc on ne peut plus envoyer le lien d'une initiative à un collègue, ni le mettre en favori, ni s'attendre à ce que le bouton Retour fonctionne. Sur un registre dont l'objet même est la transparence, perdre les URL partageables coûte cher.

Trois options proposées ; retenue : la troisième, le modal avec synchronisation de l'adresse. C'est le meilleur des deux mondes et c'est plus de travail.

Ce qui a été construit :

  • Registre public en tableau dense, trié et paginé côté serveur (25 par page), en-têtes de colonnes cliquables portant aria-sort, cartes empilées sous 768 px. Aucune bibliothèque de type DataTables : elle chargerait les 250 lignes d'un coup, ajouterait une dépendance et cesserait de fonctionner sans JavaScript.
  • Modal synchronisé : ouvrir une fiche pousse son adresse dans l'historique, fermer revient en arrière. Une visite directe de cette adresse, ou une visite sans JavaScript, sert la page complète — le modal n'est qu'un raccourci depuis la liste.
  • Le contenu de la fiche vit dans une seule partie de vue, servie telle quelle à la page complète et au modal : les deux ne peuvent pas diverger. C'est la seule raison d'être de ce fichier.
  • L'en-tête X-Fiche-Fragment porte la demande du modal plutôt qu'un paramètre d'URL : l'adresse est le lien partageable, la polluer d'un ?vue=fragment ruinerait l'intérêt de l'exercice.

Une dépendance ajoutée, et assumée : @alpinejs/focus, pour le piège de focus du modal. Un piège de focus écrit à la main est faux la moitié du temps, et un modal sans piège de focus est inutilisable au clavier. La contrainte du projet dit « aucune dépendance sans nécessité démontrée » ; la nécessité est démontrée.

19:25 — Journal des modifications replié · décision humaine

« le journal des modifications pourait etre colapsé, rare qu'il est consulté, mais utile d'existé. »

Exactement le bon arbitrage, et bien formulé : utile d'exister, rare d'être consulté. Replié, jamais retiré.

Sur la fiche publique, le journal entier passe dans un dépliant fermé annonçant son nombre d'entrées. Côté administration, la nuance compte : le champ de commentaire est utilisé à chaque passage, l'historique presque jamais. Le formulaire reste donc au-dessus du pli et seule la liste des événements se replie — sauf quand un filtre de type est actif, auquel cas elle s'ouvre, parce qu'on ne filtre pas quelque chose qu'on ne veut pas voir.

Tests : 315 verts.

19:45 — « La page de connexion ne fonctionne plus » · correction

« la page de connexion ne fonctionne plus » puis, décisif : « non, je clique sur connexion et ca ne fait que recharger la page ai.madeli.co/fr »

Ma première hypothèse était fausse, et le second message l'a corrigée. J'avais diagnostiqué un problème d'identifiants : le compte [email protected] avait disparu, remplacé par [email protected]. J'avais vérifié que le mécanisme d'authentification fonctionnait (compte jetable créé, connecté, page protégée atteinte, compte supprimé) et j'allais proposer une réinitialisation de mot de passe. Ç'aurait été inutile.

La précision « ça recharge /fr » est la signature exacte du middleware guest : il renvoie vers l'accueil tout utilisateur déjà connecté qui atteint la page de connexion.

Le vrai défaut est le mien : l'en-tête et le pied de page affichaient « Connexion » quel que soit l'état d'authentification. Un utilisateur déjà connecté cliquait donc sur un lien qui, par construction, ne pouvait que le ramener à l'accueil — sans aucun message expliquant pourquoi.

Corrigé : la navigation publique s'adapte désormais à l'état de connexion. Un visiteur anonyme voit « Connexion » ; un utilisateur authentifié voit son nom, un lien vers le registre d'administration, un lien vers son compte et un bouton de déconnexion. La mention « Connecté en tant que… » est délibérée : sans elle, rien n'expliquait le comportement.

Leçon consignée : un lien qui ne peut mener nulle part n'aurait jamais dû être affiché. Et un test qui vérifie qu'une page se rend ne dit rien de la cohérence des chemins qu'elle propose — d'où le nouveau fichier NavigationAuthentifieeTest, qui parcourt les pages publiques dans les deux états.

Tests : 322 verts.


20:30 — Livraison 3 : import du registre existant · décision humaine

« go avec la prochaine livraison » puis, décisif, le chemin du fichier : « ce n'est pas des données sensible, /var/www/ai.madeli.co/data.xlsx »

J'ai lu le fichier avant de concevoir quoi que ce soit, et il a corrigé plusieurs de mes hypothèses. 224 initiatives, huit colonnes seulement — bien moins que les 26 questions du formulaire. Quatre constats ont décidé de la conception :

  • L'identifiant source est la seule clé fiable. Trois fiches s'appellent « Nearshore biotopes model » : le titre ne peut pas servir.
  • Le SME est un nom, pas un courriel, et 62 personnes couvrent 223 lignes. La réutilisation massive que j'avais avancée comme argument de vente du modèle relationnel se vérifie sur les vraies données — une experte apparaît sur 80 initiatives. Mais le schéma exigeait un courriel unique : rendu facultatif.
  • « AI Bucket » est une dimension que le registre n'avait pas.
  • 25 descriptions dépassent la limite du formulaire. L'import ne l'applique pas : elle protège un rédacteur, pas une reprise de données.

Deux questions posées, deux recommandations retenues : « On hold » devient un indicateur qui se superpose à l'étape plutôt qu'une étape — en faire une casserait l'entonnoir ; et l'import propose, l'humain valide, avec mémorisation des correspondances.

La taxonomie fournie en cours de route · décision humaine

L'utilisateur a transmis le document de catégorisation des « AI Buckets ». J'allais créer cinq libellés nus ; chaque catégorie reçoit désormais sa description et son indicateur clé — la phrase qui permet de trancher entre deux catégories voisines. J'ai ajouté les 21 sous-catégories en champ facultatif : la source n'en contient pas, mais la taxonomie fait autorité.

Une hypothèse de fond corrigée, puis maintenue · décision humaine

La présentation « Notre parcours en matière d'IA » révèle que le modèle opérationnel officiel du ministère compte quatre étapes : Idée → Preuve de concept → Pilote → Production. Mon registre en a sept, inventées à la conception. J'ai remonté l'écart et proposé l'alignement.

Réponse : garder les sept. Le modèle officiel décrit le parcours d'une initiative retenue ; le registre suit aussi le triage interne de l'équipe d'activation. Décision de l'utilisateur, qui connaît son processus. Consignée pour ne pas la reproposer.

Trois corrections

  1. Le lecteur de tableur retournait des valeurs vides. Après un appel à children($NS), SimpleXML cherche les attributs dans ce même espace de noms, alors que r et t n'en ont aucun. Le type de cellule revenait vide et le lecteur lisait la mauvaise branche.
  2. Mon rapprochement automatique était trop prudent. Il n'accepte l'inclusion qu'au-delà de huit caractères — « CDOS » en fait quatre et est resté sans correspondance, laissant 95 fiches sans secteur. Correspondances préparées explicitement, avec les secteurs que la source révèle. Il reste 5 valeurs volontairement non attribuées : « Arctic Region » et « Quebec » sont des régions classées comme secteurs, « PSSI », « EMB » et « O&P AE, AIS » sont des sigles dont je ne connais pas le développé. Mieux vaut un champ vide qu'un rattachement inventé.
  3. Les six fiches en pause n'avaient aucune étape. Ma règle « étape conservée » n'a rien à conserver au premier import : elles comptaient au registre tout en disparaissant de l'entonnoir, deux chiffres de la même page se contredisant. Elles s'ouvrent désormais à l'idéation — la seule étape qu'on puisse affirmer sans rien inventer.

Et une assertion de test trop large

Le test « la connexion n'est plus proposée à un utilisateur authentifié » échouait sur /fr/transparence — cette page rend ce journal, qui contient légitimement le mot « Connexion » dans le récit du bug précédent. L'assertion porte désormais sur le lien, pas sur le mot.

État après import

224 fiches, 62 personnes, 223 rattachements. Entonnoir : 221 idéation → 169 développement → 154 pilote → 2 production. Zéro fiche du registre sans étape.

Tests : 351 verts.

21:00 — Un score de zéro n'est pas un score · correction

Signalé par la production, pas par un test : la matrice valeur × faisabilité n'affichait qu'un seul point. Les 224 fiches importées avaient toutes un score de 0 et se superposaient donc à l'origine.

Cause réelle : ScoreService retournait 0 pour une initiative dont aucun critère n'était renseigné. Or un zéro affirme « nous avons évalué, et c'est zéro ». La vérité était « personne n'a évalué » — ces fiches viennent d'un registre antérieur et n'ont jamais été soumises au formulaire.

Les deux scores retournent désormais null quand aucune donnée d'entrée n'existe, et la matrice affiche une phrase qui explique pourquoi elle est vide plutôt que de montrer un point trompeur.

C'est le genre de défaut qu'aucun test ne pouvait attraper tant que les données de démonstration remplissaient toujours les critères.

Question de l'utilisateur, et réponse : « j'ai seulement 221 initiative, au lieu de 224 ? » — Les trois manquantes sont celles dont le statut source était « Resolved via Existing Capability », donc marquées non retenues. Le registre public n'affiche que ce qui est inscrit ou archivé ; elles restent visibles dans l'espace d'administration. Comportement conforme aux deux décisions prises plus tôt, et confirmé par l'utilisateur.

21:30 — L'entonnoir en dents de scie · correction

Visible à l'œil sur les vraies données : l'entonnoir affichait 221 → 0 → 0 → 15 → 146 → 2. Ça ne ressemble pas à un entonnoir.

Cause : je comptais une étape comme « atteinte » seulement si sa ligne était terminée ou en cours. Or l'import fait sauter les étapes intermédiaires — une fiche « Pilot » n'a jamais eu de ligne « Évaluation » datée. Ma définition était défendable sur des données saisies au formulaire ; sur des données reprises d'un registre antérieur, elle produit un graphique illisible.

Nouvelle définition : « au moins à ce stade ». Une fiche arrivée en pilote est nécessairement passée le cap de l'évaluation, même si le registre n'en garde aucune trace datée. Les mesures de délai, elles, continuent de ne compter que les étapes réellement travaillées : une étape sautée n'a pas de durée. Les deux notions coexistent parce qu'elles répondent à deux questions différentes.

Deux corrections techniques au passage :

  • Le scope de filtrage impose un tri sur la date de soumission, incompatible avec un GROUP BY sous only_full_group_by. Tri retiré pour la requête d'agrégat.
  • Bug dans mon aide de test, pas dans le code : jusqua('ideation') sautait sa propre condition d'arrêt et poussait la fiche jusqu'à l'archivage. Aucun test ne l'avait vu parce qu'aucun n'en dépendait — jusqu'à celui qui vérifie que l'entonnoir décroît.

Tests : 352 verts.