mirror of
https://github.com/MaksTinyWorkshop/_Assistant_Lead_Tech
synced 2026-05-18 08:18:15 +02:00
capitalisation: intégration ~60 entrées RL799_V2 (triage 2026-05-02)
Triage du 95_a_capitaliser.md (~75 propositions) : - 60 entrées intégrées dans knowledge/ (backend, frontend, workflow) - 4 nouveaux fichiers : backend/patterns/tests.md, backend/risques/tests.md, frontend/patterns/general.md, workflow/patterns/general.md - 6 doublons rejetés - Mise à jour des READMEs index pour refléter les nouvelles entrées - 95_a_capitaliser.md restauré à sa structure initiale - 40_decisions_et_archi.md : décision mono-tenant déployable vs SaaS multi-tenant - 90_debug_et_postmortem.md : sub-agents Write indisponible, effet iceberg CI, prisma migrate diffs cosmétiques Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -187,3 +187,268 @@ Quand une fonction crypto travaille en base64 pour la sérialisation, prévoir u
|
||||
### Signal review
|
||||
|
||||
- `buffer.toString('base64')` suivi immédiatement de `decrypt(base64String)` qui fait `Buffer.from(str, 'base64')` → round-trip inutile
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-zod-strict-mutations"></a>
|
||||
## Pattern : Zod `.strict()` systématique sur les schémas de mutation
|
||||
|
||||
- Objectif : bloquer la pollution de champs internes via PATCH/POST/PUT en rejetant tout champ supplémentaire non listé dans le schéma.
|
||||
- Contexte : tout schéma Zod qui valide un payload de mutation côté API.
|
||||
- Quand l'utiliser : systématiquement sur tous les schémas de mutation.
|
||||
- Quand l'éviter : schémas de réponse (où l'API est l'émetteur) ou schémas d'enrichissement intentionnel.
|
||||
- Avantage :
|
||||
- première ligne de défense contre la pollution de payload (`uploadedBy`, `createdAt`, `isAdmin` injectés par un client malveillant)
|
||||
- rejet à 400 avant d'atteindre Prisma → pas de risque de spread accidentel dans `data: parsed.data`
|
||||
- Limites / vigilance :
|
||||
- ne dispense pas de la deuxième ligne de défense : ne JAMAIS spread `parsed.data` directement dans `prisma.update`, construire `data` au champ près
|
||||
- Validé le : 20-04-2026
|
||||
- Contexte technique : TypeScript / Zod — RL799_V2
|
||||
|
||||
### Implémentation
|
||||
|
||||
```typescript
|
||||
export const updateXxxSchema = z.object({
|
||||
name: z.string().min(1).optional(),
|
||||
status: z.enum(['active', 'inactive']).optional(),
|
||||
}).strict();
|
||||
```
|
||||
|
||||
### Combiné avec le repo
|
||||
|
||||
```typescript
|
||||
const data: Partial<UpdateXxxData> = {};
|
||||
if (parsed.data.name !== undefined) data.name = parsed.data.name;
|
||||
if (parsed.data.status !== undefined) data.status = parsed.data.status;
|
||||
// …jamais `data: parsed.data` brut
|
||||
```
|
||||
|
||||
### Test à ajouter
|
||||
|
||||
```typescript
|
||||
test('PATCH .strict() rejette les champs hors-whitelist', async () => {
|
||||
const r = await PATCH(makeReq({ name: 'OK', uploadedBy: 'attacker' }));
|
||||
expect(r.status).toBe(400);
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-rigidification-zod-2-phases"></a>
|
||||
## Pattern : Rigidification Zod en 2 phases (données d'abord, schémas ensuite)
|
||||
|
||||
- Objectif : rigidifier un schéma Zod artificiellement laxiste sans casser la suite de tests en cascade.
|
||||
- Contexte : schéma qui accepte une forme large (`z.string().min(1).max(128)`) pour compenser une donnée hétérogène en base (slugs + UUIDs cohabitent), avant d'avoir uniformisé la donnée.
|
||||
- Quand l'utiliser : tout chantier de rigidification (`.uuid()`, `.email()`, `.enum()`) sur un champ dont la base contient encore l'ancien format.
|
||||
- Quand l'éviter : si la donnée est déjà uniforme — rigidifier directement.
|
||||
- Avantage :
|
||||
- diagnostic séparé : si le commit 2 casse un test, on sait que c'est la rigidification, pas la migration
|
||||
- rollback granulaire : on peut rollback la rigidification sans reperdre la migration
|
||||
- revue plus lisible : un reviewer valide indépendamment "migration correcte" puis "rigidification sûre"
|
||||
- Limites / vigilance :
|
||||
- tentation de tout faire d'un coup → écarter
|
||||
- Validé le : 24-04-2026
|
||||
- Contexte technique : Zod / Prisma — RL799_V2
|
||||
|
||||
### Séquence obligatoire (2 commits séparés)
|
||||
|
||||
**Phase 1 — Normalisation des données** :
|
||||
- Migrer la base (seed, fixtures, lignes legacy via `prisma migrate`)
|
||||
- Adapter tous les consommateurs qui référencent l'ancien format (tests, helpers E2E, scripts admin)
|
||||
- Le schéma Zod reste laxiste à ce stade — il accepte les deux formats pendant la transition
|
||||
- Ajouter un test d'invariant qui valide que la base ne contient plus que le format cible
|
||||
- Commit : `feat(<domaine>): migration <X> + adaptation tests`
|
||||
|
||||
**Phase 2 — Rigidification du schéma** :
|
||||
- Remplacer `z.string()` par `z.uuid()` / `z.email()` / `z.enum()` sur les champs concernés
|
||||
- Adapter les quelques tests qui reposaient sur l'ancienne sémantique laxiste
|
||||
- Vérifier par grep final qu'aucun autre schéma n'a le même pattern laxiste oublié
|
||||
- Commit : `feat(<domaine>): rigidification Zod sur <X>`
|
||||
|
||||
### Signaux de dérive
|
||||
|
||||
- Schéma avec un commentaire "accepte toute chaîne pour compatibilité avec X" → dette à rigidifier dès que X est migré
|
||||
- `.min(1).max(128)` sur un champ conceptuellement UUID/email/enum → forme laxiste en attente de rigidification
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-enum-canonique-sous-ensembles-nommes"></a>
|
||||
## Pattern : Enum canonique + sous-ensembles nommés (vs flags par usage)
|
||||
|
||||
- Objectif : factoriser les règles métier sur une enum partagée par plusieurs domaines fonctionnels sans alourdir l'enum elle-même de flags.
|
||||
- Contexte : enum (rôles, statuts, types) qui sert plusieurs usages avec des règles différentes (annuaire, pointage rituel, mandats administratifs).
|
||||
- Quand l'utiliser : dès qu'un même `enum.filter(r => …)` apparaît à plusieurs endroits avec une règle métier explicite.
|
||||
- Quand l'éviter : si le filtre n'apparaît qu'une fois — laisser inline, l'extraction est prématurée.
|
||||
- Avantage :
|
||||
- chaque sous-ensemble a un nom métier explicite — le lecteur comprend sans chercher
|
||||
- les règles sont localisées au point de définition, pas éparpillées en flags
|
||||
- ajouter un usage = ajouter un sous-ensemble, pas modifier la structure de l'enum
|
||||
- Limites / vigilance :
|
||||
- les sous-ensembles doivent être typés `readonly Role[]` pour bénéficier du narrowing
|
||||
- propagation côté front ET côté Zod backend (defense-in-depth)
|
||||
- Validé le : 21-04-2026
|
||||
- Contexte technique : TypeScript / Zod — RL799_V2
|
||||
|
||||
### Anti-pattern
|
||||
|
||||
```typescript
|
||||
// ❌ Flag par usage, multipliable, illisible
|
||||
export const OFFICER_ROLES = [
|
||||
{ code: 'venerable', label: '...', isRitual: true, isAdmin: true },
|
||||
{ code: 'archiviste', label: '...', isRitual: false, isAdmin: true },
|
||||
// … 12 rôles × 3-4 flags
|
||||
];
|
||||
```
|
||||
|
||||
### Pattern correct
|
||||
|
||||
```typescript
|
||||
export const OFFICER_ROLES = [
|
||||
'venerable', 'premier-surveillant', /* … */ 'archiviste',
|
||||
] as const;
|
||||
type OfficerRole = (typeof OFFICER_ROLES)[number];
|
||||
|
||||
/** Officiers avec fonction rituelle pendant la tenue (pointage). */
|
||||
export const RITUAL_OFFICER_ROLES: readonly OfficerRole[] =
|
||||
OFFICER_ROLES.filter((role) => role !== 'archiviste');
|
||||
|
||||
/** Officiers éligibles à un mandat administratif. */
|
||||
export const MANDATABLE_OFFICER_ROLES = OFFICER_ROLES;
|
||||
```
|
||||
|
||||
### Propagation Zod backend
|
||||
|
||||
```typescript
|
||||
// Le sous-ensemble est utilisé côté front ET côté Zod
|
||||
export const tenueOfficerAssignmentSchema = z.object({
|
||||
role: z.enum(RITUAL_OFFICER_ROLES as readonly [OfficerRole, ...OfficerRole[]]),
|
||||
});
|
||||
// → POST avec role: 'archiviste' = 400, sans duplication de la règle
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-constantes-variant-fige-selecteur-strict"></a>
|
||||
## Pattern : Constantes par variant figé + sélecteur enum strict
|
||||
|
||||
- Objectif : figer dans le code des règles ou textes versionnés via Git tout en sélectionnant l'implémentation à l'exécution via un champ DB (tenant, pays, juridiction).
|
||||
- Contexte : règles métier figées (CGV par juridiction, formats de facture par pays, libellés réglementaires par régulateur) qui doivent rester typées strictement et versionnées via Git, mais sélectionnées au runtime.
|
||||
- Quand l'utiliser : préparation multi-variant **avant** d'avoir réellement plusieurs implémentations, OU cas où on veut des diffs visibles dans la PR à chaque modification (texte à autorité).
|
||||
- Quand l'éviter : règles métier admin-éditables runtime — ces données appartiennent à la DB, pas au code.
|
||||
- Avantage :
|
||||
- une seule source de vérité par variant, typée strictement
|
||||
- étendre l'union à `'A' | 'B'` propage automatiquement la nouvelle option (Zod, UI, tests)
|
||||
- diff visible dans la PR à chaque modification — review éclate sur un mot changé
|
||||
- Limites / vigilance :
|
||||
- throw explicite dans le sélecteur (pas de fallback silencieux) — un drift DB doit échouer fort
|
||||
- pour du texte à autorité, préférer `expect(X).toBe(...)` à `toMatchSnapshot` — diff visible vs snapshot file rarement lu
|
||||
- Validé le : 28-04-2026
|
||||
- Contexte technique : TypeScript / Zod — RL799_V2
|
||||
|
||||
### Structure type
|
||||
|
||||
```
|
||||
packages/shared/src/<domain>/
|
||||
types.ts ← SupportedXCode union fermée + SUPPORTED_X_CODES tuple runtime
|
||||
<variantA>.ts ← Constantes du variant A (typées <Constants>)
|
||||
index.ts ← getXConstants(code) + isSupportedXCode + UnsupportedXError
|
||||
```
|
||||
|
||||
### Source de vérité unique pour le code
|
||||
|
||||
```typescript
|
||||
// types.ts
|
||||
export type SupportedRiteCode = 'REAA';
|
||||
export const SUPPORTED_RITE_CODES = ['REAA'] as const
|
||||
satisfies readonly SupportedRiteCode[];
|
||||
```
|
||||
|
||||
`SUPPORTED_RITE_CODES` est consommé partout :
|
||||
- `z.enum([...SUPPORTED_RITE_CODES] as [...])` côté validation
|
||||
- `<select v-for="code in SUPPORTED_RITE_CODES">` côté UI
|
||||
- `switch` exhaustif dans `getXConstants`
|
||||
|
||||
### Sélecteur avec narrowing runtime
|
||||
|
||||
```typescript
|
||||
export class UnsupportedRiteError extends Error { /* … */ }
|
||||
|
||||
export const isSupportedRiteCode = (v: string): v is SupportedRiteCode =>
|
||||
(SUPPORTED_RITE_CODES as readonly string[]).includes(v);
|
||||
|
||||
export const getRitualConstants = (code: SupportedRiteCode): RitualConstants => {
|
||||
switch (code) {
|
||||
case 'REAA': return REAA_RITUAL;
|
||||
}
|
||||
throw new UnsupportedRiteError(code); // garde-fou DB drift
|
||||
};
|
||||
```
|
||||
|
||||
Côté service backend, `assertSupportedX(record.code)` AVANT d'exposer dans le DTO public — protège contre une row DB qui aurait drift.
|
||||
|
||||
### Tests : assertions explicites pour texte à autorité
|
||||
|
||||
```typescript
|
||||
expect(X.formule).toBe('chaîne exacte'); // diff visible en review
|
||||
|
||||
// Avec glyphes Unicode à risque de swap (ex : ' U+0027 vs ’ U+2019)
|
||||
expect([...X.formule].map(c => c.codePointAt(0)!)).toEqual([0x41, 0x2234, /* … */]);
|
||||
expect(X.formule).not.toMatch(/'/); // anti-régression typographique
|
||||
```
|
||||
|
||||
### Anti-patterns
|
||||
|
||||
- Stocker le texte figé en DB "pour pouvoir l'éditer plus tard" — si le texte est versionné, il appartient au code
|
||||
- Hardcoder le code variant dans la validation UI (`if (code === 'REAA')`) — toujours dériver de `SUPPORTED_X_CODES` runtime
|
||||
- Fallback silencieux dans le sélecteur (`switch (code) { default: return DEFAULT }`) — throw explicite
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-regex-critique-partagee-anti-divergence"></a>
|
||||
## Pattern : Regex critique partagée serveur ↔ client (anti-divergence)
|
||||
|
||||
- Objectif : éviter qu'une règle de validation critique (regex anti open-redirect, format de slug) ne dérive entre serveur (Zod) et client (composant, store, Service Worker).
|
||||
- Contexte : règle de sécurité ou d'intégrité qui doit s'appliquer identiquement des deux côtés.
|
||||
- Quand l'utiliser : règle où une divergence côté un seul des deux mène à un trou (anti open-redirect, anti SQL injection visible client-side, format de path/URL).
|
||||
- Quand l'éviter : règle UX uniquement (pattern d'email pour autocomplétion live).
|
||||
- Avantage :
|
||||
- une seule source de vérité — `packages/shared` ou équivalent
|
||||
- dérive impossible (ou détectée au build TS) si l'import partagé est possible
|
||||
- Limites / vigilance :
|
||||
- si le client ne peut PAS importer le package partagé (cas Service Worker en mode `injectManifest`), DUPLIQUER avec un commentaire `⚠️ DOIT correspondre à <chemin>` + un test croisé qui vérifie l'alignement string-wise
|
||||
- Validé le : 28-04-2026
|
||||
- Contexte technique : monorepo TypeScript — RL799_V2
|
||||
|
||||
### Implémentation (cas idéal — import partagé)
|
||||
|
||||
```typescript
|
||||
// packages/shared/src/dto/push.ts (source de vérité)
|
||||
/**
|
||||
* Regex unique anti open-redirect : démarre par '/' simple (pas '//'),
|
||||
* caractères alphanum + '/_-?&=%.', pas de ':' (bloque 'javascript:').
|
||||
*/
|
||||
export const INTERNAL_PATH_REGEX = /^\/(?!\/)[a-zA-Z0-9/_\-?&=%.]*$/;
|
||||
|
||||
export const pushPayloadSchema = z.object({
|
||||
linkUrl: z.string().regex(INTERNAL_PATH_REGEX).optional(),
|
||||
});
|
||||
```
|
||||
|
||||
### Cas duplication contrôlée (SW mode `injectManifest`)
|
||||
|
||||
```typescript
|
||||
// apps/frontend/src/sw-helpers.ts
|
||||
/**
|
||||
* ⚠️ DOIT correspondre à INTERNAL_PATH_REGEX de packages/shared/src/dto/push.ts.
|
||||
* Le SW (mode injectManifest) ne peut pas importer le package partagé directement.
|
||||
* Test croisé : apps/frontend/src/__tests__/regex-alignment.test.ts
|
||||
*/
|
||||
const INTERNAL_PATH_REGEX = /^\/(?!\/)[a-zA-Z0-9/_\-?&=%.]*$/;
|
||||
```
|
||||
|
||||
### Checklist
|
||||
|
||||
- [ ] Une seule source de vérité, idéalement dans `packages/shared`
|
||||
- [ ] Si duplication forcée : commentaire `⚠️ DOIT correspondre à <chemin>` des deux côtés
|
||||
- [ ] Test croisé qui assert l'alignement string-wise des deux regex
|
||||
- [ ] JSDoc qui rappelle que c'est un contrat de cohérence (revue obligatoire si modif)
|
||||
|
||||
Reference in New Issue
Block a user