Conventions de code
Conventions de code
Dernière mise à jour : 2026-05-10
Source de vérité : CONTRIBUTING.md. Cette page reformule les règles structurantes.
Règles d'or backend
1. Logger central uniquement
- Tout log passe par
src/lib/logger.ts - Aucun
console.*danssrc/(saufsrc/config.tsqui boot avant le logger) - Niveau filtré via
LOG_LEVEL(debug/info/warn/error) - JSON ligne en prod, pretty en dev
- Préfixer le scope :
logger.info('[meta-sync] ...')
Vérifier :
grep -rn "console\." src/ --include="*.ts" | grep -v src/lib/logger.ts | grep -v src/config.ts
# → doit retourner zéro résultat
2. Variables d'env centralisées
- Toute env var déclarée dans
envSchemadesrc/config.ts(Zod + défaut + commentaire) - Lue uniquement via
import { config } from '<path>/config.js'+config.MA_VAR - Drizzle Kit (
drizzle.config.ts) est la seule autre exception (s'exécute hors compile TS)
Quand on ajoute une var, 3 fichiers en parallèle :
src/config.ts.env.example(commentaire explicite)unraid/boardgame-referee.xml
Et si la var change un comportement structurel, mettre à jour ARCHITECTURE.md.
3. Helper envBool() pour les booléens
- ⛔
z.coerce.boolean()→Boolean("false") === trueen JS (piège VISION_ENABLED=false silencieusement true) - ✅ Utiliser
envBool(defaultValue)défini en haut desrc/config.ts. Comprendtrue/false/1/0/yes/no/on/off.
4. Repository pattern (Phase 2)
- Seuls les fichiers
src/repositories/*.repo.tsont le droit d'importerdb,drizzle-ormou les tables du schéma - Routes, services, middleware, scripts, crons : passent toujours par un repository
- Nommer les fonctions par intention métier (
getByBggId,setIngestStatus), pas par verbe Drizzle (select1) - Si une route a besoin d'une nouvelle requête : l'ajouter au repository
5. Services découpés (Phase 3) — barrel imports
- Un dossier par gros service (
services/rag/,services/ingest/,services/chunking/) - Toujours importer depuis
index.ts(le barrel) du dossier, jamais un sous-module direct - Le barrel est le contrat public, le reste est implémentation
- Aucun fichier > 350 lignes — découper en
types.ts+ un fichier par responsabilité
6. Stages d'ingestion : signature standard
async function runStageX(
game: Game,
...,
push: EventPusher
): Promise<...>
push('event_name', payload)pour l'UI (jamaisconsole.log)- Logger
infopour le diagnostic serveur - Erreurs non-fatales : try/catch local ou autour de l'appel dans le coordinator
- Erreurs fatales : remontent au try global de
coordinator.ts
7. Aucun cache global dans services/rag/retrieve/
- Les structures partagées entre étapes (
seen: Set<string>,candidateRrfScores: Map<string, number>,synergyCards/metaCards) sont créées dansorchestrator.tset passées en paramètre aux 5 étapes — qui les MUTENT explicitement - Aucun cache, singleton, ou module-scope state
- Raison : si une étape crée sa copie locale (par closure ou import circulaire), les passes redécouvrent les mêmes chunks → doublons en sortie ou blending RRF qui s'éteint silencieusement
8. Code partagé — ne pas réinventer
Avant d'écrire un utilitaire, vérifier qu'il n'existe pas dans :
slugify(name):src/lib/utils.ts. Une divergence casserait le lien nom-jeu / collection-Qdrant / nom-fichier-PDF.- Schémas Zod transverses :
src/lib/schemas.ts(askRetrieveSchema,askStreamSchema,askFeedbackSchema,adminFeedbackQuerySchema,gameIngestMetadataSchema,decksParseSchema) + constantesCONTENT_TYPES,RULES_LANGUAGES. Schéma strictement local (usage unique) peut rester dans la route. - Flux SSE frontend :
useEventStream(composables/useEventStream.ts). Ne jamais réécrirefetch().getReader()+ parsing SSE manuel. Pour un endpoint métier, wrapperuseEventStreamdans un composable dédié (cf.useAskStream.ts). - Heartbeat des streams longs (backend) :
streamSSEqui peut rester silencieux >30s ouvresetInterval8s avec{ type: 'heartbeat' }. Clear dansfinally. - Fallback streams longs : si la réponse est persistée en DB avant la fin du stream, exposer
GET /api/<resource>/:idretournant 200/202/404. Front démarre polling si SSE casse +meta.questionIdconnu.
Règles d'or frontend
1. Pas de scoped CSS qui contredit Tailwind
display: flexscoped écrasehidden lg:flexdu template- Préférer Tailwind utilities partout.
@applysi tu dois absolument grouper.
2. Type-check via --build (pas --noEmit)
- La CI fait
npm run build(vue-tsc --build + vite build) --noEmitpeut passer en local et fail en CI- Toujours
npm run buildavant de push
3. Regex avec flag u pour Unicode
- ⛔
\bsans flagurate les boundaries Unicode (é,ñ, accentués) - ✅ Lookahead Unicode-aware avec flag
u:(?=\\s|$|[^\\p{L}\\p{N}_]) normalizeCardKey()(useArbiterMarkdown.ts:74-81) : NFC + lowercase + apostrophe/tiret normalization
4. Composants async pour les modales lourdes
const CardZoomModal = defineAsyncComponent(() => import('./components/CardZoomModal.vue'))
CardZoomModal + DeckImportModal : ~50 KB gzip retirés du bundle initial PlayView.
Conventions de nommage
- Fichiers backend :
kebab-case.ts(claude-ssh.ts,validate-model.ts) - Fichiers frontend :
PascalCase.vuepour composants,camelCase.tspour composables (useAskStream.ts) - Stores Pinia :
useFooStore - Composables :
useFoo - Types :
PascalCase(SessionUser,RetrievedChunk) - Constantes :
UPPER_SNAKE_CASE(STICKY_WITH_DECK_CAP,MAX_INJECTED_IMAGES)
Convention de commits
Convention courte façon Conventional Commits :
type(scope): description courte
Optional body
Exemples actuels :
security(ssh): isolate Claude SSH on dedicated 'oracle' userperf(rag): parallelize Qdrant searches + adopt withTimeout (8 sites)refactor(frontend): split AdminFeedbackView — Detail + helper markdownfeat(ingest): OCR auto pour PDFs scannés (tesseract)
Types courants : feat, fix, refactor, perf, chore, docs, test, style, security.
Linter / formatter
Pas de eslint/prettier formel actuellement. Convention manuelle :
- Indentation 2 espaces
- Pas de
;semi-colons obligatoires (TS le tolère mais on en met) - Quotes : single
'partout sauf JSX (qui utilise double")
Si tu veux ajouter eslint+prettier : config standard Vue + TS, no-config-thrasher.
No comments to display
No comments to display