Règles de développement à respecter strictement pendant toute la vie du projet. Ces règles s'appliquent aussi bien à Claude Code qu'au développeur humain.
- TypeScript strict activé (
strict: truedanstsconfig.json). Jamais deany, jamais deas unknown as. - Préférer les interfaces pour les objets publics, les types pour les unions/intersections/utilitaires.
- Toujours typer les props de composant. Utiliser
React.ComponentProps<'button'>pour étendre les props HTML natives. - Valider toutes les données venant de l'extérieur (WS, data channel, Tauri) avec Zod avant usage.
- Commenter chaque hook custom, chaque fonction > 10 lignes, et chaque bloc de logique non triviale.
- Commentaires en anglais dans le code (convention internationale). Explications de haut niveau en français dans les README.
- Ne supprimer un commentaire QUE s'il est devenu obsolète ou faux.
camelCasepour variables, fonctions, hooks (useSignaling,generatePin).PascalCasepour composants et types (PinDisplay,SessionState).SCREAMING_SNAKE_CASEpour constantes globales (PIN_ROTATION_MINUTES = 30).- Pas d'abréviations cryptiques.
controllerPeerConnection>ctrlPC.
- Max 40 lignes par fonction. Au-delà, découper.
- Max 150 lignes par composant. Au-delà, extraire en sous-composants ou hooks.
- Un hook = une responsabilité (ne pas mélanger signaling + WebRTC dans un seul hook).
- Ordre : (1) libs externes, (2) imports
@/absolus, (3) imports relatifs, (4) types, (5) styles. - Jamais d'import relatif qui remonte de plus de 2 niveaux (
../../../..). Utiliser l'alias@/. - Pas d'import circulaire. Vérifier avec
madgeavant chaque merge.
- Jamais de
catch (e) {}vide. Toujours logger ou remonter. - Utiliser des Result types (ou Zod
SafeParseReturnType) pour les opérations qui peuvent échouer légitimement. - Les erreurs de réseau/WebRTC doivent toujours être affichées à l'utilisateur via un toast.
cargo clippyclean, zéro warning toléré. Ajouter#![deny(warnings)]en Phase 5.- Pas de
.unwrap()ni.expect()en production. Utiliser?ou gérer explicitement viaResult. - Documenter chaque fonction publique avec
///(style rustdoc). - Utiliser
thiserrorpour définir les types d'erreur,anyhowuniquement dansmain.rs.
snake_casepour tout (variables, fonctions, modules, fichiers).- Modules regroupés par domaine (voir STRUCTURE.md).
- Les commandes Tauri (
#[tauri::command]) retournent toujoursResult<T, String>(le message d'erreur est remonté au frontend).
- Aucun
unsafesans justification écrite en commentaire avec audit. - Crates avec
unsafe(commeenigo) : isoler derrière une API sûre danscore/. - Secrets jamais loggés, jamais retournés au frontend en clair.
- TypeScript strict également côté server.
- Fastify plugins : toujours async register avec encapsulation des routes.
- Logs via Pino uniquement, jamais
console.logen production. - Validation Zod sur chaque message WebSocket entrant.
- Une feature = un dossier dans
features/. Handler = fonction pure testable. session-manager.tsest le seul endroit qui mute l'état global des sessions.
- Zéro courbe d'apprentissage : un utilisateur doit comprendre quoi faire en < 5 secondes.
- Un écran = une décision. Jamais plus de 2 actions principales par écran.
- Texte en français partout dans l'UI. Pas d'anglais visible pour l'utilisateur final.
- Utiliser shadcn/ui comme base. Customiser via props ou
className, ne pas réécrire. - Spacing via multiples de 4px (système Tailwind par défaut).
- Typography :
text-base(16px) minimum pour le corps,text-lgou plus pour les CTA.
Pour chaque action utilisateur, prévoir :
- Loading : spinner ou skeleton visible.
- Empty : message clair si pas de données.
- Error : toast ou inline message + action de retry possible.
- Success : feedback visuel confirmant l'action (toast, animation).
- Toute action produit un retour en < 100ms.
- Les actions destructives (déconnecter, refuser) ne demandent pas de confirmation supplémentaire si elles sont réversibles (on peut se reconnecter).
- Les connexions entrantes (côté hôte) doivent faire du bruit : son système + popup OS-level + mise en avant fenêtre.
- Tout élément interactif est atteignable au clavier (
Tab+Enter). aria-labelsur tous les boutons icône-only.- Contraste minimum 4.5:1 (WCAG AA).
- Support du mode sombre via classe
dark:(shadcn gère nativement).
- L'app est une fenêtre desktop, pas besoin de responsive mobile.
- Min window size : 900x600. Max : illimité.
- Layout fluide, pas de largeurs fixes au pixel près.
- Colocation : garder le hook, le composant et les types associés dans le même dossier
features/xxx/. - Séparation : la logique métier est dans
features/(hooks), jamais inline dans un composant. - Types partagés : dans
src/types/. Types locaux : colocalisés avec leur module. - Commandes Tauri : jamais d'appel
invoke()direct dans un composant. Toujours via un wrapper typé danssrc/lib/tauri.tsousrc/features/xxx/commands.ts. - Pas d'import circulaire : vérifier avec
madge --circular src/avant chaque PR.
- Local state (
useState) par défaut. - Zustand si un état doit être partagé entre 3+ composants non-reliés.
- Pas de Redux, overkill pour ce projet.
- Jamais de
useState([])pour des collections mutables : préférerMapouSetwrappés.
- Toute donnée venant de l'extérieur (WS, data channel, Tauri commands,
localStorage) est validée via Zod avant usage. - Schémas Zod définis dans
features/xxx/schemas.ts, utilisés côté client ET serveur si possible (monorepo = package partagé possible en Phase 5).
- Jamais de secret en dur dans le code.
- Variables Tauri préfixées
VITE_pour le webview. Côté Rust, viaenv!()ou fichier de config chiffré. .env.exampleà jour avec toutes les variables, commentées.- ID machine, PIN, et données de session : jamais loggés en clair.
- ID machine : chiffré via Tauri Stronghold.
- Log de session : SQLite local via
tauri-plugin-sql. - Pas de
localStoragepour les données sensibles (accessible depuis devtools).
- Toujours utiliser
iceGatheringState === 'complete'avant d'envoyer l'offer finale (trickle ICE possible mais complexifie). - Timeout à 15s sur le handshake. Au-delà, abort + message utilisateur clair.
- Tester systématiquement sur 3 scénarios : même LAN, WAN avec IP publique, derrière double NAT (TURN requis).
- Canal unique
ordered: true, maxRetransmits: 0pour les inputs (low latency > fiabilité). - Messages JSON compacts (pas de champs verbeux). Envisager MessagePack en Phase 5 si bande passante problématique.
- Throttling des événements souris à 60Hz max (1 message / 16ms).
- Reconnexion automatique avec backoff exponentiel (1s, 2s, 4s, 8s, max 30s).
- Heartbeat ping/pong toutes les 30s, timeout 10s.
- Si le signaling tombe en cours de session active : la session P2P continue, on tente juste de se ré-enregistrer en arrière-plan.
- Pour toute librairie de la stack, utiliser Context7 pour obtenir la documentation à jour avant de coder.
- Ne jamais coder une API de librairie de mémoire sans vérification.
- Librairies concernées :
@tauri-apps/api,enigo,fastify,ws,react,@radix-ui/*(via shadcn),zod,zustand. - WebRTC : MDN est prioritaire, mais Context7 peut aider pour les bibliothèques d'abstraction.
README.mdracine : vue d'ensemble, quick start, état d'avancement.desktop-app/README.md: setup local (Rust toolchain + Node), commandes utiles.signaling-server/README.md: deploy guide + variables d'env.- Mettre à jour à chaque fin de phase.
.env.example: toutes les variables documentées avec un commentaire et une valeur exemple.- Exemple :
VITE_SIGNALING_WS_URL=ws://localhost:3001 # URL du serveur de signaling
- Un commit = une tâche atomique.
- Format :
type(scope): description - Types :
feat|fix|refactor|docs|chore|test|perf - Exemples :
feat(pin): add PIN rotation timerfix(webrtc): handle ICE candidate parse errorchore(tauri): upgrade to 2.1.0
- Tag Git à chaque fin de phase :
git tag v0.X-labelpuisgit push --tags. - Branches :
main→ production onlydev→ branche d'intégrationfeat/xxx,fix/xxx→ branches de travail
- Ne jamais committer :
.env,node_modules/,target/(Rust),dist/,*.log,.DS_Store, clés privées, certificats.
- Valider côté serveur toute donnée reçue (jamais faire confiance au client).
- Sanitizer les chaînes avant affichage (
DOMPurifysi HTML, sinon échappement par défaut React). - Le PIN est un secret éphémère : transmission uniquement en WSS (TLS obligatoire).
- Pas d'auth utilisateur pour le MVP, mais consentement explicite obligatoire avant chaque connexion.
- Le consentement est toujours côté hôte, avec nom du pair (ou IP si pas de nom) affiché clairement.
- Timeout 30 secondes sur le consentement (refus par défaut).
- Signaling server : 10 tentatives PIN / IP / 5 min. Dépassement = ban IP 15 min.
- Reconnexion WS : max 5 tentatives / min / machine_id.
- Signaling server derrière reverse proxy avec TLS (Caddy ou Nginx).
- Headers :
Strict-Transport-Security,X-Content-Type-Options: nosniff. - Origines WS autorisées : liste blanche stricte en prod.
- Audit hebdomadaire en phase de dev, quotidien en prod :
npm audit,cargo audit. - Pas de dépendance avec
< 100 weekly downloadsou sans mise à jour depuis > 1 an.
- Jamais logger : PIN, machine_id en clair, SDP, ICE candidates (contiennent des IPs).
- Logger : timestamps, types d'événements, codes d'erreur, durées.
- Logs locaux : rotation automatique, max 10 MB cumulés.
À chaque fin de phase, exécuter dans l'ordre :
- Build —
npm run tauri build+npm run build(signaling) → zéro erreur. - Lint —
npm run lint+cargo clippy --all-targets -- -D warnings→ zéro warning. - Tests —
npm test+cargo test→ tous verts. - Vérification manuelle — Tester le flow complet entre 2 machines (ou 2 instances locales).
- README — Mettre à jour la section "Avancement" + les nouvelles variables d'env.
- .env.example — Synchroniser avec les nouvelles variables.
- Commit final — Message :
chore: complete phase X - Tag Git —
git tag v0.X-label && git push --tags
## Rapport Phase X — [Titre]
### Implémenté
- [Feature A] — description courte
- [Feature B] — description courte
### Non implémenté (et pourquoi)
- [Feature C] — raison (complexité, dépendance manquante, hors scope MVP)
### Problèmes rencontrés
- [Problème] → [Solution appliquée]
### Recommandations Phase suivante
- [Point d'attention, pré-requis, refactoring à prévoir]
### Métriques (si applicable)
- Latence handshake : Xms
- Latence input → action : Xms
- Taille bundle : X MBProcessus systématique en 6 étapes, à suivre dans l'ordre :
- Observer — Lire les fichiers concernés, reproduire le bug. Ne rien modifier à cette étape.
- Diagnostiquer — Identifier la cause racine, pas le symptôme. Lire les logs (console, Tauri devtools, serveur).
- Formuler 2-3 hypothèses — Classer par probabilité. Les présenter clairement.
- Valider — Attendre la validation de Guillaume avant d'appliquer un fix.
- Corriger — Fix minimal. Pas de refactoring simultané.
- Expliquer — Documenter le changement dans le commit + éventuellement dans le README.
- Ne jamais modifier > 1 fichier à la fois sans le signaler explicitement.
- Si le bug implique une API de librairie : consulter Context7 avant tout code.
- Si le bug implique un changement de schéma (BDD locale, messages WS) : STOP et alerter.
- Vérifier tous les logs (console browser,
console_logRust, serveur Fastify) avant de conclure. - Ne jamais supprimer du code "parce que ça marche sans" : comprendre pourquoi d'abord.
- Toujours activer
chrome://webrtc-internals(ou équivalent) pour diagnostiquer les problèmes ICE/DTLS. - En cas de "connection failed" : vérifier successivement STUN, TURN, firewall, NAT type.
- Latence élevée : vérifier le codec négocié (H.264 > VP8 en général), la résolution, la fréquence d'images.
- Erreur "command not found" : vérifier que la commande est bien enregistrée dans
main.rs(.invoke_handler). - Permissions refusées : vérifier
tauri.conf.jsonetcapabilities/default.json. - Build échoue en release : tester d'abord en dev, puis
cargo build --releaseseul pour isoler.
- Lire le PRD.md et STRUCTURE.md avant toute modification de code.
- Respecter l'arborescence définie. Ne pas créer de fichiers en dehors sans justification.
- Utiliser Context7 pour toute API de librairie avant de coder.
- En cas de doute sur une décision architecturale : demander, ne pas improviser.
- Une tâche ambiguë → proposer 2-3 approches avant de coder.
- Ne jamais désactiver un test qui échoue sans documenter pourquoi.