Revue de code globale — pré-v0.6.0 (2026-05-17, clôture Phase 4)
Scope : audit de la delta v0.5.1..HEAD (Phase 4 Authentification complète + multi-tenant + Flyway V1 squash), 6 commits, 98 fichiers (+3 833 / −676 lignes). Trois chantiers cumulés : (1) OAuth foundation — Google OIDC avec dual user services (CustomOAuth2UserService + CustomOidcUserService via interface marker AppUserPrincipal), SecurityConfig @Profile("!local-no-auth") avec CSRF cookie-based SPA (CookieCsrfTokenRepository.withHttpOnlyFalse + CsrfTokenResponseFilter), bypass profile local-no-auth (LocalNoAuthSecurityConfig + LocalNoAuthFilter + LocalNoAuthUserInitializer), refacto secrets .env boot-time vs runtime-editable, auth.repository port + adapter HTTP côté SPA, AuthService signal-based primé via provideAppInitializer, interceptor 401→/login, guards authGuard + adminGuard, pages /login et /error standalone, DevX toggle BACKEND_AUTH_MODE avec boutons Tilt ; (2) Multi-tenant user_id FK — Portfolio et WatchlistEntry gagnent un @ManyToOne User, services et repositories scopés par user (PortfolioQueryService, CsvImportService, WatchlistService, SnapshotController), convention logs userId UUID (jamais l'email), AssetRepository.findOwnedTickerRows JPQL avec WHERE a.portfolio.user.id = :userId ; (3) Provider gating + Flyway V1 squash — ConfigKeys.PROVIDER_REQUIRED_KEY map, AppConfigService.set refuse un live sans clé, DTO AllowedValueDto.disabledReason, 5 toggles UI disabled + tooltip, V1→V10 fusionnés en un seul V1__init.sql avec baseline-on-migrate: true + baseline-version: 0.
Méthode : 3 subagents code-reviewer lancés en parallèle sur trois lentilles complémentaires — sécurité & isolation multi-tenant (focus OAuth flow, CSRF, logs redaction, secrets, multi-tenant leak paths), qualité backend (Kotlin idioms, Spring conventions, hexagonal DDD, Flyway squash correctness, tests-as-documentation, KDoc accuracy), qualité frontend (Angular 21 signals + zoneless, auth flow SPA, folder structure core/, i18n, Vitest discipline). Chacun briefé sur les conventions ground truth (CLAUDE.md, architecture.md, ddd.md, skills kotlin-idioms, spring-boot, hexagonal-ddd, folders-structure-backend, angular-component, angular-di, angular-signals, angular-testing, folders-structure-frontend, code-review-excellence) et sur l'audit précédent (2026-05-16-pre-v0.5.1.md) comme format de référence. Sortie : punch-list structurée Bloquants / À discuter / Mineurs avec extrait + suggestion + verdict per-tranche. Synthèse + dédup par le main thread.
État du commit au moment de la revue : branche master, dernier commit committé f9588c2 fix: code-review patches (Flyway baseline-version + minors). Working tree clean. La pre-tag review locale (/code-review lancée plus tôt) avait surfacé un Bloquant B1 (baseline-version: 1 → 0) déjà patché, plus 5 Mineurs + 1 À discuter (E) déjà appliqués. Cet audit pré-tag couvre donc le post-pre-tag state : ce qui reste après les patches B1+Mineurs+E, et ce que la review locale n'avait pas vu (parce qu'elle n'a regardé que les 2 derniers commits, pas la Phase 4 entière).
Résumé exécutif
Phase 4 livre proprement une feature majeure : OAuth2 Google OIDC en production, isolation multi-tenant systématique côté lecture (4 services backend scopés + tests d'isolation alice/bob sur WatchlistService), CSRF re-enabled avec pattern cookie-based SPA correct, profile local-no-auth qui préserve le flow single-user dev. Le Flyway squash V1→V10 produit un état BDD final équivalent au cumulatif historique (vérifié par le reviewer backend : indices, FKs, ON DELETE policies, CHECK constraints, UNIQUE, seed prompt v2 — tout est aligné). Les patterns hexagonaux tiennent : ports outbound en domain/, adapters en infrastructure/, routing @Primary quand plusieurs adapters cohabitent.
2 Bloquants identifiés — patches courts, à appliquer avant tag v0.6.0 :
-
Drift factuel
architecture.md— la section « Schéma de base de données » (lignes ~208, 210, 269) décrit encore 9 migrations séparéesV1__init.sql → V9__app_user.sqlcomme si elles existaient individuellement, et affirme « portfolio/watchlist/config/snapshots restent single-tenant en BDD à v1 ». Ces deux claims sont obsolètes : (a) le squash de ce diff a supprimé les 9 fichiers et fusionné l'historique dans unV1__init.sqlunique ; (b) le multi-tenant FK a été livré dans ce même diff (V10 historique, inclus dans V1 unifié). Un onboarding qui lit la doc et chercheV9__app_user.sqlne trouvera rien — contradiction directe. À patcher en remplaçant le paragraphe par une description du V1 unifié + référence au header du fichier comme source de vérité. -
Violation hexagonale
AuthService.kt:6—AuthService(coucheapplication/) importeAppUserPrincipaldepuisauth/infrastructure/security/. C'est une violation de la règle forte du projet «application/ne dépend que dedomain/, jamais deinfrastructure/». OrAppUserPrincipalest une interface marker pure (val userId: UUIDuniquement, pas de dépendance à Spring Security). La déplacer enauth/domain/lèverait la violation sans effort, et alignerait avec le pattern « les concepts sémantiques de domaine vivent endomain/» (mirror :Role,User). Affecte 5 fichiers à patcher pour les imports (AuthService,AppOAuth2User,AppOidcUser,LocalNoAuthFilter, plusAppUserPrincipallui-même qui change de package).
Verdict global : needs-fix. Les deux Bloquants sont des patches de quelques minutes chacun, sans risque.
11 À discuter : 5 sécurité (forward-headers-strategy pas dans Décisions techniques notables, tests CSRF bout-en-bout absents, DB hit per-request sur AuthService.getCurrentUser() + LocalNoAuthFilter, AppOidcUser non exercé dans AuthServiceTest), 3 backend (SnapshotControllerTest absent malgré le nouvel ownership check, PortfolioQueryServiceTest absent malgré le refactor user-scoped, fragilité du test d'intégration LocalNoAuthIntegrationTest partagé), 3 frontend (auth.interceptor.ts sans spec, error-page.ts sans spec, CLAUDE.md:101 qui décrit l'interceptor comme redirigeant sur 5xx alors que ce n'est plus le cas — drift doc↔code).
~8 Mineurs : WatchlistService.persistNew:100 FQCN inline com.portfolioai.auth.domain.User au lieu d'utiliser l'import du fichier, LocalNoAuthIntegrationTest:61 dev!! après assertNotNull (remplaçable par requireNotNull), AuthService.kt:19 KDoc périmée qui mentionne AppOAuth2User au lieu de AppUserPrincipal après le refacto OIDC, CsvImportServiceTest:56-64 double bloc KDoc successif (le second écrase le premier en doc rendering), login-page.ts:35 effect() sans commentaire // effect-because: inline (la justification est en KDoc de classe lignes 18-22 mais pas au point d'usage comme demande le skill angular-signals), configuration.spec.ts:273-275 commentaire dangling promettant des tests disabledReason côté SPA qui n'ont jamais été écrits (le contrat backend est testé dans ConfigControllerTest.kt, mais pas le rendering UI disabled + tooltip), auth.http.ts:14-22 commentaire qui mentionne logoutSuccessUrl("/") alors que le backend utilise defaultSuccessUrl(app.frontend-url).
Faux positifs / non actionnables filtrés : 4 — (a) application.yml:63 Postgres password en clair (déjà documenté + accepté pour la DB Docker éphémère), (b) app.spec.ts:24 useValue au lieu de useClass pour mock AuthService (service concret sans builders, useValue est l'idiome correct), (c) AuthControllerTest avec addFilters = false qui n'exerce pas la 401 (couverture intégration via LocalNoAuthIntegrationTest, convention du projet), (d) ConfigController.annotateAllowedValue branche « key non listée dans PROVIDER_REQUIRED_KEY » non testée (le cas n'est jamais déclenché car OLLAMA_MODEL n'est pas dans ENUM_KEYS, code path mort par construction).
Forces
- OAuth flow correctement câblé en dual-mode —
userService+oidcUserServicedansuserInfoEndpoint, interface markerAppUserPrincipalqui unifieAppOAuth2User+AppOidcUser. Le bug pré-fix « principal SpringDefaultOidcUserau lieu deAppOidcUser» est explicitement documenté dansarchitecture.md > Phase 4 — authentification, capturé dansjournal-livraisons.md, et le post-mortem a permis de poser une convention claire (les deux services partagentfindOrCreateUser, seul le wrapping en principal diffère). - Multi-tenant isolation systématique côté lecture — 4 services scopés par
authService.getCurrentUser().id(PortfolioQueryService,CsvImportService,WatchlistService,SnapshotControllerviaexistsByIdAndPortfolioUserId).AssetRepository.findOwnedTickerRowsrejoint lePortfolio.user.idvia JPQL natif. Le testWatchlistServiceTestpin l'isolation alice/bob avec un scénario complet (deux users, deux symbols, vérification que A ne voit pas la liste de B). Pattern à généraliser àPortfolioQueryServiceTest(filed sous À discuter). - Flyway squash V1__init.sql équivalent à V1→V10 — l'audit backend a comparé section par section : ordre FK respecté,
CHECK (role IN ('ADMIN', 'USER'))présent,UNIQUE(user_id, symbol)sur watchlist_entry,ON DELETE CASCADEpartout où nécessaire,ON DELETE SET NULLsurticker_narrative_snapshot.prompt_template_id(intentionnel pour absorber les snapshots pré-Phase 3),ON DELETE RESTRICTsurprompt_score.prompt_template_id, seed prompt v2 actif via dollar-quoted verbatim.baseline-on-migrate: true+baseline-version: 0(corrigé en pre-tag depuis1qui empêchait V1 de s'appliquer sur greenfield) — comportement validé empiriquement par l'utilisateur (docker compose down -v && tilt upproduit un schéma fonctionnel). - CSRF cookie-based SPA pattern complet —
CookieCsrfTokenRepository.withHttpOnlyFalse()pour que JS lise le cookie,CsrfTokenRequestAttributeHandlerplain (pas XOR — la SPA forward la valeur raw),CsrfTokenResponseFilterqui force l'écriture du cookie à chaque réponse (Spring 6 le résout lazy par défaut, sans ce filter le cookie n'existe pas et la SPA n'a rien à forwarder). Activé même souslocal-no-authpour matcher le shape prod — pas de divergence dev/prod sur CSRF. AngularHttpClientlit auto le cookieXSRF-TOKENet envoieX-XSRF-TOKENsur POST/PUT/PATCH/DELETE (built-in, pas d'interceptor custom requis). - Logs redaction stricte — convention
userIdUUID documentée dans.claude/CLAUDE.md > Conventions. Vérification croisée :CustomOAuth2UserService.findOrCreateUserlogid=(UUID),CsvImportService.importloguserId=(UUID),LocalNoAuthFilter+LocalNoAuthUserInitializerne loggent jamais l'email. Aucun site qui toucheUseret qui aurait pu logger l'email (subagent sécurité a grep-é exhaustivement). - Secrets management séparé boot-time vs runtime-editable —
.env(gitignored) pour OAuth creds +APP_ADMIN_EMAILS+APP_FRONTEND_URL(lus par leserve_cmdTilt → exportés au sous-process gradle → relaxed binding Spring),app_configtable pour Anthropic/Twelve Data/Finnhub keys (runtime-editable via/settings/configuration).application-local.ymlvolontairement vide de credentials.application.ymlcommitted n'a aucun secret en clair, juste des patterns${ENV:default}. - DevX toggle
BACKEND_AUTH_MODE— pattern propre où le mode d'auth (no-auth vs oauth) est lu au runtime dans leserve_cmdshell du Tiltfile (pas au parse Starlark), 2 boutons Tilt qui flip la valeur en éditant.env+ touchapplication.ymlpour redéclencher le serve_cmd. Permet de basculer entre les deux modes sans recompile. - Provider gating end-to-end — backend
ConfigKeys.PROVIDER_REQUIRED_KEYmap + validationAppConfigService.setqui refuse 400 si la clé requise est blank ; DTOAllowedValueDto.disabledReasoncarrying la property path de la clé manquante ; UI consomme et rend disabled + tooltip i18n. Tests backend ajoutés en pre-tag (ConfigControllerTest > GET config annotates allowedValues with disabledReason when the required secret is blank+ mirror négatif). - Tests-as-documentation respecté sur les nouveaux fichiers —
AuthServiceTest,LocalNoAuthIntegrationTest,WatchlistServiceTest(extension multi-tenant),CsvImportServiceTest(extension userId) ont tous une class-level docstring qui nomme l'aire testée + les failure modes + les invariants à protéger. Test names en full sentences, fixtures réalistes (dev@local.test), un scenario par test (assertions multiples uniquement quand elles décrivent le même scenario).
Punch-list détaillée
Bloquants
B1. docs/technique/architecture.md:208-210 + 269 — Drift factuel sur le schéma Flyway
La section « Schéma de base de données » a été rédigée pré-squash. Trois claims sont obsolètes depuis ce diff :
- Ligne 269 : « Neuf migrations Flyway :
V1__init.sql(schéma Phase 0 historique...),V2__ticker_narrative.sql...,V9__app_user.sql(Phase 4 — ...) ». Les 9 fichiers ont été supprimés. SeulV1__init.sqlexiste. - Ligne 208 : « table
app_userV9 » — référence à un fichier qui n'existe plus. - Ligne 210 : « portfolio/watchlist/config/snapshots restent single-tenant en BDD à v1 » — faux depuis V10 historique (inclus dans V1 unifié).
portfolioetwatchlist_entryont des FKuser_id.
Suggestion : remplacer le paragraphe par une description courte du V1 unifié + référence au header du fichier comme source de vérité. Ré-énumérer les tables sans accrocher à des numéros de version.
B2. backend/src/main/kotlin/com/portfolioai/auth/application/AuthService.kt:6 — application/ importe infrastructure/security/
import com.portfolioai.auth.infrastructure.security.AppUserPrincipal
Violation hexagonale forte. AppUserPrincipal est une interface marker pure :
interface AppUserPrincipal { val userId: UUID }
Aucune dépendance à Spring Security elle-même. C'est un concept de domaine (l'identifiant du principal authentifié), pas un détail d'implémentation.
Suggestion : déplacer auth/infrastructure/security/AppUserPrincipal.kt → auth/domain/AppUserPrincipal.kt. Mettre à jour les imports dans : AuthService.kt, AppOAuth2User.kt, AppOidcUser.kt, LocalNoAuthFilter.kt (4 fichiers + le déplacement). 5 minutes de patch, lève la violation sans effet de bord.
Note : UserRepository importé sur la ligne précédente traverse aussi application → infrastructure, mais ce pattern est accepté dans tout le projet (Spring Data JpaRepository est framework-tied par construction — c'est documenté dans architecture.md > Patterns transverses backend > Ports outbound dans domain/). Le cas AppUserPrincipal est différent — pas un repository, juste un concept sémantique.
À discuter
A1. architecture.md > Décisions techniques notables > Phase 4 — forward-headers-strategy pas dans les Décisions
Le commentaire dans application.yml:7-13 documente le risque + la décision « framework choisi pour simplifier dev + matcher prod derrière LB ». Le ticket backlog Phase 5 🟡 est filed. Mais cette décision technique n'est pas dans les Décisions techniques notables d'architecture.md — alors que d'autres décisions équivalentes (@ManyToOne User cross-context, defaultSuccessUrl configurable, etc.) y figurent.
Suggestion : ajouter un paragraphe court dans architecture.md > Décisions techniques notables > Phase 4, sur le modèle du paragraphe « Multi-tenant FK » ajouté pre-tag. À discuter avec l'utilisateur si la décision est suffisamment notable, ou si le ticket backlog + commentaire YAML suffisent (option pré-tag retenue : ticket suffit).
A2. CSRF — pas de test bout-en-bout « POST sans token → 403, POST avec token → 200 »
Le wiring est correct (vérifié manuellement), mais aucun test n'épingle le contrat. LocalNoAuthIntegrationTest valide uniquement GET /api/me qui est exempté CSRF (les GET sont permitAll par défaut sur CSRF). Le risque concret reste faible (SPA Angular + same-origin via proxy en dev + cookie + header builtin), mais l'invariant n'est pas testé.
Suggestion : ajouter dans LocalNoAuthIntegrationTest un test POST /api/watchlist sans X-XSRF-TOKEN → 403, puis avec token lu du cookie → 2xx. Ancre le contrat de manière vérifiable.
A3. AuthService.getCurrentUser() — DB hit par invocation
fun getCurrentUser(): User {
...
return userRepository.findById(principal.userId).orElseThrow { ... }
}
Chaque requête API qui appelle authService.getCurrentUser() (4 services + 1 controller) déclenche un SELECT sur app_user. Un endpoint composite (dashboard load = portfolio + watchlist + snapshots) totalise 3+ DB hits sur app_user par request. Acceptable single-user, mais à filer en dette Phase 5.
La KDoc justifie le choix (« always get fresh fields »), pas de @Cacheable parce qu'un role peut changer SQL-live. Le bon design serait un cache request-scoped (@RequestScope bean qui charge une fois et expire en fin de request). Pas pré-tag.
A4. LocalNoAuthFilter — DB hit par requête HTTP
Idem A3 mais côté profil dev : chaque request en mode local-no-auth fait un userRepository.findByEmail(LOCAL_DEV_EMAIL). En dev le proxy ne forward que /api/** + /oauth2/** + /logout + /login/oauth2/** (pas les assets statiques), donc le traffic est borné aux appels API. Mais si quelqu'un branche le backend directement, chaque static asset déclenche un select.
Suggestion : cacher le devUser dans un field du filter (initialisé lazy au premier appel + invalidé si row pas trouvée). Optimisation cosmétique, pas pré-tag.
A5. AuthServiceTest — AppOidcUser non exercé
Les tests utilisent uniquement AppOAuth2User comme principal. Le production path Google OIDC produit AppOidcUser. Le cast as? AppUserPrincipal fonctionne via l'interface, mais la branche n'est pas pin par un test.
Suggestion : ajouter un test getCurrentUser résout aussi un AppOidcUser principal qui prouve que le cast vers l'interface (pas vers la classe concrète) est stable.
A6. SnapshotController — pas de spec dédié malgré le nouvel ownership check
SnapshotController injecte maintenant AuthService pour vérifier existsByIdAndPortfolioUserId. La logique d'ownership + la projection DTO + le path d'erreur NoSuchElementException("Snapshot $id not found") ne sont couverts par aucun test.
Suggestion : SnapshotControllerTest @WebMvcTest avec deux scenarios — happy path retourne les snapshots du user courant, 404 quand l'ID appartient à un autre user.
A7. PortfolioQueryService — pas de spec dédié
Le refactor systématique des 4 méthodes vers findByUserId… ou findByIdAndUserId n'a pas de test. WatchlistServiceTest couvre l'isolation alice/bob côté watchlist ; le symétrique côté portfolio manque.
Suggestion : PortfolioQueryServiceTest sur le modèle de WatchlistServiceTest — mock AuthService, vérifier que findAll() passe currentUser.id à portfolioRepository.findAllByUserId(), et que findById sur un ID appartenant à un autre user throw NoSuchElementException.
A8. LocalNoAuthIntegrationTest — fragilité de la DB partagée
Le test boot Spring + tape la vraie Postgres. Si quelqu'un a modifié le user dev@local.test via SQL dans sa DB locale (e.g. changé son rôle à USER pour tester l'expérience), le test échoue avec un message trompeur. C'est le pattern du projet (« no DB mocks »), donc conforme — mais ce test est plus fragile qu'un test unitaire contre une DB de test dédiée. Pas pré-tag.
A9. auth.interceptor.ts — pas de spec
Load-bearing : seule règle de redirection globale 401→/login pour /api/**. La logique des trois skip conditions (/api/me, /api/config, non-/api/ URLs) est non-triviale.
Suggestion : 5 scenarios à couvrir — (a) 401 sur /api/market/... → auth.clear() + navigate /login, (b) 401 sur /api/me → pas de redirect, (c) 401 sur /api/config → pas de redirect, (d) 500 sur /api/market/... → pas de redirect, erreur propagée, (e) 401 sur /oauth2/authorization/google → pas de redirect (non-/api/).
A10. error-page.ts — pas de spec
Trois chemins : signOut() → logout() + navigate /login sur next + error, backToLogin() → clearError() + navigate, lecture query params via toSignal(route.queryParamMap). Le troisième est subtil (résolution depuis snapshot si synchrone).
A11. .claude/CLAUDE.md:101 — drift factuel sur l'interceptor 5xx
// CLAUDE.md ligne 101 (description actuelle — incorrecte)
401 → `clear()` + redirect `/login` ; 5xx → redirect `/error` avec query params `status` + `url`
Le code auth.interceptor.ts ne fait pas de redirect 5xx (décision intentionnelle, documentée dans la JSDoc + dans app.routes.ts). Mais CLAUDE.md reflète encore le plan initial. À corriger pour ne pas induire en erreur la prochaine session.
Suggestion : remplacer par 401 → clear() + redirect /login. Les 5xx ne sont pas interceptés ici — les composants gèrent leurs erreurs en local (fail-soft, banners inline) ; /error reste atteignable par navigation directe.
Mineurs
M1. WatchlistService.kt:100 — FQCN inline com.portfolioai.auth.domain.User
private fun persistNew(
user: com.portfolioai.auth.domain.User, // ← FQN inline
symbol: String,
...
User est déjà importé en haut du fichier (utilisé par WatchlistEntry constructor). FQCN redondant. Patch : remplacer par user: User.
M2. LocalNoAuthIntegrationTest.kt:61 — dev!! après assertNotNull
assertNotNull JUnit n'impacte pas le type flow Kotlin → dev!! obligatoire. Alternative plus propre : requireNotNull(dev) { "..." } qui retourne non-nullable.
M3. AuthService.kt:19 — KDoc mentionne AppOAuth2User au lieu de AppUserPrincipal
// Throws [IllegalStateException] when no authentication is set or the principal isn't an
// [AppOAuth2User] — both indicate a wiring bug, not a user-facing error.
L'implémentation cast vers AppUserPrincipal (interface). Le message d'erreur runtime est correct (« expected AppUserPrincipal »), la KDoc reste périmée depuis le refacto OIDC.
M4. CsvImportServiceTest.kt:38-64 — double bloc KDoc successif
Deux /** ... */ blocs adjacents — le second écrase le premier dans la doc rendering. Fusionner en un seul bloc avec une sous-section dédiée à @MockitoSettings(strictness = Strictness.LENIENT).
M5. login-page.ts:35 — effect() sans commentaire // effect-because: inline
Le skill angular-signals > Rules if you do reach for effect() impose un commentaire // effect-because: au point d'usage. La justification existe en KDoc de classe (lignes 18-22) mais pas au niveau du effect() lui-même. Patch : ajouter 3 lignes de commentaire au-dessus du effect().
M6. configuration.spec.ts:273-275 — commentaire promettant des tests qui n'existent pas côté SPA
// Phase 4 — allowedValues changed from string[] to AllowedValue[] (value + disabledReason)
// to support provider gating in the UI. We assert on the mapped values only ; the
// disabledReason path is exercised in dedicated tests below.
Confusion : le pre-tag a ajouté les tests disabledReason dans ConfigControllerTest.kt (backend), pas dans configuration.spec.ts (frontend). Le commentaire ment.
Deux options :
- (a) Supprimer la promesse fausse : remplacer le commentaire par « Le contrat disabledReason est testé côté backend dans ConfigControllerTest.kt. Côté SPA, le rendering disabled + tooltip n'a pas de spec dédié. »
- (b) Honorer la promesse : ajouter un fixture MARKET_PROVIDER_WITH_DISABLED ({ value: 'twelvedata', disabledReason: 'market.twelvedata.api-key' }) et un test qui vérifie le toggle est disabled + le tooltip contient la clé.
M7. auth.http.ts:14-22 — commentaire mentionne logoutSuccessUrl("/")
Le commentaire dit que Spring redirige vers / après logout. Mais SecurityConfig utilise defaultSuccessUrl(app.frontend-url). Drift de détail dans un commentaire de code, pas une erreur fonctionnelle.
M8. AppConfigServiceTest — pas de test Ollama explicite pour le provider gating
Le test set refuses to switch to a live provider when the required API key is empty couvre twelvedata, finnhub, claude, mais pas le cas symétrique ollama (aucune clé requise, jamais refusé). Couvert implicitement par set allows mock provider…, mais une assertion explicite sur Ollama serait claire.
Verdict
needs-fix
Deux Bloquants requièrent un patch avant tag — tous deux à courte cible :
- B1 : 1 paragraphe d'
architecture.mdà réécrire pour refléter le V1 unifié (5 minutes). - B2 : 1 fichier à déplacer (
AppUserPrincipal.ktdeinfrastructure/security/versdomain/) + 4 imports à mettre à jour (5 minutes).
Les 11 À discuter et 8 Mineurs sont des améliorations à arbitrer par l'utilisateur — la décision pré-tag se fera item par item. Aucun ne bloque le tag, mais certains méritent d'être patchés dans le même pass (A11 le drift CLAUDE.md, M3 la KDoc périmée, M5 le effect-because inline, M6 le commentaire menteur, M7 le drift logoutSuccessUrl) parce qu'ils racontent une histoire « doc & code drift après refacto » cohérente à corriger en bloc.
Les autres À discuter (A1 forward-headers dans Décisions, A2 tests CSRF, A6/A7/A9/A10 specs manquants, A3/A4 DB hits) sont du polish ou de la défense en profondeur — à filer en backlog si non patchés pré-tag.
Faux positifs filtrés
Quatre suggestions des subagents écartées après vérification :
application.yml:63Postgres password en clair — délibéré pour la DB Docker éphémère locale (gitignoredapplication-local.ymln'a pas de surcharge). Documenté dansdeveloppement.md. À reprendre en Phase 5 deploy via secret manager — déjà ticket dette.app.spec.ts:24useValueau lieu deuseClass—AuthServiceest un service concret (pas un port abstrait avec builders hérités).useValueest l'idiome correct du projet pour les services concrets ;useClassne s'applique qu'aux ports.AuthControllerTestavecaddFilters = falsequi n'exerce pas la 401 — couverture intégration est explicitement déléguée àLocalNoAuthIntegrationTest. Le test@WebMvcTestreste valable pour la couche controller. Documenté dans le KDoc du test.ConfigController.annotateAllowedValuebranche « key non listée dans PROVIDER_REQUIRED_KEY » — code path mort par construction.OLLAMA_MODELn'est pas dansENUM_KEYS(STRING free-form), doncannotateAllowedValuen'est jamais appelé pour lui. Le code retourne proprement(value, null)dans tous les cas.
Référence
- Audit précédent :
2026-05-16-pre-v0.5.1.md(delta Phase 3 + dette tech + infra Claude). - Patches pre-tag déjà appliqués (avant cet audit) :
f9588c2 fix: code-review patches (Flyway baseline-version + minors)— couvre B1 (baseline-version: 0), 5 Mineurs (V1 header,@TestPropertySourceretiré, 2 refs V10 dans repositories,app.routes.tscomment), et À discuter E (2 testsdisabledReasoncôté backend). - Méthode : 3 subagents
code-revieweren parallèle, output : 1 verdict per-tranche (mergeable+needs-fix+mergeable), synthèse + dédup main thread.