Appearance
Playbook CTO — Snag
Réconciliation des audits, feuille de route et guide de montage
Note de nommage. Le produit s'appelle Snag — SKN était le nom de travail, abandonné le 2026-07-29. L'organisation GitHub a suivi le 2026-07-30 : c'est désormais
snag-tcg(snagseul était déjà pris par un compte utilisateur). Voir l'historique des renommages pour ce qui a bougé et ce qui n'a pas bougé — les dossiers du VPS gardent volontairement l'ancien nom, dix bind mounts actifs passant par eux.Ce qui porte encore l'ancien nom : les noms de dépôts, la marque dans l'app (« Passer à SKN Pro »), le nom d'affichage sur les stores (« TCG Collector »), et surtout l'
applicationId— la suggestioncom.skntcg.collectorest caduque. Cet identifiant devient définitif à la première publication, il doit donc être re-tranché — vraisemblablement encom.snag…— avant tout envoi sur un store. Un renommage de dépôt est réversible et peut attendre ; celui-là non.
Date : 2026-07-27, mis à jour 2026-07-29 Auteur : synthèse des trois sources ci-dessous, avec arbitrage des contradictions Destinataire : Kael (CTO / Tech Lead / Tech Lead Infra) Équipe cible : 2 développeurs, 2 PO (Nini — Head of Marketing, Spleen — PO), + le CTO
Les trois sources réconciliées
| Doc | Périmètre | Force | Limite |
|---|---|---|---|
synthese-infra-build-test-mobile.md | Build & distribution mobile iOS/Android, format des tickets PO | Seul document qui traite la chaîne jusqu'au testeur | Suppose un pipeline de build sain qui n'existe pas encore |
zai_audit_report_...md | Audit TDD/BDD sur 5 dépôts, + déploiement, sécurité, PR | Seul à couvrir les 5 dépôts et à repérer des défauts que l'autre a manqués | Chiffres non fiables (voir §2.3), YAML générés à traiter comme brouillons |
claude_audit_report_...md | Audit TDD/BDD sur 2 dépôts, garde-fous équipe, sécurité/RGPD/pentest | Faits mesurés et vérifiables, arbitrages tranchés | N'a audité que 2 dépôts sur 5, ignore iOS et la distribution |
Aucun des trois ne suffit seul. Le premier décrit une chaîne de distribution sans garde-fous, le deuxième couvre large mais mesure faux, le troisième mesure juste mais couvre étroit. Ce document est leur intersection arbitrée.
Table des matières
- 0. Mode d'emploi
- 1. Résumé en une page
- 2. Réconciliation
- 3. Les sept décisions qui t'appartiennent
- 4. Le système cible
- 5. Feuille de route en quatre vagues
- 6. Guide de montage infra
- 7. Le contrat d'équipe
- 8. Tenir la charge : cache, file d'attente, redondance
- 9. Mesurer les progrès
- 10. Skills : purger, compléter, créer
- 11. Questions ouvertes
- Annexe A — Budget
- Annexe B — Fiches de présentation
- Annexe C — Les contradictions réelles
0. Mode d'emploi
Ce document sert quatre usages. Ne le lis pas linéairement.
| Tu veux… | Lis |
|---|---|
| Comprendre où on en est | §1, §2.2, §2.3 — 15 minutes |
| Décider | §3 (sept décisions), Annexe A (budget) |
| Présenter à l'équipe | Annexe B, puis §7 (le contrat d'équipe) — c'est ce qui les concerne |
| Monter l'infra | §6, dans l'ordre. C'est un runbook, pas un exposé |
| Fabriquer des skills | §10 |
Une règle de lecture. Quand ce document dit « mesuré », la valeur a été vérifiée par commande le 2026-07-27. Quand il dit « estimé » ou « à vérifier », c'est une hypothèse. Les deux audits sources mélangent les deux registres sans les distinguer ; c'est la première chose que cette réconciliation corrige.
1. Résumé en une page
Ce qui va bien. Le dépôt mobile est sain : 1757 tests qui passent, TypeScript strict, une discipline de nommage nettement au-dessus de la moyenne, et un système de design dont le contraste est vérifié au build sur 20 palettes × 2 thèmes. Le backend a une CI sérieuse (typecheck, unitaires, build Docker, smoke API, intégration Postgres réelle) et une conception de sécurité soignée : comparaison de jeton en temps constant, allowlist explicite de la surface publique, fail-closed, SQL intégralement paramétré.
Ce qui ne va pas. Quatre choses, et elles sont structurelles.
Postgres n'est sauvegardé nulle part. Aucune tâche cron, aucun répertoire de sauvegarde — mesuré. La base contient le catalogue et 143 452 embeddings ; la perdre coûte des heures à des jours de reconstruction, sans produit pendant ce temps. C'est l'action la plus urgente de ce document, avant les garde-fous et avant la CI (§8.7).
Il n'existe aucun garde-fou au sens strict. Les dépôts sont privés sur une organisation GitHub au plan Free : la protection de branche n'est pas disponible. Le hook « pas de commit sur main » ne contraint que l'agent Claude, pas un humain. Le hook
pre-commithusky est un no-op depuis toujours. Le dépôt mobile n'a aucune CI. Ce qui tient le projet aujourd'hui, c'est qu'une seule personne y touche.Le harnais de test protège le mauvais code. 1757 tests, et zéro sur les 22 écrans comme sur le chemin de données que l'application emprunte réellement. Les quatre derniers bugs sont tous passés dans cet angle mort, et trois d'entre eux par une couture d'intégration qui n'existe pas côté mobile.
La chaîne de distribution n'existe pas. Aucun PO ne peut tester quoi que ce soit aujourd'hui. Ni compte Apple, ni machine de build iOS, ni canal de distribution.
L'ordre importe autant que le contenu. Poser des garde-fous sur un harnais qui ment revient à industrialiser la production de checks verts sans signification. Brancher une distribution sur un build non testé revient à livrer plus vite du code non vérifié à des non-techniciens qui ne sauront pas distinguer un bug d'une fonctionnalité.
Séquence non négociable : corriger ce qui ment → poser les garde-fous → brancher la distribution → ouvrir l'équipe.
Coût de l'ensemble : environ 40 €/mois et 99 €/an, hors matériel optionnel. Voir Annexe A.
2. Réconciliation
2.1 Les accords — à exécuter sans débat
Ces points sont affirmés par au moins deux sources et vérifiés. Ils ne demandent aucun arbitrage : ce sont des tâches.
| # | Constat | Sources | Action |
|---|---|---|---|
| A1 | Aucun mutation testing nulle part | zai + claude | Stryker sur une liste courte (§2.2-D) |
| A2 | Aucune spécification Gherkin exécutable | zai + claude | 1 .feature exécutable sur le parcours scan |
| A3 | Le .feature du catalogue backend n'est pas branché | zai + claude | cucumber-js, ou l'assumer comme documentation |
| A4 | Le pipeline de scan n'a aucune spec de bout en bout | zai + claude | C'est le produit ; c'est la priorité BDD |
| A5 | Rechargement d'index concurrent d'un scan en vol : risque réel | zai + claude | Voir l'arbitrage §2.2-C sur le mécanisme |
| A6 | Python quasi non testé et hors CI | zai + claude | pytest en CI |
| A7 | Tests d'intégration à automatiser, zéro étape manuelle | zai + claude | §2.2-F pour la limite |
| A8 | iOS exige un compte Apple + une machine macOS | infra (seul, mais factuel) | Décision §3-2 et §3-3 |
| A9 | Firebase App Distribution ne compile rien | infra | C'est un hébergeur de binaires signés, pas un CI |
| A10 | Les tickets PO ont besoin de critères d'acceptation testables | infra + claude (§recevabilité) | §7.4 |
2.2 Les contradictions, arbitrées
Six divergences réelles. Pour chacune : ce que dit chaque source, la preuve, et le verdict.
A — Qualité de conception du dépôt mobile : 8/10 ou défaut structurel ?
zai : « Design Quality 8/10 — architecture propre avec séparation des couches bien contrôlée, minimum de couplage accidentel. » claude : la couche testée n'est pas la couche utilisée ; violation de couche dans ScannerScreen ; six écrans morts dans design-system/.
Preuve — imports mesurés :
src/data/repos.ts → 6 interfaces, 6 implémentations, toutes nommées Mock*
11 écrans → importent directement @app/data/demo/catalog
design-system/src → 68 imports de @/design, 1 de @/components, 0 de @/screensVerdict : les deux ont raison, à des altitudes différentes — et c'est important.
zai juge la qualité intra-module : types littéraux, injection de dépendances, nommage aligné sur le domaine. À ce niveau, 8/10 est mérité et je le confirme.
claude juge la qualité inter-module : quel chemin le code emprunte réellement. À ce niveau, il y a une abstraction repository entièrement testée que l'application n'emprunte pas, et un catalogue de démo — qui contient le calcul du portefeuille, donc l'argent — qu'elle emprunte partout.
Ce que ça change pour toi : ne te rassure pas avec le 8/10. Un dépôt peut être excellent module par module et fragile dans son assemblage. C'est précisément le défaut qu'aucune note globale ne capture, et c'est celui qui a produit les bugs.
Action : trancher un seul chemin de données (câbler les repositories, ou les supprimer et tester demo/catalog). Priorité haute, avant l'arrivée des devs — sinon deux personnes construiront sur deux chemins différents.
B — Discipline test-first : B+ ou bimodale ?
zai : mobile 8/10, backend 4/10 (« patterns test-after dominants »). claude : le dépôt mobile est bimodal — le pipeline de scan est piloté par les tests, l'UI ne l'est pas du tout.
Preuve — ratio fichiers-de-test / fichiers-source par commit :
3e5990d fix(scanner): route mobile scans through SKN tests=7 src=9
5d8951e fix(scanner): simplify scan mode selection tests=4 src=3
---
ecacc77 feat(screens): redesign every screen tests=1 src=27
a8cffa9 fix(ui): resolve the review findings tests=2 src=24Verdict : la note unique de zai masque l'information utile. 8/10 est une moyenne entre un sous-système à ~1:1 (signature d'un cycle rouge-vert) et un autre à 1:27 (atterrissage en bloc). La moyenne ne te dit pas où mettre l'effort ; la bimodalité si.
Action : n'exige pas « plus de TDD » globalement. Exige des tests de rendu sur trois écrans — Scanner, CardDetail, Wallet — c'est-à-dire précisément là où le ratio s'effondre.
C — La « race condition » du rechargement d'index
zai : « ❌ TOCTOU RACE — double-checked locking sans lock » sur refreshScanIndex, avec recommandation d'« implémenter un verrouillage approprié ». claude : frontière async où l'invariant peut casser en silence — un scan peut matcher contre un index à moitié permuté.
Verdict : bonne inquiétude, mauvais mécanisme — et la correction proposée par zai serait inutile.
ts
if (scanIndexReloading) return;
scanIndexReloading = true;Node est mono-thread avec une boucle d'événements : aucune préemption ne peut s'insérer entre ces deux instructions synchrones. Ce n'est donc pas un TOCTOU, et un mutex n'y changerait rien.
Le risque réel est ailleurs et il est bien réel : une requête de scan qui commence avant le remplacement de l'index et se termine après peut lire un état incohérent, ou mélanger deux générations d'index.
Action correcte : garantir un échange atomique de référence (construire le nouvel index complètement, puis réassigner en une seule instruction) et que chaque requête capture la référence une seule fois en début de traitement. Plus un test qui fait se croiser un reload et une requête en vol — aucun n'existe.
Ce que cet exemple enseigne : un audit produit par un modèle peut nommer un vrai symptôme avec le mauvais vocabulaire et proposer une correction inopérante. À faire relire par quelqu'un qui connaît le modèle d'exécution. C'est exactement ton rôle.
D — Quels modules muter en priorité ?
Les deux listes sont différentes et toutes deux défendables.
| zai propose | claude propose |
|---|---|
scan/backendScan.ts (validation) | design/contrast.ts |
data/repoFactory.ts | scan/matching.ts (backend) |
data/AsyncStorageScanHistoryRepo.ts | scan/phash.ts (backend) |
demo/catalog.ts::walletTotals | |
ui/format.ts::formatMoney |
Verdict : fusionner, et ordonner par levier, pas par criticité perçue.
Le bon critère n'est pas « ce module est-il important » mais « combien d'assertions s'effondrent si ce module est faux ».
| Rang | Module | Pourquoi ce rang |
|---|---|---|
| 1 | design/contrast.ts | 1270 assertions reposent dessus. S'il est faux, elles sont fausses ensemble et le build reste vert. Meilleur levier du dépôt. |
| 2 | scan/matching.ts (backend) | C'est le produit. C'est là qu'a vécu le bug qui cassait 100 % des scans. |
| 3 | scan/backendScan.ts (mobile) | Ajout de zai, justifié : ~40 lignes de conditions de validation sur entrée non fiable. |
| 4 | demo/catalog.ts::walletTotals + ui/format.ts | De l'argent affiché à l'utilisateur. |
| 5 | scan/phash.ts (backend) | Seuils et distances : territoire canonique du mutant survivant. À coupler à fast-check. |
repoFactory.ts et AsyncStorageScanHistoryRepo.ts sortent de la liste — le premier parce que la décision §2.2-A peut le supprimer, le second parce que son vrai problème est la concurrence, qui se teste directement et pas par mutation.
E — iOS ou Android d'abord ?
Trois positions dans les sources, plus une quatrième dans le code :
| Source | Position |
|---|---|
synthese-infra | iOS prioritaire, « plus bankable » |
claude_audit | Android d'abord, faute de matériel |
poc-tcg-collector/CLAUDE.md | « iOS is not on the roadmap — don't add iOS-specific code paths » |
| Réalité du dépôt | Android uniquement : pas de dossier ios/ exploité, build Gradle seul |
Verdict : iOS-first est une décision business, elle prime — et elle rend le CLAUDE.md activement nuisible.
Ce fichier est lu par chaque agent avant chaque tâche. Tant qu'il dit « iOS n'est pas au programme », il oriente les agents contre ta stratégie. Il contient déjà trois autres affirmations fausses (React Navigation, zustand, sqlite — mesurés absents). Sa correction est la toute première action de la feuille de route, et elle ne coûte qu'une heure.
Nuance à conserver : la priorité iOS ne signifie pas abandonner Android. Android reste le chemin de vérification rapide et gratuit — Gradle tourne sur ton VPS Linux, sans compte Apple ni machine macOS. Garder les deux, prioriser iOS pour la distribution aux PO.
F — « Zéro test manuel » : jusqu'où ?
zai : chapitre entier « TESTS D'INTÉGRATION AUTOMATISÉS — ZÉRO MANUEL », objectif 100 %. claude : trois choses ne doivent pas être automatisées.
Verdict : accord sur l'exécution, désaccord sur le jugement — et la distinction est ce qui sauve l'automatisation.
Automatiser à 100 % l'exécution : oui, sans réserve. Aucune étape de test ne doit dépendre de quelqu'un qui pense à la lancer.
Mais trois décisions doivent rester humaines, sans quoi l'automatisation grave les erreurs dans le marbre :
- L'acceptation d'un golden diff. La régénération est une commande ; l'acceptation est une revue. Un golden accepté à l'aveugle transforme un bug en référence.
- L'arbitrage des mutants survivants. Certains sont des équivalents sémantiques : les tuer coûte plus que la garantie obtenue.
- Le seuil de similarité du scan. C'est un compromis produit — précision contre rappel. Il se mesure automatiquement (le benchmark existe), il se décide humainement.
Formule à retenir et à répéter à l'équipe : « zéro exécution manuelle, pas zéro jugement humain ».
2.3 Fiabilité des chiffres
Point important pour un CTO qui va s'appuyer sur ces documents : les chiffres de l'audit zai ne sont pas fiables.
| Métrique | zai annonce | Mesuré le 2026-07-27 | Écart |
|---|---|---|---|
| Cas de test (mobile) | 607 | 1757 | ×2,9 |
| Suites de tests | 127 | 55 | faux |
| Fichiers de test | 55 | 48 | faux |
| Fichiers source | 101 | 119 | faux |
bash
$ npx jest --ci --silent
Test Suites: 1 skipped, 54 passed, 54 of 55 total
Tests: 4 skipped, 1757 passed, 1761 totalCe que ça implique, et ce que ça n'implique pas.
Ça n'invalide pas l'audit zai : ses constats qualitatifs sont largement justes, et il a trouvé au moins un défaut réel que l'autre audit a manqué (§2.4). Mais ses notes chiffrées (7,5/10, 4/10, 1/10) reposent sur des mesures fausses et ne doivent pas servir d'indicateur de suivi ni être présentées à l'équipe comme un état des lieux.
Règle de gouvernance à adopter : toute métrique qui pilote une décision doit être produite par la CI, pas par un rapport. Un chiffre dans un document est une photo qui vieillit ; un chiffre dans un job est une mesure. C'est aussi ce qui rend la §5 vérifiable.
De même, les blocs YAML de workflows GitHub Actions présents dans l'audit zai sont des brouillons générés : utiles comme point de départ, à ne jamais copier sans relecture — plusieurs contiennent des motifs que la §6.6 déconseille explicitement.
2.4 Ce que chaque document a manqué
synthese-infra a manqué : toute la question de la qualité. Le pipeline décrit livre un binaire à Nini et Spleen sans qu'aucun test n'ait à passer. Livrer plus vite du code non vérifié à des non-techniciens, c'est transformer tes PO en détecteurs de régressions — le poste le plus coûteux et le plus démoralisant de l'équipe.
zai a manqué : la couche d'application des garde-fous. Il propose des branch protection rules sans voir que le plan GitHub Free ne les autorise pas sur dépôt privé — donc rien de ce qu'il décrit n'est activable en l'état. Il a aussi manqué le hook husky no-op et l'absence totale de CI mobile.
claude a manqué :
- Trois dépôts sur cinq.
tcg-app2(backend Python FastAPI, ~2 % de couverture),tgc-app(copie morte),frontend(placeholder). - Un vrai défaut de test, que zai a correctement identifié :
src/api/tests/query.test.mjsassert sur la chaîne SQL produite —
js
assert.equal(whereSql, 'WHERE set_id = $1 AND rarity = $2 AND name ILIKE $3');
assert.equal(whereSql, "WHERE data->'types' ? $1");C'est un test boîte-blanche : tout refactoring du builder casse le test sans qu'aucun comportement ne change. Le bon test interroge l'API et vérifie les cartes retournées. Bon point pour zai.
- Toute la chaîne iOS et la distribution.
Conséquence concrète, à décider (§3-4) : tgc-app n'est même pas un dépôt git (aucun historique) et duplique les fichiers de tcg-app2, dont le dernier commit date du 19 mai 2026. Deux dossiers morts dans le workspace, c'est deux endroits où un nouveau développeur peut travailler par erreur.
3. Les sept décisions qui t'appartiennent
Aucun audit ne peut les prendre à ta place. Chacune débloque une partie de la §5.
Décision 1 — Passer l'organisation GitHub en plan Team
Constat mesuré : snag-tcg est au plan free, les deux dépôts sont privés. Sur cette combinaison, GitHub n'offre ni ruleset, ni protection de branche, ni revue obligatoire, ni check requis, ni protection d'environnement.
Sans cette décision, les §5 à §7 de ce document sont inapplicables. Il ne resterait que des conventions, c'est-à-dire ce qui existe déjà et qui ne tient que parce qu'une seule personne code.
Coût : ~4 $/utilisateur/mois × 5 ≈ 20 $/mois. Recommandation : oui, immédiatement. Meilleur rapport garantie/euro du projet.
Décision 2 — Confirmer iOS-first et ouvrir le compte Apple
Recommandation : oui, si le « plus bankable » est étayé côté marché. 99 $/an, souscrivable depuis Windows. Conséquence immédiate : corriger CLAUDE.md (§2.2-E) le jour même, sans quoi les agents travailleront contre la décision.
Décision 3 — Comment compiler iOS ?
synthese-infra pose trois voies. Les voici comparées, la troisième incluse — elle avait disparu de la première version de ce playbook, ce qui escamotait l'arbitrage au lieu de le rendre.
| A — Mac mini | B — VPS macOS (MacStadium, EC2 Mac) | C — Codemagic | |
|---|---|---|---|
| Coût | 600–800 € une fois | location mensuelle, > A à l'usage continu | gratuit puis à l'usage |
| Mise en route | jours | heures | heures |
| Maintenance | Xcode, certificats, disponibilité — à ta charge | matériel géré, OS à ta charge | nulle |
| Intégration | SSH depuis l'orchestrateur | SSH / API | API/webhook, natif RN + Firebase |
| Certificats Apple | manuels | manuels | automatisés via clé App Store Connect |
Recommandation : C, Codemagic, pour démarrer. Tu es seul sur l'infra ; une machine de build, physique ou louée, est une astreinte. B est le moins bon des trois ici : il coûte plus cher qu'un Mac mini amorti tout en te laissant la gestion des certificats, donc il cumule le prix de C et le travail de A. Il ne se justifierait qu'avec un volume de builds qui rendrait Codemagic plus cher — un problème que tu n'as pas.
Réévalue quand la facture Codemagic dépasse l'amortissement d'un Mac mini. C'est un basculement facile, pas un aller sans retour.
Prérequis oublié par les trois documents : il n'existe aujourd'hui aucun projet iOS — pas de dossier
ios/, pas de Podfile, pas de.xcodeproj. S'y ajoute un module natif Kotlin maison,quaddetector(détection des coins de carte), sans équivalent iOS. Le compte Apple et Codemagic ne servent à rien tant que ce chantier n'est pas fait : iOS-first est d'abord du développement, pas de l'infra. Seule la compilation exige macOS ; générer le projet et porter le module natif peut s'écrire depuis Windows.
Décision 4 — Que faire de tcg-app2, tgc-app et frontend ?
Recommandation : archiver tgc-app (copie morte, hors git) et frontend (placeholder) ; décider explicitement du sort de tcg-app2 — soit il rentre dans le périmètre qualité (CI, pytest, garde-fous), soit il est archivé. Ce qui n'est pas tenable, c'est le statu quo : un dépôt Python à ~2 % de couverture, sans CI, que personne ne sait s'il est vivant.
Décision 5 — Format des tickets PO
synthese-infra propose deux niveaux. Recommandation : sa propre recommandation, qui est la bonne — les PO écrivent en langage naturel, un agent convertit en format structuré. N'impose pas Given/When/Then à Nini et Spleen : le coût d'apprentissage tomberait sur eux et la qualité des tickets baisserait. Voir §7.4 et le skill spec-from-ticket (§10.4).
Décision 6 — Droits GitHub des PO
Recommandation : rôle Write, comme les devs. Une fois les rulesets posés, ce n'est plus la permission qui protège mais la branche protégée. Les rétrograder crée de la friction sans gagner de sécurité. Ce qui borne le rayon de leurs erreurs, c'est CODEOWNERS + staging. Admin : toi seul — un admin peut modifier un ruleset.
Décision 7 — Rétention des photos de scan
Constat mesuré : chaque scan archivé écrit la photo brute de l'utilisateur sur le disque du VPS, et il n'existe aucune purge, aucun TTL, aucun unlink dans le code. Conservation illimitée.
Recommandation : rétention 30 jours + purge automatique, et surtout recadrage sur la carte détectée avant écriture. Le quad detector connaît déjà le contour. On conserve ce qui sert au débogage sans conserver l'arrière-plan — qui est précisément la partie personnelle. Cette mesure supprime le problème au lieu de le gérer.
4. Le système cible
4.1 Le tronc commun — les quatre portes
Une seule image, qui fusionne les trois documents. C'est le tronc que toutes les variantes du §4.2 partagent — elles ne diffèrent que sur qui orchestre les agents et qui compile iOS.
┌──────────────────────────────────────────────────────┐
│ TICKET — Slack / GitHub Issues / Discord / … │
│ écrit en langage naturel par Nini ou Spleen │
└───────────────────────┬──────────────────────────────┘
│ skill spec-from-ticket
▼
┌──────────────────────────────────────────────────────┐
│ ISSUE GITHUB structurée (User Scenario + Given/When/ │
│ Then + hors-scope + critères de fin) │
└───────────────────────┬──────────────────────────────┘
│ « Create a branch » depuis l'issue
▼
┌──────────────────────────────────────────────────────┐
│ feature/xxx · dev ou agent │
└───────────────────────┬──────────────────────────────┘
│ PR → staging
┌───────────────────────────┴───────────────────────────┐
│ PORTE 1 — RECEVABILITÉ (déterministe, bloquant) │
│ R1 nom de branche cohérent avec le diff │
│ R2 rattachée à une issue / spec / rapport de fix │
│ R3 documentation à jour, parité EN/FR │
├────────────────────────────────────────────────────────┤
│ PORTE 2 — CORRECTION (déterministe, bloquant) │
│ typecheck · lint · tests · contrat · gitleaks · build │
├────────────────────────────────────────────────────────┤
│ PORTE 3 — JUGEMENT (agents) │
│ R4 adéquation au ticket → visible, non bloquant │
│ Manu revue de code → visible, non bloquant │
│ Zaher sécurité → BLOQUANT en critical/high │
├────────────────────────────────────────────────────────┤
│ PORTE 4 — HUMAIN │
│ 1 approbation (staging) · 2 + CTO (main) │
└───────────────────────────┬────────────────────────────┘
▼
╔═════════╗
║ staging ║ ← intégration
╚════╤════╝
┌──────────────────┼──────────────────┐
▼ ▼ ▼
[VPS staging] [Codemagic → .ipa] [VPS → .apk]
│ └────────┬─────────┘
│ ▼
│ ┌────────────────────────┐
│ │ Firebase App │
│ │ Distribution │
│ │ groupe product-owners │
│ └───────────┬────────────┘
│ ┌─────┴─────┐
│ ▼ ▼
│ [Nini iPhone] [Spleen iPhone]
│ │ │
│ └─────┬─────┘
│ ▼
│ retour commenté sur l'issue d'origine
▼
╔══════╗ PR de release · 2 approbations · CTO
║ main ║ déploiement production = APPROBATION MANUELLE
╚══════╝Trois principes portent tout le schéma :
- Rien n'entre sans origine. Une PR non rattachée à une issue ou une spec est une décision que personne n'a prise. À une personne c'est invisible ; à cinq c'est ce qui produit la dette.
- Le déterministe bloque, le jugement éclaire. Seule exception : la sécurité, dont l'asymétrie de coût justifie de bloquer sur un jugement.
- Le merge appartient à l'équipe, la mise en production t'appartient. C'est la dernière barrière quand tout le reste est passé au vert.
4.2 Cinq variantes de mise en œuvre, par budget
Le tronc du §4.1 ne change jamais : issue → portes → staging → build → PO. Ce qui change selon la trésorerie, ce sont deux choix indépendants :
- Qui orchestre les agents. Un cron sur le VPS pilotant
claude -p— couvert par l'abonnement, coût marginal nul, mais quota partagé avec tes propres sessions. Ou l'Action GitHub officielleclaude-code-action— clé API facturée au jeton, logs natifs dans GitHub, latence de ~30 s. - Qui compile iOS. Codemagic — rien à entretenir, facturé à la minute au-delà du palier gratuit. Ou un Mac mini — achat unique, entretien à ta charge.
Croisés, ces deux choix donnent quatre variantes, plus le statu quo :
| iOS : Codemagic | iOS : Mac mini | |
|---|---|---|
| Agents : cron VPS (abonnement) | V2 — départ recommandé | V4 |
| Agents : API GitHub (Action) | V3 | V5 — vitesse de croisière |
V1, le statu quo, n'est sur aucune case : ni orchestration, ni build iOS.
Et le rappel qui cadre tout : la clientèle cible est sur iPhone. Une variante sans chaîne iOS ne livre rien à tester aux PO — c'est ce critère qui disqualifie V1, pas son coût.
V1 — Statu quo : les devs poussent, personne ne teste
mermaid
flowchart LR
D[Devs + Claude Code] --> R[PR + revue agents] --> M[main]
M -.aucun build iOS.-> X((PO : rien<br/>à tester))Les devs codent avec Claude Code, avec ou sans agents de revue ; les PR partent ; tu merges. Aucune distribution : Nini et Spleen ne peuvent rien installer, et iOS n'existe pas.
Coût additionnel : 0 €/mois. Le coût réel est invisible sur une facture : chaque validation produit passe par toi, et l'objectif iOS-first n'avance pas. V1 n'est pas une option — c'est le point de départ à quitter.
V2 — Cron VPS + Codemagic : le départ recommandé
mermaid
flowchart TB
S[Slack ou formulaire d'issue] --> I[Issue GitHub · label from-po]
I --> C[Cron VPS · claude -p<br/>abonnement, coût marginal nul]
C -->|spec, code, revue| PR[Pull request]
PR --> G{4 portes}
G --> ST[staging · merge agent ou CTO]
ST --> CM[Codemagic · build iOS signé]
ST --> AN[VPS · build Android]
CM --> F[Firebase App Distribution]
AN --> F
F -->|e-mail| PO[PO/BA · iPhone]L'idée naît dans Slack ou directement dans le formulaire d'issue (§7.3–7.4). Le cron du VPS détecte le label, claude -p convertit la spec, code, ouvre la PR ; les portes tournent ; le merge sur staging peut être fait par l'agent quand tout est vert — main reste humain. Codemagic signe et compile l'iOS, le VPS l'Android, Firebase distribue, l'e-mail part vers les PO.
Sur la question « Slack → issue, gratuitement » : oui — l'app GitHub officielle pour Slack sait créer une issue depuis un message, sans frais. Mais le formulaire d'issue reste l'entrée recommandée : c'est lui qui rend le champ « c'est fini quand » obligatoire. Un webhook maison ferait la même chose en ajoutant de l'infra à entretenir et une surface d'injection (§5.9) — à éviter.
Coût : ~8 €/mois — l'Apple Developer Program (99 $/an) au prorata, incontournable pour signer de l'iOS. Codemagic reste gratuit sous son palier (~500 minutes macOS/mois, soit de l'ordre de 20 à 25 builds React Native — à vérifier sur ta config), puis ~0,10 $/minute. Les agents ne coûtent rien de plus : l'abonnement couvre — mais le quota est partagé avec tes propres sessions, et c'est cette contention qui fera un jour basculer vers V3.
V3 — Action GitHub + Codemagic : l'orchestration sort du VPS
mermaid
flowchart TB
I[Issue GitHub · label from-po] --> A[claude-code-action<br/>API, facturée au jeton]
A -->|spec, code, revue| PR[Pull request]
PR --> G{4 portes}
G --> ST[staging]
ST --> CM[Codemagic · build iOS signé]
CM --> F[Firebase App Distribution]
F -->|e-mail| PO[PO/BA · iPhone]Même chaîne, mais le déclencheur est anthropics/claude-code-action@v1 : plus de cron ni de credentials sur le VPS, logs dans l'interface GitHub, réaction en ~30 secondes au lieu de l'intervalle du cron, et plus aucune contention avec ton quota personnel. En échange, chaque exécution se paie.
Coût : V2 + les jetons API. Ordres de grandeur : une conversion de spec, quelques centimes ; un développement + revue de feature moyenne, ~1 à 5 € selon la taille et le modèle. À 20 features/mois : ~20 à 100 €/mois. Les minutes GitHub Actions Linux sont couvertes par le plan Team (3 000 min/mois incluses).
V4 et V5 — le Mac mini remplace Codemagic
mermaid
flowchart TB
T[V4 : cron VPS · V5 : claude-code-action] -->|spec, code, revue| PR[Pull request]
PR --> G{4 portes}
G --> ST[staging]
ST --> MM[Mac mini · runner self-hosted + Xcode]
MM --> F[Firebase App Distribution]
F -->|e-mail| PO[PO/BA · iPhone]Mêmes chaînes que V2 et V3, avec le Mac mini en runner self-hosted : Xcode compile et signe, l'upload Firebase part de la machine. En V4, le mini peut aussi héberger un second claude sur abonnement, ce qui désengorge le VPS.
Coût : ~700 € une fois (≈ 29 €/mois amorti sur 24 mois) + ~5 €/mois d'électricité — et surtout l'entretien : mises à jour Xcode, rotation des certificats, disponibilité de la machine. C'est du temps CTO, le seul coût qui ne se lit sur aucune facture. Rentable quand la facture Codemagic dépasse durablement l'amortissement, pas avant.
La trajectoire recommandée
V1 ──(cette semaine)──▶ V2 ──(quota partagé devenu goulot,
ou besoin de logs natifs)──▶ V3
│
(facture Codemagic > ~30 €/mois durables)
▼
V5V4 est rarement l'étape optimale : si tu achètes le mini, c'est que le volume est là — et à ce volume, l'API (V5) vaut mieux que la contention de quota du cron.
Rappel de la Décision 3, qui conditionne toutes les variantes iOS : le projet ios/ n'existe pas encore — ni dossier, ni Podfile, ni portage du module natif quaddetector. Aucun euro dépensé chez Apple ou Codemagic ne produit un build avant ce chantier.
5. Feuille de route en quatre vagues
Ordonnée par dépendance, pas par importance. Chaque vague débloque la suivante.
Vague 0 — Rendre vrai ce qui ment (1–2 jours, seul, avant tout le reste)
| # | Action | Pourquoi maintenant |
|---|---|---|
| 0.1 | Passer l'org en GitHub Team | Débloque tout le reste (Décision 1) |
| 0.2 | Réécrire poc-tcg-collector/CLAUDE.md | 3 libs déclarées absentes, cible iOS inversée, porte QA fictive. Chaque agent raisonne dessus. |
| 0.3 | Backend : glob au lieu de la liste manuelle de tests | 6 fichiers de test ne s'exécutent jamais, dont 3 sur l'OCR |
| 0.3bis | Réparer la CI backend — elle n'a JAMAIS été verte | 0 succès sur 100 runs depuis le 2026-05-10. La rendre requise en l'état gèlerait tous les merges (§6.3) |
| 0.4 | Créer la CI du dépôt mobile | Il n'y en a aucune. typecheck + test : 20 lignes |
| 0.5 | gitleaks sur l'historique complet des 2 dépôts | Savoir avant d'ouvrir l'accès à quatre personnes |
| 0.6 | Purger les 107 branches mergées (sur 239) | Un nouveau dev repartira d'une branche morte |
| 0.7 | Rétention 30 j + purge des scans archivés | Photos d'utilisateurs conservées sans limite (Décision 7) |
| 0.8 | Archiver tgc-app et frontend | Deux dossiers morts = deux endroits où se tromper (Décision 4) |
| 0.9 | Nettoyer .claude/ | clean-architecture copy, lp-conversion-skill, skills RN côté backend ; trancher tdd vs canon-tdd |
| 0.10 | Sauvegarder Postgres — automatique, et restauration testée | Mesuré : aucune tâche cron, aucun répertoire de sauvegarde. La base contient le catalogue et 143 452 embeddings. La perdre, c'est re-ingérer puis re-calculer tous les vecteurs : des heures à des jours sans produit. Le guide de déploiement demande de sauvegarder les certificats TLS et ignore la base (§8.7). |
| 0.11 | Limites de ressources par conteneur | Zéro aujourd'hui sur 12 services : sous pression, l'OOM killer peut choisir Postgres (§8.2) |
Sortie de vague : les documents disent vrai, tous les tests s'exécutent, une CI existe des deux côtés.
Vague 1 — Poser les garde-fous (2–3 jours, avant l'arrivée de l'équipe)
| # | Action |
|---|---|
| 1.1 | Créer staging dans les deux dépôts |
| 1.2 | Rulesets A (main), B (staging), C (nommage) — liste de bypass vide, toi compris |
| 1.3 | CODEOWNERS + équipes @snag-tcg/devs et @snag-tcg/product |
| 1.4 | Checks requis branchés sur les rulesets |
| 1.5 | Job de recevabilité R1–R3 |
| 1.6 | Templates de PR (dev et PO), suppression auto des branches, squash uniquement |
| 1.7 | ESLint + lint-staged réellement branchés (le hook husky est un no-op) |
| 1.8 | Environnements staging / production, approbation CTO en production |
Sortie de vague : personne — toi compris — ne peut modifier main sans passer les portes.
Vague 2 — Brancher la distribution (3–5 jours)
| # | Action |
|---|---|
| 2.1 | Compte Apple Developer (99 $/an) |
| 2.2 | Firebase + App Distribution + groupe product-owners |
| 2.3 | Codemagic connecté au dépôt, clé API App Store Connect |
| 2.4 | Pipeline iOS : build signé → Firebase, déclenché par un push sur staging |
| 2.5 | Pipeline Android : npm run aab sur le VPS → Firebase (canal unifié) |
| 2.6 | Enregistrement des UDID de Nini et Spleen (une fois) |
| 2.7 | Retour automatique : lien Firebase commenté sur l'issue d'origine |
Point de séquencement, à ne pas contourner : 2.4 se déclenche sur staging, donc après les portes de la vague 1. Un build qui part sans passer les tests remet les PO en position de détecteurs de régressions.
Sortie de vague : Nini et Spleen installent et testent en autonomie, depuis leur propre iPhone.
Vague 3 — Ouvrir l'équipe (première semaine à cinq)
| # | Action |
|---|---|
| 3.1 | Agent recevabilité R4 — visible, non bloquant |
| 3.2 | Manu (revue) — non bloquant |
| 3.3 | Zaher (sécurité) — bloquant en critical/high uniquement |
| 3.4 | Webhook Discord sur trois canaux : #ci, #pr-review, #alerts |
| 3.5 | Skill spec-from-ticket en production (Décision 5) |
| 3.6 | Une heure avec les quatre : le modèle de branches, la création de branche depuis une issue, et surtout pourquoi |
Vague 4 — Consolider (deuxième mois et au-delà)
| # | Action |
|---|---|
| 4.1 | Un seul chemin de données (§2.2-A) — la plus rentable pour la valeur de la suite |
| 4.2 | Tests de rendu sur Scanner, CardDetail, Wallet |
| 4.3 | Schéma de contrat unique, types générés des deux côtés — supprime une classe de bugs |
| 4.4 | Config Jest d'intégration mobile (preset react-native, HTTP intercepté) |
| 4.5 | Corpus auto-alimenté depuis scan-debug-cases |
| 4.6 | Job nocturne : build release + émulateur + Maestro |
| 4.7 | Stryker hebdomadaire sur les 5 cibles (§2.2-D) |
| 4.8 | .feature exécutable sur le parcours scan |
| 4.9 | Couverture sur le diff à 80 %, avertissement puis bloquant |
| 4.10 | Réécrire query.test.mjs en tests de comportement HTTP (apport zai) |
| 4.11 | Échange atomique de l'index + test de croisement (§2.2-C) |
| 4.12 | pytest en CI si tcg-app2 est conservé |
| 4.13 | Stack VPS de staging |
| 4.14 | Threat model interne, une demi-journée à quatre |
| 4.15 | Supprimer les six écrans morts de design-system/src/screens/ — mesuré : 68 imports de @/design, 1 de @/components, 0 de @/screens. Deux jeux d'écrans dont un seul est monté, c'est un fork qui attend de diverger |
| 4.16 | Réparer le nommage sim / similarity — voir l'encadré ci-dessous |
Le smell que 4.3 corrige, nommé. Le serveur émet
sim, le client attendsimilarity: deux noms pour un même concept, maintenus à la main dans deux dépôts. Chaque côté était cohérent avec lui-même, aucun test n'a rien vu, et 100 % des scans ont cassé. C'est l'exemple type du défaut que les tests ne peuvent structurellement pas attraper — un désaccord de modèle, pas une erreur de logique. Le schéma de contrat unique (4.3) est la correction ; ceci en est le diagnostic, à garder parce que la même faute se reproduira au prochain champ ajouté à la volée.
6. Guide de montage infra
Runbook. À dérouler dans l'ordre.
6.1 GitHub — organisation et rulesets
Ruleset A — main
| Règle | Valeur |
|---|---|
| Restrict deletions | ✅ |
| Block force pushes | ✅ |
| Require a pull request | ✅ |
| ↳ Required approvals | 2 |
| ↳ Dismiss stale approvals on push | ✅ |
| ↳ Require review from Code Owners | ✅ |
| ↳ Require approval of the most recent push | ✅ |
| Require status checks | ✅ (§6.2) |
| ↳ Require branches to be up to date | ✅ |
| Require linear history | ✅ |
| Require signed commits | ✅ |
| Bypass list | vide |
Ruleset B — staging : identique, 1 approbation, signature optionnelle. Le débit compte sur l'intégration, l'irréversibilité est sur main.
Ruleset C — nommage, sur ~ALL_BRANCHES :
^(main|staging)$|^(feature|fix|refactor|docs|chore|test|perf|poc|hotfix)/[a-z0-9]+(-[a-z0-9]+)*$Ruleset D — tags refs/tags/v* : suppression et mise à jour interdites.
Ne te mets pas dans la liste de bypass. Désactiver un ruleset en urgence laisse une trace dans le journal d'audit ; un bypass permanent n'en laisse aucune. Exception observable contre trou silencieux.
Piège des commits conventionnels : avec la fusion en squash, c'est le titre de la PR qui devient le message de commit. Linte le titre de PR, pas les commits de la branche.
Merge queue : inutile à cinq. Require branches to be up to date couvre le même risque bien moins cher.
6.2 CODEOWNERS
/.github/CODEOWNERS, dépôt mobile :
* @snag-tcg/devs
# Infra, CI, build, signature — CTO uniquement
/.github/ @kael
/android/ @kael
/ios/ @kael
*.gradle @kael
/scripts/ @kael
/codemagic.yaml @kael
# La configuration agentique EST une surface de sécurité
/.claude/ @kael
/CLAUDE.md @kael
/AGENTS.md @kael
# Le harnais se modifie sous revue, jamais en passant
/jest.config.js @kael
/jest.setup.ts @kael
/__tests__/ @snag-tcg/devs @kael
# Frontière produit / technique
/design-system/src/design/ @snag-tcg/devs @snag-tcg/productBackend, en plus : /docker/, /src/ingest/migrations/, /.github/workflows/ → @kael.
Pourquoi
.claude/est sous CODEOWNERS. Dès que quatre personnes peuvent pousser,skills/,hooks/etCLAUDE.mddeviennent du code exécutable qui décide de ce que les agents autorisent. Une PR qui modifie discrètement un hook de revue désarme le gardien sans toucher une ligne de code applicatif — et personne ne relit un.md.
6.3 Checks requis
Mobile — tout est à créer, il n'existe aucune CI.
| Job | Commande | Requis sur | Cible |
|---|---|---|---|
typecheck | tsc --noEmit | staging + main | < 2 min |
test | jest --ci | staging + main | < 3 min |
lint | eslint . (config à créer) | staging + main | < 1 min |
pr-title | commitlint sur le titre | staging + main | < 10 s |
receivability | R1–R3 (§7.3) | staging | < 30 s |
gitleaks | scan du diff | staging + main | < 30 s |
build-debug | assemble debug, sans secret | staging + main | < 8 min |
build-release | npm run aab + keystore | main uniquement | — |
Deux pièges mobiles :
- Le bundle JS. Le job doit passer par
npm run apk:*/npm run aab, jamais par ungradlew assembleReleasenu — ce dernier embarque silencieusement un bundle périmé. Un job qui construirait « à la main » reproduirait le bug déjà rencontré, en pire : validé par un check vert. - Le keystore. Uniquement dans
build-release, déclenché surmain, jamais dans un workflow lancé par une PR.
Backend — la CI est bien conçue et n'a jamais abouti. Correction apportée le 2026-07-28, après vérification des résultats et non plus du seul fichier ci.yml :
100 derniers runs du workflow « ci », toutes branches :
92 failure · 8 cancelled · 0 success (le plus ancien visible : 2026-05-10)Le workflow est pourtant sérieux — matrice de typecheck, docker-build, api-smoke sur la stack compose, ingest-integration sur un vrai pgvector. Mais il n'a jamais été vert une seule fois, et personne ne l'a remarqué parce que rien ne dépendait de lui.
Conséquence directe sur la feuille de route : ne PAS rendre ces checks requis en l'état. Le faire gèlerait instantanément tous les merges de l'équipe. C'est l'inverse de l'effet recherché.
Séquence corrigée :
| Ordre | Action |
|---|---|
| 1 | Réparer le job api e2e — un test de e2eScan.test.mjs se bloque et atteint le --test-timeout=180000, ce qui fait ensuite dépasser au job son plafond de 15 min |
| 2 | Construire l'index de scan en CI — sans lui, POST /scan … identifies Galarian Obstagoon est SKIP : l'assertion la plus proche du produit ne s'exécute jamais |
| 3 | Glob des tests (V0.3), pytest, gitleaks |
| 4 | Alors seulement, rendre les checks requis |
La leçon, et elle vaut au-delà de ce job. J'ai qualifié cette CI de « solide » en lisant sa définition. Un workflow bien écrit et un workflow qui passe sont deux choses différentes, et seule la seconde protège quoi que ce soit. Même chose pour les tests d'intégration : le backend en a le code, pas l'exécution. Ni le mobile ni le backend n'ont aujourd'hui d'étage d'intégration qui tourne réellement.
Corollaire de gouvernance, à appliquer dès la vague 1 : un badge de statut sur le README de chaque dépôt. Une CI rouge doit être visible sans que personne ait à aller la chercher — c'est ce qui a manqué pendant deux mois et demi.
6.4 Pipeline iOS
Ordre de montage :
- Compte Apple Developer — 99 $/an, depuis developer.apple.com, faisable sous Windows.
- Clé API App Store Connect — c'est elle qui permet à Codemagic de gérer les certificats sans intervention. Sans elle, tu gères les certificats à la main : ne saute pas cette étape.
- Firebase — projet, App Distribution activé, groupe
product-owners. - Codemagic — connecter le dépôt, coller la clé API, cible : build signé → upload Firebase.
- Déclencheur : push sur
staging. Pas sur les PR (pas de secrets en PR), pas surmain(c'est la production). - UDID — chaque iPhone s'enregistre une fois, au premier lancement de Firebase App Tester. Ajouter l'UDID au compte Apple, rebuild avec le profil mis à jour. Ensuite, tout est automatique. Limite Apple : ~100 appareils/an, très largement suffisant.
Le piège qui coûtera une demi-journée si tu l'ignores : le premier build après ajout d'un nouvel UDID doit être refait, l'ancien .ipa ne contient pas le nouvel appareil dans son provisioning profile. Prévois-le lors de l'onboarding de chaque PO plutôt que de le découvrir avec Nini au téléphone.
6.5 Pipeline Android
Gradle tourne nativement sur le VPS Ubuntu — ni compte Apple ni macOS. Même destination Firebase pour garder un seul canal de distribution : un PO ne doit pas avoir à savoir sur quelle plateforme il teste.
6.6 Sécurité de la CI
Trois règles, non négociables dès que des agents lisent des PR.
1. Jamais pull_request_target avec un checkout du code de la PR. Ce déclencheur s'exécute avec le contexte du dépôt de base — donc avec accès aux secrets — et checkouter la branche proposée revient à exécuter du code non fiable avec les clés. Utiliser pull_request : l'absence de secrets par défaut est la bonne propriété, pas une gêne à contourner.
2. Un agent qui relit une PR lit du contenu hostile — titre, description, diff, commentaires. Aucun privilège n'est requis pour l'y placer : ouvrir une PR suffit.
- l'étape qui analyse :
permissions: contents: read, aucun secret ; - l'étape qui publie :
pull-requests: writeet rien d'autre, à partir d'une sortie structurée.
Principe : l'étape qui lit le contenu hostile n'a aucun pouvoir ; l'étape qui a du pouvoir ne lit pas de contenu hostile.
3. Actions épinglées par SHA, pas par tag — un tag est mutable. Dependabot sur github-actions.
Sur les notifications : ne jamais interpoler un titre de PR dans une commande shell. C'est du texte hostile ; il passe par un champ JSON encodé.
6.7 Sécurité applicative et RGPD
Déjà en place, et c'est du bon travail : jeton comparé en temps constant, allowlist explicite de la surface publique POST, fail-closed quand API_WRITE_TOKEN est absent, limites d'upload appliquées deux fois (8 Mo / 1 fichier / 4 champs), SQL intégralement paramétré, manifeste Android propre (allowBackup="false", cleartext interdit par défaut, une seule activité exportée), aucun secret versionné, et la CI teste réellement les 401.
Ce qui protège le système aujourd'hui, c'est Tailscale, pas cette posture. Le jour de l'exposition publique, plusieurs défauts basculent ensemble :
| # | À corriger | Quand |
|---|---|---|
| S1 | CORS_ORIGIN ?? true reflète toute origine → défaut restrictif | V0, une ligne |
| S2 | Rétention des photos (Décision 7) | V0 |
| S3 | npm audit bloquant en high + Dependabot | V0 |
| S4 | Rate limit dédié aux routes de scan (10–30/min/IP) + trustProxy vérifié | avant exposition publique |
| S5 | limitInputPixels sur sharp — bombe de décompression | avant exposition publique |
| S6 | HTTPS obligatoire, hôte configurable au build, tailnet hors variante release | avant exposition publique |
| S7 | Politique de confidentialité + Data Safety + registre des traitements | bloquant Play Store et App Store |
| S8 | Validation stricte des identifiants de cas (regex allowlist) | avant exposition publique |
La règle qui décidera de toute l'architecture des comptes :
Un secret embarqué dans une app mobile n'est pas un secret.
Un APK est un ZIP ; R8 et Hermes compliquent la lecture, ils ne chiffrent rien. Toute clé injectée au build est publique dès la première installation. L'app n'obtient donc que des jetons per-utilisateur émis après authentification, stockés dans le Keystore/Keychain — pas dans AsyncStorage, qui n'est pas chiffré. Et toute autorisation se décide côté serveur, paywall compris : un contrôle d'abonnement fait dans l'app se contourne en dix minutes.
Deux points produit à trancher, pas techniques : Pokémon implique un public partiellement mineur (seuil de consentement autonome à 15 ans en France, à traiter avant l'ouverture des comptes) ; et vendre aux boutiques te place en sous-traitant de leurs données — un DPA sera exigé. Le VPS chez Hetzner, donc dans l'UE, est un argument commercial à documenter.
Pentest : pas maintenant, ce serait de l'argent gaspillé sur une architecture qui va changer. De l'automatisé continu tout de suite (npm audit, Dependabot, gitleaks, semgrep, ZAP baseline nocturne) ; pentest externe déclenché par le premier de ces trois événements — API publique, comptes utilisateurs, premier contrat B2B — 4 à 6 semaines avant, ordre de grandeur 5 000 à 15 000 €, retest inclus au contrat.
7. Le contrat d'équipe
C'est la partie à présenter. Elle tient en quatre pages.
7.1 Modèle de branches
feature/* fix/* chore/* docs/* → staging → main
(intégration) (production)| Depuis | Vers | Approbation | Fusion |
|---|---|---|---|
feature/*, fix/*, chore/*, docs/* | staging | 1 dev ≠ auteur (+ CODEOWNERS si chemin sensible) | Squash, branche supprimée |
staging | main | 2 + CTO | Merge commit |
hotfix/* | main | CTO + 1 dev | Squash, puis back-merge immédiat dans staging |
Le back-merge est le piège classique : un hotfix mergé dans main et jamais reporté réapparaît à la release suivante. À automatiser — un workflow qui ouvre la PR main → staging dès qu'il y a divergence. Ne jamais compter sur la mémoire de quelqu'un.
Pourquoi staging et pas du trunk-based : la latence des stores rend une release mobile non révocable en trente secondes, et deux PO qui poussent ont besoin d'un filet entre leur travail et la production.
7.2 Les quatre portes
| Porte | Contenu | Bloquant ? |
|---|---|---|
| 1. Recevabilité | R1 nom de branche · R2 origine · R3 documentation | Oui — déterministe |
| 2. Correction | typecheck, lint, tests, contrat, gitleaks, build | Oui — déterministe |
| 3. Jugement | R4 adéquation, revue Manu | Non — visible |
| Sécurité Zaher | Oui, en critical/high seulement | |
| 4. Humain | approbations | Oui |
Ne jamais faire d'un verdict LLM un check requis pour la correction. Un modèle est non déterministe. Rendu bloquant sur « ce code est-il correct ? », il produit des faux positifs, l'équipe apprend à le relancer jusqu'au vert, et en trois semaines il ne signifie plus rien — pire, sa présence justifie de relire moins. Seule exception : la sécurité, domaine étroit et asymétrie de coût extrême (un faux positif coûte cinq minutes de discussion, un faux négatif coûte un incident).
7.3 Recevabilité : rien au hasard
Aucune PR n'entre sur staging sans une origine traçable. Pas de code orphelin, pas de « j'en ai profité pour », pas de branche dont le nom ne dit pas ce qu'elle contient.
| Contrôle | Qui décide | |
|---|---|---|
| R1 | Le préfixe de branche correspond au diff réel — docs/* ne touche pas src/ | regex |
| R2 | Rattachée à une issue GitHub, une spec doc/specs/, ou un rapport doc/fixes/ | regex |
| R3 | doc/CHANGELOG.md présent si src/ change ; parité guide/en ↔ guide/fr | regex |
| R4 | Le diff répond à cette origine, ni moins ni plus (dérive de périmètre) | agent |
Trois sur quatre se décident mécaniquement. Ne confie pas à un LLM ce qu'une regex décide mieux, plus vite et pour moins cher.
Le détail qui rend ça indolore pour les PO : GitHub sait créer une branche depuis une issue — bouton Create a branch sur la page de l'issue. Le nom est pré-rempli au bon format et le lien est automatique. R1 et R2 deviennent un clic. C'est le premier geste à leur enseigner ; toute la traçabilité en découle gratuitement.
Échappatoire explicite : le label no-issue autorise une PR sans origine, uniquement pour un chore/* trivial. Visible dans l'historique. Une exception observable vaut mieux qu'une règle contournée.
7.4 Écrire un bon ticket
Nini et Spleen écrivent en langage naturel. Aucun formalisme à apprendre. Cette liste suffit :
- Nom de la feature
- Objectif — le pourquoi, en une phrase
- Plus-value — pour qui, quel bénéfice
- Emplacement — quel écran, quelle section
- UX attendue — comportement, états visuels
- Références concurrents — capture ou lien
- « C'est fini quand… » — 2 ou 3 phrases. C'est le champ le plus important : sans lui, personne ne sait quand s'arrêter ni quoi tester.
- Priorité — bloquant / important / nice-to-have
Le skill spec-from-ticket (§10.4) convertit ça en Given/When/Then structuré. Les PO ne voient jamais ce format.
7.5 Notifications
Ne notifier que ce qui demande une action humaine. Un canal qui reçoit chaque check vert est muté en une semaine — y compris le jour où quelque chose brûle.
| Événement | #ci | #pr-review | #alerts | |
|---|---|---|---|---|
| PR non recevable | ✅ auteur | |||
| PR prête pour revue | ✅ | |||
| Dérive de périmètre (R4) | ✅ | |||
| Blocage sécurité | ✅ @here | ✅ | ||
PR staging → main | ✅ | ✅ | ||
| Déploiement prod en attente | ✅ | ✅ | ||
| Échec de déploiement | ✅ @here | ✅ | ||
| Build disponible pour test | ✅ Nini + Spleen | |||
| Check vert de routine | ✅ |
E-mail réservé à ce qui doit laisser une trace : production, sécurité, release, build à tester. Trois à cinq par semaine au maximum — au-delà, la règle de la première ligne n'est pas respectée.
7.6 Ce qu'il ne faut pas faire
| À éviter | Pourquoi |
|---|---|
| Rendre l'agent bloquant sur la correction | Faux positifs → relances jusqu'au vert → le check ne signifie plus rien |
| Confier R1–R3 à un agent | Une regex le fait mieux, plus vite, moins cher |
Exiger 2 approbations sur staging | Goulot à cinq ; l'équipe apprendra à s'auto-approuver |
| Te mettre dans la liste de bypass | Un garde-fou avec une exception pour l'admin est un garde-fou pour les autres |
| Un seuil de couverture globale | Se contourne en ajoutant du code trivial. La couverture sur le diff pose la bonne question |
| Notifier chaque check vert | Le canal est muté en une semaine |
| Distribuer aux PO avant les portes | Tes PO deviennent des détecteurs de régressions |
| Poser les garde-fous avant la vague 0 | On industrialise des checks verts sur des prémisses fausses |
8. Tenir la charge : cache, file d'attente, redondance
Aucun des trois documents sources ne traite ce sujet, et le dépôt documentation non plus : ni Kubernetes, ni répartition de charge, ni file d'attente, ni cache de scan, ni SLA. Section écrite le 2026-07-28 à partir de mesures faites sur le VPS de production, pas de principes généraux.
8.1 La mesure qui commande tout le reste
Trois scans /scan-lab séquentiels sur le VPS, avec une vraie photo :
0,83 s 0,76 s 0,43 sPuis dix scans lancés en parallèle :
0,83 → 1,23 → 1,64 → 2,08 → 2,55 → 2,96 → 3,39 → 3,80 → 4,26 → 4,66 s
total mur : 4,71 sC'est un escalier parfaitement linéaire. Le parallélisme n'apporte rigoureusement rien : la dixième requête attend que les neuf autres finissent. Le débit réel est de ≈ 2,1 scans/seconde, quel que soit le nombre de clients.
Ce que ça donne pour ta question des 1000 utilisateurs simultanés :
| Débit soutenu | ≈ 2,1 scans/s, soit ~7 600/heure |
| 1000 scans simultanés | le dernier servi attend ≈ 8 minutes |
| Timeout client | 10 s |
| Conséquence | tout ce qui dépasse ~21 requêtes concurrentes échoue côté client — pendant que le serveur, lui, continue de calculer pour des clients partis |
Le système ne s'écroule pas : il sert 21 personnes et fait échouer les 979 autres, tout en consommant du CPU pour rien.
8.2 Où est le goulot, exactement
Par ordre de rupture :
- Le sidecar d'embeddings sérialise. C'est lui, l'escalier. Un seul worker d'inférence DINOv2.
- L'OCR est un singleton explicite — le code le dit : « the worker is a singleton (queues requests internally), a stuck recognition would block every future /scan call ». Ne concerne que
/scan, pas/scan-labque l'app utilise, mais c'est une seconde sérialisation en embuscade. - Aucune limite de ressources sur aucun conteneur — mesuré :
deploy/replicas: 0,limites: 0sur 12 services. Sous pression, l'OOM killer choisit une victime arbitraire, potentiellement Postgres. - Le rate limit ne protège rien : 600/min global, aucune limite par route. Un seul client sature la machine sans jamais l'atteindre.
- Postgres/pgvector n'est pas le problème — l'ANN HNSW répond en 1 à 5 ms.
8.3 Le gain immédiat, et il est gratuit
Mesuré sur le VPS : 4 cœurs, 7,5 Go dont ~4 Go libres. Le sidecar consomme 496 Mo.
Faire tourner 3 workers d'embeddings au lieu d'un multiplie le débit par ~3, sur la machine actuelle, sans rien acheter. C'est de très loin le meilleur rapport effort/gain de cette section — et ça ne demande ni Kubernetes, ni load balancer, ni refonte.
nginx a déjà un bloc upstream : il sait répartir. Docker Compose sait lancer des replicas. Ajouter des instances d'API en revanche ne servira à rien tant que le sidecar sérialise : l'API n'est pas le goulot, elle attend.
Plafond de cette approche : 3 à 4 workers (RAM), et 4 cœurs partagés avec Postgres et le reste. Au-delà, il faut une machine plus grosse, du GPU, ou de l'horizontal.
8.4 Cache de scan — ce qui marche et ce qui n'y changera rien
Un scan est une fonction pure de l'image : en théorie, cacheable. En pratique, deux photos d'une même carte ne sont jamais identiques au bit près — un cache indexé sur le hachage de l'image aurait un taux de succès proche de zéro en usage réel.
| Cacher quoi | Taux de succès attendu | Verdict |
|---|---|---|
| Le résultat de scan, par hachage d'image | quasi nul (photos toutes différentes) | ❌ inutile |
| L'embedding, par hachage d'image | faible — utile surtout en réessai/debug | ⚠️ marginal |
Les métadonnées carte, par card_id | élevé — les cartes populaires se rescannent | ✅ oui |
| Le top-K, par embedding quantifié | variable, et risque de servir un mauvais voisin | ❌ dangereux |
Recommandation : ne pas mettre de cache sur le scan lui-même. Cacher les métadonnées post-appariement (cards + images), TTL court, en mémoire ou Redis.
Et surtout : ne pas croire que le cache résout le problème de charge. Le goulot est l'inférence, pas la lecture. Un cache parfait sur les métadonnées laisserait le débit à 2,1 scans/s.
Le vrai gain côté volume est ailleurs : éviter le scan inutile. Une déduplication côté app sur un réessai de la même photo supprime de vraies requêtes, ce qu'aucun cache serveur ne peut faire.
8.5 File d'attente : borner, et refuser vite
Aujourd'hui il n'y en a aucune. Les requêtes s'empilent dans les sockets, la latence n'est pas bornée, le client abandonne à 10 s — et le serveur continue de travailler pour lui. C'est la frontière async/sync déjà signalée au §2.6 de l'audit, mais elle devient un problème de capacité dès qu'il y a du volume.
Ce qu'il faut, dans cet ordre :
- Une concurrence maximale explicite, égale au nombre de workers d'inférence.
- Au-delà : rejet immédiat en
429avecRetry-After— pas de mise en attente. Mieux vaut dire « reviens dans 3 secondes » que faire patienter 8 minutes pour finir en timeout. - Annuler le travail quand le client est parti : propager l'
AbortSignaljusqu'à l'appel du sidecar. Aujourd'hui, une requête abandonnée coûte quand même son inférence complète. - Une limite par route sur les chemins de scan (10–30/min/IP), distincte du global — c'est déjà l'action S4 du §6.7.
La file asynchrone — scan → identifiant de job → notification — n'est pas recommandée maintenant. Elle change le contrat d'API et l'expérience utilisateur (l'utilisateur attend son résultat, il ne consulte pas une boîte de réception). À réserver au scan en masse (le mode « Bulk » existe déjà dans l'app), où l'asynchrone est naturel.
8.6 Docker, Kubernetes, répartition de charge
Réponse franche sur Kubernetes : non. Pas maintenant, et probablement pas avant longtemps.
Ce que k8s apporte : orchestration multi-nœuds, autoscaling, mises à jour progressives, auto-réparation. Ce qu'il coûte : un cluster à exploiter, par un CTO seul, dans une équipe de cinq dont deux ne sont pas développeurs. Le rapport est défavorable, et il le restera tant qu'aucun de ces critères n'est rempli : plusieurs équipes, plusieurs environnements permanents, autoscaling réellement nécessaire, quelqu'un dont c'est le métier.
Compose fait déjà l'essentiel : 11 politiques de redémarrage, 6 sondes de santé. Ce qui manque n'est pas un orchestrateur, c'est :
- des limites de ressources par conteneur — zéro aujourd'hui sur 12 services ;
- des replicas sur le sidecar — ce que Compose sait faire.
L'échelle suivante, si le besoin arrive : un second VPS avec Docker Swarm, ou un service d'inférence managé pour sortir le goulot de ta machine. Les deux sont un ordre de grandeur moins coûteux à opérer qu'un cluster.
8.7 Reprise sur panne et SLA
Le trou le plus grave de tout ce document
crontab → aucune tâche de sauvegarde
/home/kael → aucun répertoire de sauvegardePostgres n'est sauvegardé nulle part. Il contient le catalogue complet, 143 452 embeddings, et bientôt les collections des utilisateurs. Le guide de déploiement du dépôt documentation demande de sauvegarder caddy_data — les certificats TLS, reconstructibles en une commande — et ne dit rien de la base.
Perdre ce volume, c'est reconstruire par ingest complet puis re-calculer 143 452 embeddings. Des heures, voire des jours de calcul, pendant lesquels le produit ne fonctionne pas.
C'est, à mon avis, l'action la plus urgente de la totalité de ce playbook — devant les garde-fous, devant la CI.
Supervision : absente
/metrics expose du Prometheus, la CI le teste… et rien ne le collecte. Aucun Prometheus, aucun Grafana, aucune alerte. Une panne se découvrira parce qu'un utilisateur se plaint.
Sur le SLA
Ne promets aucun SLA tant qu'il n'y a pas de redondance. Un seul VPS, zéro replica : tout est un point de défaillance unique, et les politiques de redémarrage couvrent le crash d'un processus, pas la panne de la machine.
Un SLA qu'on ne tient pas est pire que pas de SLA — surtout en B2B, où il devient contractuel et engage ta responsabilité. La progression saine est : SLO interne mesuré d'abord, engagement externe ensuite, jamais l'inverse.
Le premier contrat boutique l'exigera. C'est déjà l'un des trois déclencheurs du pentest externe (§6.7) ; c'est aussi celui de la redondance.
Ordre des travaux
| # | Action | Pourquoi ce rang |
|---|---|---|
| R1 | Sauvegarde Postgres automatique + restauration testée | Une sauvegarde jamais restaurée n'est pas une sauvegarde. Rien d'autre ne compte avant. |
| R2 | Limites de ressources par conteneur | Empêche l'OOM killer de choisir Postgres comme victime |
| R3 | Collecte des métriques + alerte | /metrics existe déjà, il ne manque que le collecteur |
| R4 | 3 workers d'embeddings (§8.3) | ×3 de débit, gratuit |
| R5 | File bornée + rejet en 429 (§8.5) | Transforme un effondrement en dégradation lisible |
| R6 | Redondance machine | Seulement ici — et seulement si un engagement externe l'exige |
9. Mesurer les progrès
La §2.3 démonte les notes de l'audit zai parce qu'elles reposent sur des mesures fausses. Démonter une métrique sans en proposer une autre laisse un vide au moment précis où tu voudras montrer à l'équipe que le travail avance. Voici de quoi le combler.
La règle qui gouverne cette section : toute métrique qui pilote une décision doit être produite par un job, pas par un document. Un chiffre dans un rapport est une photo qui vieillit ; un chiffre dans une CI est une mesure. C'est aussi ce qui rend cette page vérifiable sans me relire.
9.1 Les six indicateurs qui comptent
Six, pas vingt : un tableau de bord que personne ne regarde ne vaut pas mieux qu'un canal Discord muté.
| # | Indicateur | Aujourd'hui (mesuré 2026-07-27/28) | Cible | Produit par |
|---|---|---|---|---|
| M1 | Dépôts avec une CI verte sur main | 1 / 3 — le mobile n'a aucun workflow | 3 / 3 | badge README + gh run list |
| M2 | Fichiers de test exécutés / existants | backend 35/37 · mobile 48/48 | 100 %, garanti par testWiring | job de test |
| M3 | Couverture sur le diff | non mesurée | ≥ 80 %, avertissement puis bloquant | job PR |
| M4 | Écrans couverts par un test de rendu | 0 / 22 | 3 (Scanner, CardDetail, Wallet) puis croissance | job de test mobile |
| M5 | Score de mutation sur les 5 cibles | non mesuré | établir la ligne de base, puis ne pas régresser | job hebdomadaire |
| M6 | PR recevables du premier coup (R1–R3) | sans objet | ≥ 90 % après un mois | job de recevabilité |
Comment les lire. M1 et M2 sont binaires : ils sont soit tenus soit non, et ils se réparent en une fois. M3 et M4 sont des trajectoires — ce qui compte est la pente, pas la valeur absolue. M5 n'a pas de cible chiffrée à l'avance : un score de mutation ne se compare qu'à lui-même, et fixer « 80 % » sans ligne de base produit du bourrage de tests. M6 mesure la qualité du process, pas du code : s'il stagne bas, c'est la documentation de la porte qui est mauvaise, pas l'équipe.
9.2 Ce qu'il ne faut pas mesurer
| À éviter | Pourquoi |
|---|---|
| Nombre de tests | 1757 tests protégeant le mauvais code, c'est le point de départ de tout ce document. Le volume n'est pas une garantie. |
| Couverture globale | Se contourne en ajoutant du code trivial, et punit les anciens fichiers. La couverture sur le diff pose la bonne question : ce que tu ajoutes est-il testé ? |
| Vélocité, points, PR par semaine | Mesure l'agitation. À cinq personnes qui se voient, tu n'as pas besoin d'un proxy pour savoir qui avance. |
| Une note globale sur 10 | Précisément l'erreur de l'audit zai : elle agrège des choses non comparables et masque la bimodalité (§2.2-B). |
9.3 Le rituel
Une revue mensuelle, quinze minutes, sur les six lignes de M1 à M6 telles que la CI les rapporte — pas telles qu'on les raconte. Si un indicateur n'est pas produit automatiquement ce jour-là, il ne compte pas et on le dit.
Un seul document se met à jour : ce tableau. Les trois audits sources restent figés, ce sont des photos datées.
10. Skills : purger, compléter, créer
10.1 Ce qu'il faut supprimer
Les deux dépôts ont un .claude/ strictement identique — 16 skills, 6 agents, 34 commandes — copié tel quel d'une app React Native vers un backend Python/Node. La purge est en vague 0 (§5) ; voici la liste exacte, sans laquelle la tâche n'est pas exécutable.
| Cible | Dépôt | Raison |
|---|---|---|
skills/clean-architecture copy/ | les deux | Dossier dupliqué littéralement, avec des references/ que l'original n'a pas. Deux sources de vérité contradictoires sur l'architecture. |
skills/lp-conversion-skill/ | les deux | Copywriting de landing page. Aucun rapport avec l'un ou l'autre dépôt. |
skills/react-native, react-native-best-practices, react-patterns, web-performance | backend | Il n'y a pas de React dans le backend. |
skills/api-design-ts | mobile | L'app ne sert pas d'API. |
commands/scaffold-android.md, commands/import-cards.md | backend | Hors sujet. |
tdd ou canon-tdd | les deux | Deux skills TDD chargés en parallèle. Garder canon-tdd (il a les references/) — sinon on arbitre entre deux doctrines à chaque tâche. |
typescript-patterns ou typescript-craftsmanship | les deux | Recouvrement large, même arbitrage. |
CHANGELOG.md, PROGRESS.md, TODO.md, DONE.md à la racine | backend | Son propre CLAUDE.md les déclare « banned », doc/ fait foi. Ils sont toujours là. |
Rappel de §6.2 :
.claude/passe sous CODEOWNERS le même jour. Dès que quatre personnes peuvent pousser, ces fichiers sont du code exécutable qui décide de ce que les agents autorisent — et personne ne relit un.md.
10.2 Ce qui manque, par ordre d'impact
- Mutation testing — rien, nulle part. Le seul ajout qui fasse passer de « je relis les lignes » à « je fais confiance au harnais ».
- Property-based testing (
fast-check) — pHash, distances de Hamming, embeddings, quad detector. Le domaine exact où l'exemple choisi à la main ne prouve rien. - Test RN (
@testing-library/react-native) — 22 écrans, 0 test de rendu (M4). - Python —
src/embeddings/est en Python avec un seul fichier de test et aucune CI. - BDD/Gherkin exécutable — un
.featureexiste, aucun runner ne le lance. - Release / versioning mobile — signature,
versionCode, canaux Play. Rien. Voir aussi les décisions irréversibles avant publication.
10.3 Les trois skills de métier — la base, quelle que soit la variante
Ceux-là sont prioritaires sur tous les autres, et ils sont indépendants du workflow retenu au §4.2 : que les agents soient pilotés par un cron ou par l'Action GitHub, ils lisent les mêmes skills.
Pourquoi les remplacer plutôt que les compléter
skills/clean-architecture → « adapted to this Next.js 15 App Router + Prisma + React 19 project »
skills/clean-code → « applied to this TypeScript / Next.js / React project »
skills/react-native → annonce React Navigation v6, zustand, SQLiteAucune de ces descriptions ne correspond à Snag. Le mobile est du React Native bare sans React Navigation (navigation écrite à la main), sans zustand, sans SQLite — mesuré. Le backend est du Fastify + pg, sans ORM. Un agent qui lit « Clean Architecture pour ce projet Next.js App Router » applique des patterns de serveur web à une app mobile.
C'est la même pathologie que le CLAUDE.md du §2.2-E : un document faux est pire qu'un document absent, parce qu'on lui fait confiance. Ces trois skills génériques doivent être supprimés (§10.1) et remplacés par les trois ci-dessous, écrits à partir de l'architecture réelle.
snag-architecture — le socle que les deux autres lisent
Ce n'est pas un skill qu'on invoque : c'est la référence que snag-implement et snag-review chargent tous les deux, pour qu'un développeur et son relecteur ne raisonnent pas sur deux modèles différents.
Ce qu'il fixe :
- La stack réelle, mesurée et datée — RN 0.73.6 bare, Hermes activé, New Architecture désactivée, navigation à la main,
@/designcomme seul point d'entrée du design system. Backend Fastify 5 +pg+ pgvector, SQL paramétré, aucun ORM. - Un seul chemin de données. La règle qui sort du défaut le plus grave du §2.2-A : il n'existe qu'une source pour une donnée. Introduire une seconde façon de lire les cartes est un rejet en revue, pas un débat.
- Les écrans n'orchestrent pas.
ScannerScreenpilote aujourd'hui le pipeline de vision (crop, détection, appels backend) : c'est la violation de couche à ne pas reproduire. Un écran affiche et délègue. - L'extraction pour tester est un symptôme, pas un patron. Si une logique doit sortir d'un composant uniquement pour devenir testable, le composant en faisait trop. Le skill le dit explicitement, parce que le dépôt contient neuf fichiers de test qui testent des fonctions extraites plutôt que des écrans.
- Un contrat, une définition. Le champ échangé entre client et serveur est défini une seule fois et les types sont générés des deux côtés. C'est la leçon
sim/similaritytransformée en règle.
snag-implement — le skill de développement
Chargé par : tout agent ou dev qui écrit du code de production.
Ce qu'il impose :
- La boucle canon-TDD telle qu'elle se pratique ici — liste de tests, rouge, vert, refactor — et le niveau d'assertion attendu : comportement, jamais implémentation.
- Où va le code, par délégation à
snag-architecture. Aucune décision d'emplacement n'est laissée à l'improvisation. - Les pièges déjà payés, chacun avec sa contre-mesure — ce sont eux qui font la valeur du skill, parce qu'ils ne se déduisent d'aucun principe général :
- un
require()dans untry/catchne protège de rien en build release Hermes ; - le bundle JS doit être régénéré avant tout build de release, sinon l'artefact embarque du code périmé ;
- un fichier de test ajouté doit être câblé dans un script, sinon il ne s'exécutera jamais ;
- une assertion multi-ligne sur un fichier lu doit être insensible aux fins de ligne, sinon la suite est verte sur Linux et rouge sous Windows ;
- tout
fetchporte un délai maximal.
- un
- Le niveau de test attendu selon la cible : un écran se teste par rendu, une fonction pure par ses bornes, une couture par un test d'intégration. Le skill refuse le contournement consistant à extraire pour tester unitairement.
snag-review — le skill de relecture
Chargé par : l'agent de revue (Manu) et tout humain qui relit.
Ce qui bloque — court, et uniquement du déterministe ou du grave :
- une seconde source pour une donnée existante ;
- une assertion sur une chaîne d'implémentation (le cas
query.test.mjs:assert.equal(whereSql, "WHERE …")casse à chaque refactor sans qu'aucun comportement ne change) ; - une liste de fichiers de test énumérée à la main ;
- un
fetchou un appel externe sans délai maximal ; - du SQL construit par concaténation ;
- un secret, ou un jeton attendu côté client.
Ce qui se commente sans bloquer : le nommage, la taille des fonctions, la duplication consciente, les suggestions d'architecture. Cette séparation est la même que celle des quatre portes (§7.2) — le déterministe bloque, le jugement éclaire.
Ce que le skill interdit au relecteur : demander plus de tests sans dire lesquels, et exiger une abstraction sans une seconde implémentation en vue. Le dépôt porte déjà six interfaces à une seule implémentation (§2.2-A) ; le skill existe aussi pour ne pas en ajouter une septième.
10.4 Les six skills de process
Par ordre de rentabilité. Chacun encapsule une décision déjà arbitrée dans ce document. Ils s'ajoutent aux trois skills de métier du §10.3, ils ne les remplacent pas.
spec-from-ticket — le plus rentable
Déclencheur : une issue étiquetée from-po, ou une commande manuelle. Entrée : la description en langage naturel de Nini ou Spleen. Sortie : une issue structurée — User Scenario, Acceptance Criteria en Given/When/Then, emplacement, hors-scope explicite, états UX (chargement / erreur / vide), contraintes techniques, dépendances. Règles : ne jamais inventer un critère absent — lister les manques en « questions au PO ». Le hors-scope est obligatoire : c'est ce qui prévient la dérive de périmètre que R4 devra sinon rattraper.
pr-receivability — R1 à R3
Déclencheur : ouverture ou mise à jour d'une PR. Nature : déterministe, pas un agent. Un script. Sortie : un commentaire unique mis à jour en place (jamais un nouveau à chaque exécution), avec pour chaque échec la manière de débloquer. Un garde-fou qui explique la sortie est respecté ; un garde-fou qui dit « refusé » est contourné — et c'est encore plus vrai pour des contributeurs non développeurs.
scope-drift — R4
Nature : agent, non bloquant. Évalue : adéquation au ticket, dérive de périmètre, complétude, honnêteté du titre de PR (qui devient le message de commit en squash). N'évalue pas : la qualité du code — c'est Manu. Mélanger les deux produit un commentaire que personne ne lit.
release-mobile
Enchaîne : vérifier que staging est vert → bundle JS → build iOS (Codemagic) et Android (VPS) → upload Firebase → notifier le groupe product-owners → commenter les issues incluses dans le build. Garde-fou codé en dur : refuser de builder si la génération du bundle JS n'a pas eu lieu. C'est le bug déjà rencontré ; il ne doit pas pouvoir se reproduire par oubli.
gdpr-check
Déclencheur : diff touchant le stockage, l'upload, les logs ou un nouveau champ utilisateur. Vérifie : durée de conservation déclarée, base légale, pas de donnée personnelle en log, cohérence avec le registre des traitements.
harness-guard
Déclencheur : diff touchant jest.config, package.json (scripts de test), .github/workflows, .claude/. Vérifie : aucune liste de tests énumérée à la main (glob obligatoire), aucun test skip/todo sans issue liée ni date d'expiration, aucun seuil abaissé sans justification. Raison d'être : c'est le skill qui empêche le harnais de se dégrader silencieusement — le mode de défaillance le plus probable à cinq personnes.
11. Questions ouvertes
Ce que ni les trois documents ni cette réconciliation ne tranchent, et qui demande une réponse de ta part.
Unification des canaux de tickets.
synthese-infrale pose sans le résoudre : cinq canaux (Slack, GitHub Issues, Discord, Hermes, OpenClaw). Ma recommandation : GitHub Issues comme source unique de vérité, les autres canaux servant d'entrées converties par un bot. Sinon la traçabilité R2 est impossible à tenir, et c'est elle qui porte tout le §7.3.Le sort de
tcg-app2. Vivant ou archivé ? Le statu quo — un backend Python à ~2 % de couverture, sans CI, dernier commit du 19 mai — n'est pas tenable à cinq.Qui approuve quand tu es en congé ? Avec 2 approbations + CTO obligatoire sur
main, tu es un point de défaillance unique. À prévoir avant d'en avoir besoin.Environnement de staging côté VPS. Il n'y en a qu'un aujourd'hui. Une PR ne peut donc être exercée que sur la production. Seul poste de ce document qui demande de la ressource machine.
Quota de scans et coût par utilisateur. Le paywall parle de « scans illimités » côté Pro. Quel est le coût réel d'un scan (CPU/GPU sur le VPS) ? Sans cette donnée, le modèle de prix est une hypothèse — et c'est aussi ce qui dimensionne le rate limit S4.
Nini et Spleen poussent-ils vraiment du code ? Si leur contribution est en réalité du contenu et des libellés, une bonne part de la lourdeur CODEOWNERS peut être allégée. À vérifier auprès d'eux plutôt que supposé.
Le renommage Snag, jusqu'où encore ? Partiellement répondu. Le nom produit a été tranché le 2026-07-29 et l'organisation renommée en
snag-tcgle lendemain. Portent encore l'ancien nom : les noms de dépôts (poc-tcg-collectorn'est plus un POC), la marque dans l'app (« Passer à SKN Pro »), le nom affiché sur les stores (« TCG Collector »), et l'applicationId—com.skntcg.collectorest caduque et cet identifiant est définitif à la première publication. Les renommages de dépôts sont réversibles (GitHub garde une redirection) et peuvent attendre ; l'identifiant, non. À trancher avant tout envoi sur un store.
Annexe A — Budget
| Poste | Coût | Quand | Nécessité |
|---|---|---|---|
| GitHub Team (5 sièges) | ~20 $/mois | V0 | Bloquant pour tout le reste |
| Apple Developer Program | 99 $/an | V2 | Bloquant pour iOS |
| Firebase App Distribution | gratuit | V2 | — |
| Codemagic | gratuit puis à l'usage | V2 | Alternative : Mac mini 600–800 € une fois |
| Jetons API Claude (variantes V3/V5 du §4.2) | ~20–100 €/mois à 20 features/mois | quand le quota partagé devient le goulot | Optionnel — le cron sur abonnement (V2/V4) coûte 0 € marginal |
| VPS staging | ~10–20 €/mois | V4 | Recommandé à cinq |
| Pentest externe | 5 000–15 000 € | plus tard | Déclenché par un événement, pas par le calendrier |
Récurrent au démarrage : environ 40 €/mois, plus 99 €/an. À rapporter au coût d'une seule journée perdue à cinq personnes sur une régression que la CI aurait attrapée. Le chiffrage détaillé par variante de workflow (V1 à V5) est au §4.2 — c'est lui qui fait foi pour l'arbitrage budget/trésorerie.
Annexe B — Fiches de présentation
Six écrans pour la réunion d'équipe. Un message par écran.
1 · Où on en est
1757 tests verts. Une CI backend sérieuse. Un design system dont le contraste est vérifié au build. Et : aucune protection de branche, aucune CI mobile, aucun moyen pour un PO de tester. Ce qui tient le projet aujourd'hui, c'est qu'une seule personne y touche. On est cinq lundi.
2 · Le modèle de branches
feature/*→ staging → main Surstaging: 1 relecture. Surmain: 2 + le CTO.mainest toujours livrable. La mise en production est une approbation, pas un merge.
3 · Les quatre portes
Recevabilité — d'où vient ce code ? (bloquant, automatique)Correction — est-ce que ça marche ? (bloquant, automatique)Jugement — est-ce que c'est bien ? (visible, non bloquant — sauf sécurité)Humain — quelqu'un l'a lu (bloquant)
Les machines bloquent sur ce qui se mesure. Les humains décident de ce qui se juge.
4 · Rien au hasard
Toute PR est rattachée à une issue, une spec ou un rapport de fix. Le nom de branche dit ce qu'elle contient. La doc suit le code.
Le geste à retenir : sur une issue GitHub, bouton Create a branch. Nom correct et lien automatique. Un clic, et 90 % de la règle est respectée.
5 · Pour Nini et Spleen
Écrivez vos tickets normalement, en français. Aucun format à apprendre. Un seul champ compte plus que les autres : « c'est fini quand… ». Vous recevrez un e-mail à chaque build testable, installable sur votre iPhone en un tap. Et vous pouvez pousser du code :
stagingest là pour que se tromper ne coûte rien.
6 · La règle qui résume tout
Zéro exécution manuelle. Pas zéro jugement humain.
Tout ce qui se mesure tourne tout seul, à chaque PR, sans que personne y pense. Ce qui se juge — accepter un golden, arbitrer un mutant, fixer le seuil du scan — reste à nous.
Annexe C — Les contradictions réelles
À lire d'une traite. Cette annexe se suffit à elle-même : rien à aller vérifier ailleurs.
Les deux audits ne disent pas la même chose. Ce n'est pas un détail de forme — sur six points ils s'opposent frontalement, et sur quatre d'entre eux suivre le mauvais des deux te ferait travailler pour rien. Voici ce qui les sépare, ce que dit la mesure, et ce que j'en conclus.
1. Le dépôt mobile a-t-il 607 tests ou 1757 ?
L'audit zai bâtit ses notes sur un comptage : 607 cas de test, 127 suites, 55 fichiers de test pour 101 fichiers source.
J'ai relancé la suite le jour même :
Test Suites: 1 skipped, 54 passed, 54 of 55 total
Tests: 4 skipped, 1757 passed, 1761 totalEt sur disque : 48 fichiers de test, 119 fichiers source. Les quatre chiffres sont faux, dont un d'un facteur trois.
Ce que j'en conclus. Les constats qualitatifs de zai restent largement valables — il a vu des choses justes, dont une que j'avais manquée. Mais ses notes chiffrées en dérivent, donc 7,5/10 et 4/10 ne sont pas un état des lieux et ne doivent servir ni d'indicateur de suivi ni de support de réunion.
La leçon dépasse ce rapport : toute métrique qui pilote une décision doit sortir de la CI, pas d'un document. Un chiffre dans un rapport est une photo qui vieillit ; un chiffre dans un job est une mesure. C'est aussi la seule façon de rendre la feuille de route vérifiable sans me relire.
2. L'architecture du mobile : 8/10 ou défaut structurel ?
zai note la conception 8 sur 10, avec « séparation des couches bien contrôlée, couplage accidentel minimal ».
J'ai mesuré les imports réels :
src/data/repos.ts → 6 interfaces, 6 implémentations, toutes nommées Mock*
11 écrans → importent directement @app/data/demo/catalog
design-system/src → 68 imports de @/design, 1 de @/components, 0 de @/screensAutrement dit : une abstraction repository entièrement testée que l'application n'emprunte jamais, et un catalogue de démo — qui contient le calcul du portefeuille, donc l'argent — qu'elle emprunte partout. Plus six écrans morts dans design-system/.
Ce que j'en conclus, et c'est le point le plus important de cette annexe : les deux ont raison, à des altitudes différentes.
zai regarde l'intérieur des modules : types littéraux, injection de dépendances, nommage aligné sur le domaine. À ce niveau, 8/10 est mérité, je le confirme sans réserve.
Je regarde le câblage entre les modules : quel chemin le code emprunte réellement.
Un dépôt peut être excellent module par module et fragile dans son assemblage. C'est précisément le défaut qu'aucune note globale ne capture, et c'est celui qui a produit tes bugs. Ne te rassure pas avec le 8/10 — mais ne conclus pas non plus que le code est mauvais. Il est bien écrit et mal branché.
3. La discipline de test : bonne, ou coupée en deux ?
zai donne 8/10 au mobile. C'est une moyenne, et la moyenne écrase l'information utile.
Le ratio fichiers-de-test sur fichiers-source, commit par commit :
fix(scanner): route mobile scans through SKN tests=7 src=9
fix(scanner): simplify scan mode selection tests=4 src=3
---
feat(screens): redesign every screen tests=1 src=27
fix(ui): resolve the review findings tests=2 src=24Le pipeline de scan est à un pour un — la signature d'un vrai cycle rouge-vert. L'UI est à un pour vingt-sept — un atterrissage en bloc.
Ce que j'en conclus. N'exige pas « plus de TDD » à l'équipe, ce serait une consigne sans cible. Exige des tests de rendu sur trois écrans — Scanner, CardDetail, Wallet — c'est-à-dire exactement là où le ratio s'effondre, et là où passent la vision et l'argent.
4. La « race condition » : le seul cas où suivre zai te ferait perdre ton temps
zai signale une TOCTOU dans le rechargement d'index et recommande « d'implémenter un verrouillage approprié » :
ts
if (scanIndexReloading) return;
scanIndexReloading = true;Node est mono-thread avec une boucle d'événements. Aucune préemption ne peut s'insérer entre ces deux instructions synchrones. Ce n'est donc pas une TOCTOU, et un mutex n'y changerait strictement rien.
Le risque, lui, est bien réel — mais ailleurs : une requête de scan qui commence avant le remplacement de l'index et se termine après peut lire un état incohérent, ou mélanger deux générations.
Ce que j'en conclus. La bonne correction n'est pas un verrou, c'est un échange atomique de référence — construire le nouvel index entièrement, puis réassigner en une instruction — plus la garantie que chaque requête capture la référence une seule fois en début de traitement. Et un test qui fait se croiser un reload et une requête en vol : il n'en existe aucun.
C'est le cas le plus instructif des six : un audit produit par un modèle peut nommer un vrai symptôme avec le mauvais vocabulaire et proposer un remède inopérant. Le filtre, c'est quelqu'un qui connaît le modèle d'exécution. C'est ton rôle, et c'est pour ça qu'aucun de ces trois documents ne se suffit.
5. iOS ou Android ? Trois réponses, dont une dans ton code
| Où | Ce qui est dit |
|---|---|
synthese-infra | iOS prioritaire, « plus bankable » |
claude_audit | Android d'abord, faute de matériel |
poc-tcg-collector/CLAUDE.md | « iOS is not on the roadmap — don't add iOS-specific code paths » |
La décision business tranche : c'est iOS. Mais cela signifie que le fichier lu par chaque agent avant chaque tâche dit exactement le contraire de ta stratégie.
Et ce n'est pas sa seule erreur : il déclare trois bibliothèques comme socle du projet — React Navigation « pinned », zustand, react-native-sqlite-storage — mesurées absentes du package.json. Il annonce aussi une porte QA Maestro/Detox dont ni l'outil ni le dossier n'existent.
Ce que j'en conclus. Corriger ce fichier est l'action la moins coûteuse et la plus rentable de tout le playbook : une heure, avant toute autre chose. Tant qu'il ment, chaque agent — et bientôt chaque nouveau développeur — raisonne sur des prémisses fausses. Un document faux est pire qu'un document absent, parce qu'on lui fait confiance.
Nuance à garder : priorité iOS ne veut pas dire abandon d'Android. Gradle tourne sur ton VPS, gratuitement, sans compte Apple ni macOS. Android reste le chemin de vérification rapide ; iOS devient le chemin de distribution aux PO.
6. « Zéro test manuel » : jusqu'où exactement ?
zai consacre un chapitre entier à l'objectif zéro manuel, à 100 %. J'ai listé trois choses qu'il ne faut pas automatiser.
Ce que j'en conclus : l'accord porte sur l'exécution, le désaccord sur le jugement — et c'est cette distinction qui sauve l'automatisation plutôt qu'elle ne la limite.
Automatiser l'exécution à 100 % : oui, sans réserve. Aucune étape de test ne doit dépendre de quelqu'un qui pense à la lancer — c'est déjà ce qui a laissé six fichiers de test backend ne jamais s'exécuter.
Trois décisions doivent rester humaines, sinon l'automatisation grave les erreurs dans le marbre :
- accepter un golden diff — la régénération est une commande, l'acceptation est une revue ; un golden validé à l'aveugle transforme un bug en référence ;
- arbitrer un mutant survivant — certains sont des équivalents sémantiques, les tuer coûte plus cher que la garantie obtenue ;
- fixer le seuil de similarité du scan — c'est un compromis produit, précision contre rappel. Il se mesure automatiquement, il se décide humainement.
La formule à répéter à l'équipe : zéro exécution manuelle, pas zéro jugement humain.
Et un septième désaccord, qui n'en est pas un : l'impensé
synthese-infra décrit une chaîne complète — ticket, build, signature, distribution, iPhone de Nini — sans qu'aucun test n'ait à passer nulle part.
Ce n'est pas une contradiction explicite avec les deux audits. C'est un angle mort, et c'est le plus coûteux des sept : livrer plus vite du code non vérifié à des non-techniciens revient à transformer tes PO en détecteurs de régressions. C'est le poste le plus cher et le plus démoralisant qu'on puisse leur confier, et ils ne sauront pas distinguer un bug d'une fonctionnalité inachevée.
C'est la raison — la seule — pour laquelle le build iOS de la vague 2 se déclenche sur staging et jamais sur une branche de feature. La distribution vient après les portes, pas à leur place.
Ce qu'il faut retenir avant d'éteindre
Sur les six contradictions, une seule est une erreur franche (les chiffres), une seule mène à une correction inutile (le verrou), deux sont des différences d'altitude où les deux ont raison (l'architecture, la discipline de test), et deux sont des décisions qui t'appartiennent (iOS, la frontière de l'automatisation).
Aucun des trois documents n'est à jeter. Mais aucun n'est à appliquer tel quel — et c'est exactement pourquoi ce playbook existe.