Revue de code globale — fin Phase 2 (2026-05-06)
Scope : audit du nouveau code Phase 2 livré depuis le précédent audit du 2026-05-02 — modules backend analyst/, earnings/, config/ (déjà partiellement audité), news/, refonte sector via FinnhubSectorClassifier, ajout instrumentType end-to-end, sidebar config et 4 routers @Primary ; côté frontend la sidenav outils chart, les sous-blocs Fondamentaux (analyst + earnings), l'accordéon news, le chart analyse v1+v2+v3 et l'overlay benchmark v2. Modules gelés Phase 0 hors scope, modules portfolio/ / watchlist/ audités uniquement sur leurs interactions Phase 2 (validation autocomplete, instrumentType).
Méthode : revue par agent automatisé, read-only sur le checkout courant, briefée sur les conventions ground truth (CLAUDE.md, architecture.md, ddd.md) et sur les findings que l'audit 2026-05-02 avait remontés. Findings classés par sévérité avec référence fichier:ligne. Limité aux risques objectifs et aux violations de conventions stated.
État du commit au moment de la revue : working tree de la branche feat, dernier commit 31997d2 feat: recommendation analyst. Phase 2 marquée close dans backlog.md / fonctionnalites.md avec news inline minimaliste comme dernier livrable. 6 caches Caffeine, 7 clés runtime, 11 repositories frontend, 4 routers @Primary. Tests : 41 fichiers backend, 18 specs frontend.
Résumé exécutif
App globalement saine et la Phase 2 a tenu le pattern hexagonal proprement : ports/adapters appliqués sans concession à analyst/ et earnings/, fail-soft documenté sur les endpoints Finnhub paid-tier, mappers purs séparés des HTTP clients, mocks avec symboles réservés cohérents (UNKNOWN / RATELIMIT / NOTARGET / NOCALENDAR). Le swap Twelve Data → Finnhub côté sector est bien couvert par le test RoutingSectorClassifierTest qui pin la décision routing.
Principaux risques identifiés : (1) le FinnhubAnalystClient n'a pas de test MockWebServer alors que tous ses voisins (news, earnings, sector, twelvedata) en ont un — c'est une couture observable manquante ; (2) la page /settings/configuration n'expose ni analyst.provider ni earnings.provider alors que le backend les surface dans ConfigKeys.KNOWN_KEYS et que la doc affirme que les 4 toggles y sont éditables — drift contrat silencieux ; (3) RoutingSectorClassifier route twelvedata → Finnhub ce qui veut dire qu'un user qui aurait rempli sa Twelve Data API key mais pas sa Finnhub se verra refuser le sector benchmark sans message évident — la validation côté Settings ne couvre pas ce cas asymétrique. Plusieurs petits items de cohérence sur la stratégie de cache (key prefix présent sur MARKET_CHART_CACHE mais absent ailleurs, contradiction avec la doc).
Forces : i18n FR / EN strictement parallèles (334 keys en miroir), tests-as-documentation très bien tenus sur les mappers Finnhub avec docstring + test names en phrases complètes, pas de wildcard imports neufs, inject() partout côté front, signal-based 100 %, no BehaviorSubject. La discipline RestController/Service/Client reste claire à travers tous les nouveaux modules.
Critique
(blocker ou risque sévère — corruption data, sécurité, contrat cassé)
1. La page /settings/configuration cache deux clés runtime live
frontend/src/app/features/settings/configuration/configuration.ts:14-19ne déclare queTWELVE_DATA_KEY,FINNHUB_KEY,CACHE_TTL_KEY,MARKET_PROVIDER_KEY,NEWS_PROVIDER_KEY. Pas deANALYST_PROVIDER_KEYniEARNINGS_PROVIDER_KEY.backend/.../config/application/ConfigKeys.kt:14-51exporte les 7 clés etGET /api/configles retourne toutes.docs/technique/architecture.md:191-200etCLAUDE.mdaffirment que les togglesanalyst.provider/earnings.providersont éditables depuis cette page.
L'utilisateur pour switcher analyst/earnings provider doit éditer la BDD à la main ou redémarrer avec une YAML changée. C'est exactement ce que la Phase 2 promettait de supprimer.
→ Soit afficher les deux toggles supplémentaires dans configuration.html (avec les keys existantes, le code est paramétrable), soit retirer ces clés de ConfigKeys.KNOWN_KEYS et documenter qu'elles restent YAML-only.
Important
(à traiter en priorité, dans la semaine)
2. FinnhubAnalystClient n'a pas de test MockWebServer
backend/src/test/.../analyst/infrastructure/analyst/contientFinnhubAnalystMappersTest,MockAnalystClientTest,RoutingAnalystClientTestmais aucun test sur le client HTTP lui-même.- À comparer avec
FinnhubClientTest(news),FinnhubEarningsClientTest,FinnhubSectorClassifierTest,TwelveDataClientTest: tous ont un MockWebServer + mappings d'erreurs (401/403/429/5xx/network), test de fail-soft sur le second endpoint, blank-key short-circuit.
Le code dans FinnhubAnalystClient.kt:74-92 (mapping des exceptions HTTP) est dupliqué de FinnhubClient.kt mais n'est exercé par aucun test — le jour où on factorise on perdra silencieusement le bon comportement de fail-soft sur /price-target.
→ Ajouter FinnhubAnalystClientTest calqué sur FinnhubEarningsClientTest (deux endpoints, fail-soft sur le second, suite d'erreurs HTTP, blank-key).
3. Cache de MARKET_CHART_CACHE partiel : seul TwelveDataClient cache, le mock pas
backend/.../market/infrastructure/market/TwelveDataClient.kt:55:@Cacheable(MARKET_CHART_CACHE, key = "'twelvedata|' + …").backend/.../market/infrastructure/market/MockMarketChartClient.kt: pas d'@Cacheable.RoutingMarketChartClientn'a pas non plus d'@Cacheable(dispatcher simple).
Conséquence : en mode mock, chaque dossier rechargé regénère 260 bars synthétiques + une random walk. Pas dramatique en charge dev mais asymétrique, et l'architecture.md affirme « Cache key préfixée par adapter (twelvedata|, mock|) » — la moitié mock| n'existe pas. Plus important : les services news/, analyst/, earnings/, sector cachent au niveau service sans préfixe provider, donc un toggle mock → finnhub continue de servir le mock pour 15 min (cf. RoutingNewsClient docstring : « We deliberately don't include the provider in the key »). C'est intentionnel mais incohérent avec la stratégie de chart cache et la doc induit en erreur.
→ Soit documenter explicitement les deux stratégies (chart = key prefix, autres = service cache sans prefix avec accept-stale-window-on-switch) dans architecture.md et ddd.md. Soit homogénéiser. Pas de patch code, c'est une décision de cohérence.
4. RoutingSectorClassifier route silencieusement vers Finnhub sans dépendance déclarée sur la clé Finnhub
RoutingSectorClassifier.kt:39:PROVIDER_TWELVEDATA -> finnhub.classify(symbol).- Si l'utilisateur a
market.provider=twelvedata+market.twelvedata.api-keyrempli maismarket.finnhub.api-keyvide, il reçoitMarketUnavailableException("Finnhub API key is missing")→ 503 sur le toggle Sector benchmark, alors que tous les autres tools (chart, search) marchent. ConfigTestClient.testTwelveDatavalide la clé Twelve Data isolément et ne probe pas Finnhub. La page Settings ne signale pas l'asymétrie. Ce n'est pas un bug code mais une couture utilisateur invisible.
→ Soit afficher un avertissement dans la card Twelve Data (« Finnhub aussi requis pour le Sector benchmark »), soit rendre le Sector toggle Finnhub-aware dans benchmarkChoicesForCurrentTicker (filtrer si market.finnhub.api-key non set).
5. AppConfigService.set publie ConfigChangedEvent à l'intérieur de la transaction
AppConfigService.kt:74-88est@Transactional, eteventPublisher.publishEvent(ConfigChangedEvent(...))est appelé en dernière ligne avant le commit.CacheTtlListener.onConfigChangedappellecacheManager.setCaffeine(...)qui invalide les caches avant que la nouvelle valeur soit visible aux autres threads. Si la transaction rollback (peu probable mais pas exclu sur lock conflict), le cache aurait déjà été flush.
→ Préférer @TransactionalEventListener(phase = AFTER_COMMIT) sur CacheTtlListener.onConfigChanged, ou publish l'event après le @Transactional (méthode séparée).
6. WatchlistService.add fail-open silencieux sur 503 — masque les outages
WatchlistService.kt:66-76: siSymbolSearchService.validatelèveMarketUnavailableException, on logwarnpuisreturn true→ save passe.- Conséquence : un user clique « Add MSFT » pendant un outage Twelve Data → entrée sauvée sans validation → next dossier load échoue avec 503. Le rationale (l'autocomplete a déjà confirmé) est documenté mais (a) n'est pas testé, (b) ne distingue pas un user qui pousse une string sans avoir utilisé l'autocomplete (curl direct).
→ Acceptable choix produit, mais ajouter un test dédié dans WatchlistServiceTest qui pin ce comportement et idéalement remonter un flag unverified côté DTO front.
7. analystRepository.getForSymbol 404 path : pas de test pour le 503 vs 404 distinguishable côté front
frontend/src/app/features/ticker/ticker.tsdistingueanalystNotCovered()(404) deanalystError()(503).ticker.spec.tsest très large (1964 lignes) mais l'agent reviewer n'a pas confirmé qu'un test pin spécifiquement la branche « status: 404 → notCovered = true ». Pareil pour earnings.- Le pattern est identique à news mais news est plus vieux.
→ Vérifier dans ticker.spec.ts qu'il y a au moins un test pour chaque triplet (loading / 404 / 503) sur analyst et earnings. Si manquant, c'est ajouter 6 specs.
Modérée
(à filer en dette technique, à traiter quand l'occasion se présente)
8. MockMarketChartClient.MOCK_ETF_SYMBOLS ne couvre pas tous les ETFs SPDR mappés
MockMarketChartClient.kt:180-201liste 17 symboles ETF.- Les 11 SPDR sectors + 6 broad markets sont là, mais
MockSectorClassifierpeut router une stock vers un sector ETF quelconque ; un user qui clique « Sector » sur un ticker mock obtientTechnology (XLK)puis le call de bars sur XLK retourne un type ETF — OK. - Mais le commentaire Phase 2 (suite 5 du CHANGELOG) parle de masquer Sector pour les ETF. Sur un ETF qui n'est PAS dans la liste hardcodée (par exemple un ARKK fictif), il sera tagué
STOCK→ toggle Sector visible → 404 inline.
→ Acceptable v1, à documenter comme « limite mock » ou élargir le set.
9. EarningsClient.fetch n'a pas de path "no upcoming announcement" testé en prod
FinnhubEarningsMappers.toEarningsSnapshot:34:if (reports.isEmpty() && nextEntry == null) throw NoSuchElementException.- Le cas réaliste « entreprise vient de publier ses résultats, pas de prochaine date dans les 90 jours » (
reportsnon vide +nextEntry == null) ne throw pas — c'est correct. - Mais le test
FinnhubEarningsMappersTestne couvre que les paths happy / sorted defensively / empty-and-empty-throw. Pas de test pour le path « reports OK + calendar empty » qui est le cas le plus commun en pratique entre deux trimestres.
→ Ajouter un test dédié.
10. FinnhubPriceTarget.toDomainOrNull ignore le cas targetMean == 0 mais targetHigh != 0
FinnhubAnalystMappers.kt:74-80: isEmpty exige les 4 fields à 0.- Si Finnhub retourne
{high: 200, low: 0, mean: 0, median: 0}(cas dégradé partiel), on construit unPriceTargetavecmean=0que la UI affiche0.00 $.
→ Considérer un check plus strict : mean == 0 && median == 0 → null.
11. RoutingSectorClassifier else mismatchant l'ENUM — double validation
AppConfigService.validaterejette toute valeur horsENUM_KEYS[market.provider](mock | twelvedata).RoutingSectorClassifier.classify:40:else -> throw IllegalArgumentException("Unknown market provider: '$provider'"). Cette branche ne peut être atteinte qu'en cas d'incohérence (ligne BDD écrite hors validate, ou refacto).
→ Acceptable défensif, mais à harmoniser avec RoutingMarketChartClient / RoutingNewsClient / RoutingAnalystClient / RoutingEarningsClient qui ont tous le même pattern. C'est consistant — pas un finding seul.
12. MockEarningsClient.previousQuarterEnd peut être off-by-one en début de mois
MockEarningsClient.kt:128-140: sireference(today) est un 1er Mars, lequarterEndMonthcalculé est mars (((3-1)/3)*3+3 = 3),quarterEnd = 31 mars,reference < quarterEnd→ fallback àquarterEnd.minusMonths(3) = 31 décembre.- Le résultat est correct mais le code est tortueux. Une simple table
month → quarter endserait plus lisible.
→ Refacto cosmétique, à filer en dette.
13. La page Configuration n'a pas de test pour le 7e/8e/... key futur
configuration.spec.tsne semble pas tester la résilience face à l'apparition d'une nouvelle key dans la response — le composant filtre parfind(e => e.key === KEY_X)donc les nouvelles keys sont juste ignorées (bonne backward compat) mais on perd la visibilité si une nouvelle key apparaît back sans support front (cf. finding #1).
→ Test « given 8 keys returned by backend, only the 5 wired in the component render » serait un canary utile.
Mineure
(nits, polish, cohérence)
14. FinnhubModels.kt:23 — id: Long mappé en String plus tard, faisable directement
FinnhubNewsItem.id: Longpuisid.toString()dansFinnhubMappers.toDomain. Au lieu, déclarerid: Stringavec un converter Jackson (@JsonDeserialize(using = LongAsStringDeserializer)) éviterait le cast intermédiaire. Cosmétique.
15. FinnhubAnalystModels.kt:42-46 — java.math.BigDecimal qualifié au lieu d'import
- Les autres
Finnhub*ModelsimportentBigDecimal. Cohérence.
16. MockNewsClient.kt:44 — magic number rng.nextInt(10) == 0
- Le « 10 % quiet symbols » est documenté en commentaire mais n'a pas de constante. Mineur.
17. ticker.scss 1673 lignes, 5 occurrences ::ng-deep
- Documenté comme dette ouverte, à pas étendre. RAS.
18. analyst.repository.ts JSDoc dit « Up to 6 monthly snapshots » — la valeur exacte est en backend HISTORY_DEPTH = 6
- Pas faux mais fragile : si on bumpe la const back à 12, la JSDoc front ment. Référence croisée silencieuse.
19. ticker.spec.ts à 1964 lignes
- Le brief réviseur demandait de checker. La taille est explicable (toutes les permutations chart/zoom/overlay/annotation/measure/news/analyst/earnings/watchlist/narrative). À envisager un split par concern (
ticker.chart.spec.ts,ticker.fundamentals.spec.ts, etc.) pour la prochaine session, pas urgent.
20. FinnhubEarningsClient.requireApiKey dupliqué x4 (news, analyst, earnings, sector)
- Le code
if (apiKey.isBlank()) throw MarketUnavailableException("Finnhub API key is missing — set market.finnhub.api-key (env FINNHUB_API_KEY)")est dans 4 fichiers. Uneinternal fun requireFinnhubApiKey(key: String)dans un util shared pour le finnhub package éviterait la dérive du message.
Documentation
(drift doc → code, manques d'explication, etc.)
21. architecture.md:200 affirme « Cache key préfixée par adapter ⇒ pas de collision »
- Vrai uniquement pour
MARKET_CHART_CACHEcôté Twelve Data. Pas vrai pour les 4 autres caches (news-by-symbol,analyst-recommendations,earnings,sector-by-symbol) qui sont au niveau service sans préfixe — cf. finding #3. - À reformuler : « Le chart cache préfixe par adapter ; les autres caches vivent au-dessus du dispatcher et acceptent une staleness post-switch (15 min max). »
22. architecture.md / developper.md / providers.md annoncent analyst.provider et earnings.provider éditables depuis /settings/configuration
- Pas vrai aujourd'hui — finding #1. Soit on patch l'UI, soit on patch tous ces docs en cascade.
23. RoutingSectorClassifier rationale dispersée
- L'asymétrie «
market.provider=twelvedata→ Finnhub côté sector » est documentée dansarchitecture.md,RoutingSectorClassifier.ktKotlin doc, le test, et le CHANGELOG. Beaucoup de prose, mais le fait qu'une key Finnhub vide bloque le Sector benchmark même quandmarket.provider=twelvedatan'est documenté nulle part (finding #4).
24. news.provider, analyst.provider, earnings.provider se partagent la même clé Finnhub mais le swap est asymétrique
- Si l'utilisateur met
news.provider=mock,analyst.provider=finnhub,earnings.provider=finnhub, sa clé Finnhub est consommée par 2 modules et son quota se split. Pas documenté côtédevelopper.mdqui présente les toggles indépendamment.
Récapitulatif
| Niveau | Count | Exemples |
|---|---|---|
| Critique | 1 | Page Settings n'expose pas analyst.provider / earnings.provider (#1) |
| Important | 6 | FinnhubAnalystClient pas de MockWebServer test (#2), cache strategy incohérente (#3), Sector silencieux dépend de Finnhub key (#4), event publish in-tx (#5), watchlist fail-open silencieux (#6), front 404/503 paths à pin (#7) |
| Modérée | 6 | mock ETF set partiel (#8), earnings empty-cal path pas testé (#9), priceTarget partial-zero (#10), routing else-branch (#11), previousQuarterEnd code tortueux (#12), config UI new-key resilience (#13) |
| Mineure | 7 | Magic numbers, JSDoc cross-ref fragile, ticker.spec.ts size, BigDecimal qualifié, requireApiKey x4, etc. |
| Documentation | 4 | Cache prefix claim faux (#21), 4 toggles éditables claim faux (#22), Sector→Finnhub key dependency non doc (#23), Finnhub quota split non expliqué (#24) |
Reco de priorisation (1 demi-journée chacune) :
- Patcher la page Configuration pour exposer
analyst.provider+earnings.provider(15 min — le code paramétrique est déjà là, juste 2 constantes + 2 sections HTML). - Ajouter
FinnhubAnalystClientTestMockWebServer (45 min — calqueFinnhubEarningsClientTest). - Trancher la stratégie de cache (homogénéiser ou documenter explicitement les deux modèles dans
architecture.md) (30 min doc, 1-2 h si on veut homogénéiser). - Décider du UX sur le finding #4 (Sector / Finnhub key) — soit avertissement Settings, soit filtrage côté
benchmarkChoicesForCurrentTicker. @TransactionalEventListener(AFTER_COMMIT)sur le CacheTtlListener (10 min).- Le reste en cleanup commit dédié.