How this registry was built
This page makes the application's development journal public: the requests received word for word, the options set aside and the corrections made. It is updated with every deployment.
This application was designed and built with the assistance of artificial intelligence. For an AI Enablement team, "where did the human stay in control?" is exactly the question any governance framework asks. We answer it here about our own case, rather than promising it.
- Human decisions
- 5
- AI proposals accepted
- 3
- Corrections and dead ends
- 8
The development journal below is written in French and is not translated. Showing it as-is is more honest than presenting a translation that does not exist.
Development journal
5 · 3 · 8
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
personneset 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) etetape_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
composer create-projecta échoué surmkdir(): Permission denied—/var/wwwappartient àroot. Répertoire créé au préalable enmaxime:www-data.- Pest 4 refusait de s'installer : conflit avec
laravel/paov1.1.4, une dépendance de développement du squelette.laravel/paoretiré — il n'est pas utilisé par ce projet. npm run builda échoué sur « Cannot find native binding » de Rolldown, un bogue npm connu sur les dépendances optionnelles.@rolldown/binding-linux-x64-gnudéclaré explicitement, comme surcafe.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 :
-
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_uniquefait 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. -
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
RefreshDatabaseenveloppe 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 scoperecherche()enLIKEsur 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 ensaveQuietly: un score recalculé est une conséquence, pas un événement digne du journal.NumeroService—IA-AAAA-NNNNsous 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é à toucheretape_actuelle_idetetapes_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 :
-
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é. -
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-traductionvivait danspartials/; les composants anonymes de Blade doivent être dansviews/components/. - Le contrôleur de base de Laravel n'inclut plus
AuthorizesRequestsdepuis 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.