docling-studio/docs/audit/reports/release-0.5.0/07-decoupling.md
Pier-Jean Malandrino efc27932dd refactor(audit): remediate 0.5.0 audit findings — clean architecture, security, DRY, SOLID, perf
Closes the 12 MAJ raised by the release/0.5.0 audit pipeline (cf.
docs/audit/reports/release-0.5.0/summary.md → summary-reaudit.md).

Volet 1 — Reasoning architecture (audits 01/02/06/07 strengthening)
  * Domain ports: LLMProvider, ReasoningRunner, ReasoningParseError
  * Domain DTOs: LLMProviderType, ReasoningResult, ReasoningIteration
  * infra/llm/ollama_provider.py — OllamaProvider with health_check
  * infra/docling_agent_reasoning.py — runner adapter, encapsulates the
    private _rag_loop call (tracked at docling-project/docling-agent#26),
    commits OLLAMA_HOST once at boot (eliminates the per-request env race),
    translates upstream IndexError into ReasoningParseError
  * api/reasoning.py — zero coupling to docling-agent / mellea / docling-core,
    consumes app.state.reasoning_runner via the port
  * main.py — DI wires OllamaProvider + DoclingAgentReasoningRunner at boot
    when REASONING_ENABLED=true and deps are importable
  * Rename RAG_* env vars → REASONING_*, endpoint /rag → /reasoning,
    type RAGResult → ReasoningResult, frontend feature flag wiring,
    i18n strings, tests, docs (BREAKING — pre-1.0 surface, no external
    consumers in production)
  * 17 new tests: adapter unit tests with sys.modules stubs, OllamaProvider
    httpx tests, R3 concurrent-host isolation, R6 multi-iteration trace
    serialization, R13 Protocol conformance via isinstance
  * E2E Karate scenario: nav-reasoning hidden when REASONING_ENABLED=false
  * README — Live Reasoning section (env vars, archi, link to issue #26)

Bloc B — Security (audit 08, dev-only context)
  * docker-compose.yml — DEV DEFAULTS header, OpenSearch DISABLE_SECURITY_PLUGIN
    flagged as dev-only with link to OpenSearch security docs
  * main.py — boot warning if NEO4J_URI is set with the default 'changeme'
    password, so prod operators can't silently inherit it

Bloc C — DRY frontend (audit 05)
  * shared/storage/keys.ts — STORAGE_KEYS centralised (theme, locale)
  * features/settings/store.ts — dead apiUrl ref + orphan i18n keys removed
  * api/schemas.py — DOCUMENT_STATUS_UPLOADED constant

Bloc D — Quality (audits 02/06/07/09/10/12)
  * domain/ports.py — DocumentConverter.supports_page_batching property
    (LSP fix, replaces isinstance(ServeConverter) check)
  * domain/ports.py — VectorStore.ping() (encapsulation, replaces
    _vector_store._client.info() reach-around)
  * api/analyses.py + api/ingestion.py — path params {job_id} → {analysis_id}
    aligned with the user-facing terminology (URLs unchanged)
  * api/documents.py — Path.read_bytes() + generate_preview() wrapped in
    asyncio.to_thread, unblocks the FastAPI event loop on /preview
  * infra/docling_tree.py — PEP 604 union for isinstance (Ruff UP038)
  * src/__tests__/integration/ — cross-feature integration test relocated
    out of features/history/ so feature folders stay self-contained
  * Tightened terminal `assert X is not None` checks (isinstance(.., datetime),
    exact value comparisons)

Validation
  * 446 backend pytest, 202 frontend vitest — all green
  * ruff + ruff format + ESLint + Prettier + vue-tsc clean
  * Re-audit verdict: 0 CRIT / 0 MAJ, score ~94/100, GO

Closes #200
2026-04-29 14:00:00 +02:00

4.1 KiB

Rapport d'audit : Decouplage

Release : 0.5.0 Date : 2026-04-28 Auditeur : claude-code


Score de compliance

Metrique Valeur
Items conformes 13 / 15
Score 86 / 100
Ecarts CRITICAL 0
Ecarts MAJOR 1
Ecarts MINOR 1
Ecarts INFO 0

Ecarts constates

[MAJ] Inter-store coupling in history feature test

  • Localisation : frontend/src/features/history/navigation.test.ts:30-31, 67, 82-83, 143-144, 169-170, 188
  • Constat : Le test navigation.test.ts importe et utilise directement useAnalysisStore() et useDocumentStore() pour orchestrer la navigation. Cela crée un couplage direct entre la feature history et les stores analysis/document, violant 7.2.2.
  • Regle violee : 7.2.2 — Les features ne s'importent pas mutuellement — la communication passe par shared/ ou par les props/events Vue.
  • Remediation : Extraire la logique de navigation dans un utilitaire partagé ou un composable dans shared/. Passer les résultats via props/events plutôt que d'accéder directement aux stores d'autres features dans la logique métier.

[MIN] Frontend API client lacks explicit mock layer support

  • Localisation : frontend/src/features/{analysis,document,chunking,search}/api.ts — aucun pattern d'injection de mock ou de stub
  • Constat : Les fichiers api.ts utilisent apiFetch directement sans interface abstraite. Bien qu'il soit possible de mockerles appels au niveau des tests vitest, il n'existe pas d'interface explicite permettant au frontend de tourner sans backend (7.1.3).
  • Regle violee : 7.1.3 — Le frontend peut tourner avec un mock du backend (les appels API sont isoles dans des fichiers api.ts par feature).
  • Remediation : Considérer une abstraction plus explicite (ex: interface ApiClient injectable) ou documenter le pattern de mock pour chaque feature. Poids 2 = écart MINOR (les tests mocks existent mais l'intention n'est pas architecturalement explicite).

Points positifs

  • Decouplage Frontend/Backend solide : Aucun import croise, contrat API basé sur REST, types TypeScript alignés avec Pydantic via _to_camel (schemas.py). Frontend utilise apiFetch (shared/api/http.ts) comme couche unique.
  • Hexagonal Architecture Backend : Ports dans domain/ports.py définis avec types du domaine (ConversionResult, ChunkResult, etc.). Services importent uniquement domain.ports et domain.value_objects, zéro fuite de types docling.*. Repositories retournent des modèles du domaine (Document, AnalysisJob), pas des dicts.
  • Schemas API fortement typés : Tous les DTOs dans api/schemas.py utilisent Pydantic BaseModel strictement (pas de dict ou Any). Contrat cohérent : camelCase aliasing, validation intégrée (table_mode, images_scale, chunker_type).
  • Isolation des features Frontend : Chaque feature (analysis, document, chunking, history, search, settings, ingestion) a son propre store Pinia, API client et UI. Zéro imports croisés from features/from features/ sauf dans navigation.test.ts.
  • Infrastructure Adapter Boundary : infra/local_converter.py importe Docling (DoclingConverter, CodeItem, etc.) mais retourne uniquement ConversionResult (domaine). Services ne voient jamais les types docling.
  • Configuration d'infra propre : docker-compose.yml et nginx.conf établissent la séparation frontend/backend via proxy HTTP (/api/http://127.0.0.1:8000). CORS déclaré explicitement.

Verdict partiel : GO CONDITIONNEL

Conditions :

  1. Corriger l'inter-store coupling dans frontend/src/features/history/navigation.test.ts (MAJ).
  2. Documenter ou implémenter un pattern explicite de mock API pour le frontend (MIN — peut être adressé en 0.5.1).

Score 86/100 : Satisfait >= 80 (GO). Un seul écart MAJOR, zéro CRITICAL. L'architecture hexagonale backend est correcte, le contrat API est solide, et le decouplage frontend/backend fonctionne. Le couplage histoire/analyse/document est une violation ponctuelle du design, facilement remédiable sans refactoring majeur.