Comment j'ai bâti arbitres.ca avec Claude et Copilot

Développer arbitres.ca avec Claude Code et GitHub Copilot

Comment j'ai bâti arbitres.ca avec Claude et Copilot

Partie 2 de la série “Dans les coulisses d’arbitres.ca” : Partie 1 – Assigner des arbitres sans Excel | English

Dans le premier article, j’ai présenté arbitres.ca et sa stack. Ici, je passe à la méthode. À la fin juillet, le dépôt comptait 280 commits, 68 specs et 9 skills, accumulés sur environ deux mois et demi de soirées.

Je montre aussi les endroits où l’IA s’est trompée et ce qui l’a rattrapée.

Décider quoi bâtir avant d’écrire du code

C’est avant d’écrire du code que l’IA m’a le plus servi.

J’utilise la skill grill-me dont j’ai parlé dans la série sur les skills. Elle pose des questions une à la fois jusqu’à ce que les décisions soient prises. Je m’en suis servi dès le début du projet, et j’y reviens chaque fois que j’ai un gros bout à décider.

Une session comme ça en juillet a fixé le positionnement du produit. Plutôt que de refaire la gestion de calendrier que d’autres plateformes font déjà, on a décidé de mettre l’effort sur l’assignation quand il manque des arbitres. Ça a donné une contrainte de design que j’ai gardée depuis : aucun écran de configuration pour ces fonctionnalités. Les seuils et les poids sont codés en dur, et l’application explique ses suggestions au lieu d’offrir des réglages.

La même session a aussi mené à l’arrêt d’une spec. J’avais écrit une spec pour importer automatiquement les fichiers d’une plateforme externe. Le format de colonnes change d’une association à l’autre et n’est pas documenté publiquement. J’ai mis la spec en attente avec une condition pour la reprendre : avoir un vrai fichier d’export en main. Sans ça, je devinais.

Une spec par fonctionnalité

Chaque fonctionnalité commence par un fichier dans specs/. Le gabarit est encodé dans une skill /new-spec : contexte et décisions de conception, pages avec leurs rôles d’accès, schéma de base de données avec le DDL et les politiques RLS, logique métier, modèles C#, services, i18n, ce qui est hors périmètre, et des critères d’acceptance à cocher.

La section « hors périmètre » est celle qui m’a le plus servi. Elle donne une place officielle aux bonnes idées qui feraient déraper la livraison.

Les critères d’acceptance ferment la spec. Voici un extrait de ceux de l’alerte de pénurie :

- [x] La vue `v_penurie` retourne une ligne par (partie, poste) à risque sur les 3 postes
- [x] L'horizon est de 14 jours, codé en dur, aucun paramètre ni colonne de configuration
- [x] Aucun nouvel écran de configuration
- [x] Les conflits horaires sont ignorés dans le calcul (aucun appel à `arbitre_a_conflit`)
- [x] Un poste comblé (assignation active) ne remonte jamais dans la vue

Chaque ligne se vérifie une par une. C’est ce que je coche avant de fermer la spec, et c’est aussi ce que l’agent relit quand il revient sur le sujet des semaines plus tard.

Trois autres fichiers servent de référence. 00-prioritization.md est la feuille de route avec l’état de chaque spec. CLAUDE.md sert de mémoire du projet : la stack, les conventions, les pièges connus. schema_complet.sql documente l’état final du schéma, synchronisé à chaque migration.

L’agent ne se souvient pas de la conversation d’hier. Il lit la spec et le CLAUDE.md. C’est ce qui permet de reprendre une fonctionnalité trois semaines plus tard sans tout réexpliquer.

Claude écrit, Copilot révise

Le travail se fait sur des branches, avec Claude Code qui implémente à partir de la spec. Ensuite j’ouvre une pull request, et c’est Copilot qui la révise.

Faire réviser le code d’un agent par un autre agent marche mieux que de demander au même de se relire. Les chiffres du dépôt le montrent : 171 commits signés Claude, 91 par moi, 18 par l’agent Copilot qui pousse ses propres corrections.

Cette revue a attrapé de vraies affaires. Le cas le plus sérieux touchait le formulaire d’inscription public. Il n’avait ni limite de débit ni protection contre les robots, et le chemin d’écriture était accessible sans authentification. N’importe qui pouvait inonder la table des demandes sans que ça lui coûte rien. Copilot l’a levé en revue de PR. J’ai noté l’item dans specs/tech-debt.md au lieu de le régler tout de suite, et il a été réglé plus tard avec une Edge Function qui centralise le honeypot, la limite par IP et la validation du captcha.

Cette habitude est aussi utile que la revue. Un problème trouvé pendant une PR sur un autre sujet ne devrait pas faire dérailler la PR en cours. Il s’en va dans le fichier de dette technique avec sa source, son risque et le correctif prévu.

Des garde-fous aux skills qui s’appellent entre elles

Les skills du dépôt ont commencé comme des garde-fous. /new-page connaît le design system, les fichiers de ressources bilingues à créer et les pièges du projet. /schema-sync applique une migration Supabase et synchronise la documentation. /maj-tech fait la mise à jour périodique des dépendances.

Ces skills contiennent surtout les erreurs que j’ai déjà faites. Deux d’entre elles reviennent souvent avec le client Supabase en C#.

La première : toute propriété publique calculée sur un modèle est sérialisée dans le corps des requêtes, ce qui déclenche une erreur PGRST204 sur une colonne inexistante.

// inclus dans le JSON envoyé à Supabase, donc PGRST204
public bool EstGlobal => TerrainGlobalId.HasValue;

// exclu de la sérialisation
[JsonIgnore]
public bool EstGlobal => TerrainGlobalId.HasValue;

La deuxième m’a coûté une soirée. Presque tous les modèles du projet ont une clé primaire générée par la base, et l’attribut s’écrit [PrimaryKey("id", false)] pour l’omettre à l’insertion. Quand j’ai ajouté une table dont la clé est fournie par le client, l’IA a repris le patron dominant du dépôt. La colonne était donc absente de la requête et l’insertion échouait sans erreur visible. Les préférences des arbitres ne se sauvegardaient tout simplement pas.

// clé générée par la base
[PrimaryKey("id", false)]

// clé fournie par le client, il faut l'inclure
[PrimaryKey("arbitre_id", true)]

C’est le genre de bogue que l’IA produit facilement. Le code suivait la convention du dépôt, ce qui est habituellement correct, mais ce cas-là était une exception.

Toutes les règles encodées ne viennent pas de bogues. Le site est prérendu au dotnet publish avec BlazorWasmPreRendering.Build, parce que Blazor WebAssembly sert autrement une coquille HTML vide aux moteurs de recherche. La conséquence se brise facilement sans le savoir : sur une page publique, le contenu doit être le rendu par défaut, et la redirection d’un utilisateur déjà connecté doit attendre OnAfterRenderAsync. Une page qui redirige trop tôt s’affiche parfaitement dans un navigateur et reste vide pour le crawler. Rien dans le code n’empêche de la briser, alors la règle vit dans la skill /new-page et dans le CLAUDE.md.

Depuis, les skills se sont mises à s’appeler entre elles. Une skill dev-feature enchaîne la rédaction de la spec, la migration, les couches de service, les pages et les tests. Elle s’arrête à des points de contrôle : après la spec pour que je la confirme, et si l’analyse de sécurité de Supabase remonte un avertissement qui n’était pas prévu. Son critère d’usage dit qu’une spec ambiguë ou qui touche plusieurs domaines ne passe pas par elle. Les points d’arrêt sont écrits dans la skill, comme les étapes.

Les leçons de revue retournent dans le dépôt

Quand une revue de PR soulève une correction qui risque de se répéter, j’écris la convention dans CLAUDE.md.

Un commit de juillet ajoute deux conventions venues d’une seule revue, sur la façon de matérialiser une liste filtrée et sur la valeur par défaut d’un champ de DTO qui reflète le statut d’une autre table. Le même commit ajoute un hook de démarrage de session qui installe le SDK .NET dans les sessions web, sans bloquer si le réseau empêche le téléchargement.

Ça paraît dans les chiffres. Entre le 21 et le 31 juillet, les commits de Claude sont passés de 115 à 171 pendant que les miens restaient au même nombre. Plus les conventions et les tests couvrent de cas, plus je peux déléguer sans avoir à repasser derrière.

La logique métier pure est dans des classes statiques *Helper testées avec xUnit. Du code enfoui dans un service qui appelle Supabase n’est pas testable, et sans tests je ne déléguerais pas autant.

Bonne programmation, et si vous démarrez un projet avec un agent, commencez par écrire le fichier qui explique vos conventions. C’est ce qui m’a le plus rapporté.


Cet article a été rédigé avec l’aide de l’IA et révisé par moi.


Suggestions de lecture :