# Architecture Picarones — manifeste > **Audience** : développeurs et mainteneurs. Ce document explique > *pourquoi* le code est organisé comme il l'est, pas seulement *où > sont les fichiers*. Pour la liste exhaustive des modules, lire > directement le code — il est typé et documenté. ## Statut `0.9.0` — une seule arborescence canonique À la release `0.9.0` (mai 2026, clôture du rewrite architectural), Picarones a **une seule arborescence**. Tous les paquets pré-rewrite ainsi que leurs sous-paquets transitoires ont été supprimés au cours des sprints A-H. Pour le détail historique, voir le [CHANGELOG section 0.9.0](../../CHANGELOG.md) et [`docs/archive/2026-migration/`](../archive/2026-migration/). Toute documentation, tout commentaire qui mentionne « deux arborescences » ou « legacy en cours de retrait » est obsolète. La seule cohabitation acceptable depuis `0.9.0` est celle entre **modules canoniques** : par exemple `evaluation/metric_registry` (module-level, side-effect d'import) et `evaluation/registry/registry` (instance-based) — deux patterns volontairement coexistants pour deux usages distincts (auto-discovery vs DI explicite). Le test `tests/architecture/test_no_legacy_imports_in_rewrite.py` verrouille cet invariant via `LEGACY_PACKAGES = ()`. ## Architecture — 8 couches concentriques ``` domain → formats → evaluation → pipeline → adapters → app → reports → interfaces ``` **Règle de dépendance stricte** : les flèches d'import vont uniquement de l'extérieur vers l'intérieur (couche N peut importer 1..N-1, pas N+1..8). Vérifié par `tests/architecture/test_layer_dependencies.py`. | # | Couche | Rôle | |---|---|---| | 1 | `domain/` | Types purs (Pydantic + stdlib) | | 2 | `formats/` | Parsers ALTO, PAGE XML, normalisation texte | | 3 | `evaluation/` | Métriques, statistiques, vues d'évaluation | | 4 | `pipeline/` | DAG d'étapes, cache, runner corpus-wide | | 5 | `adapters/` | OCR, LLM, VLM, corpus importers, storage | | 6 | `app/` | Services applicatifs (orchestration) | | 7 | `reports/` | Rendu HTML / JSON / CSV | | 8 | `interfaces/` | CLI Click + Web FastAPI | ### `picarones/domain/` — types purs Couche 1 (la plus interne). Aucune dépendance d'exécution, aucun I/O, aucun framework. Pydantic et stdlib uniquement. | Module | Contenu | |---|---| | `artifacts.py` | `Artifact`, `ArtifactType` (10 types : IMAGE, RAW_TEXT, ALTO_XML, PAGE_XML, ENTITIES, READING_ORDER, ALIGNMENT, CORRECTED_TEXT, CANONICAL_DOCUMENT, CONFIDENCES) | | `artifact_key.py` | `ArtifactKey` — clé canonique multi-paramètres pour la reprise par hash | | `corpus.py` | `CorpusSpec`, métadonnées de corpus | | `documents.py` | `DocumentRef`, `GroundTruthRef` | | `evaluation_spec.py` | `MetricSpec`, `EvaluationView`, `EvaluationSpec` | | `pipeline_spec.py` | `PipelineSpec`, `PipelineStep`, `INITIAL_STEP_ID` | | `projection_spec.py` | `ProjectionSpec` (transformation candidate avant évaluation) | | `provenance.py` | `ProvenanceRecord` | | `run_manifest.py` | `RunManifest` — empreinte immuable d'un run, sérialisée en `run_manifest.json` | | `errors.py` | Hiérarchie d'exceptions (`PicaronesError`, `AdapterStepError`, `ArtifactValidationError`, …) | ### `picarones/formats/` — parsers et sérialiseurs Lecture/écriture des formats externes : ALTO XML, PAGE XML, texte normalisé. Dépend du domain ; aucune logique d'évaluation. Le parser XML interne (`_xml_utils.safe_parse_xml`) délègue à `defusedxml` avec `forbid_dtd=True`, bloquant XXE, Billion Laughs et déclarations ``. Les défenses sont verrouillées par `tests/security/test_xxe_attack.py`. ### `picarones/evaluation/` — moteurs d'évaluation | Sous-package | Rôle | |---|---| | `metrics/` | ~37 métriques (CER/WER, philologiques, calibration, NER, layout…). Enregistrées via `@register_metric` au registre typé | | `projectors/` | Projections inter-types (ALTO → texte, canonical → texte) avec `ProjectionReport` | | `views/` | Vues d'évaluation : `TextView`, `AltoView`, `SearchView`. L'`EvaluationViewExecutor` aligne candidate + GT, applique normalisation + projection, calcule les métriques | | `evaluation_engine.py` | Moteur central qui exécute une `EvaluationView` | | `projection_engine.py` | Moteur de projection | | `registry/` | `MetricRegistry` — découverte typée par signature `(input_type, output_type)` | | `statistics/` | Wilcoxon, Friedman/Nemenyi, bootstrap, Pareto, CDD | | `synthetic.py` | `generate_sample_benchmark` (utilisé par `picarones demo`) | **Whitelist d'imports externes** : `PIL, annotated_types, jiwer, numpy, pydantic, rapidfuzz, scipy, spacy, typing_extensions, yaml`. **Pas** `pytesseract, mistralai, azure, google, pero_ocr` — ceux-là vivent en couche 5 (`adapters/`). ### `picarones/pipeline/` — DAG d'étapes Orchestration mono-document d'une pipeline composée : | Module | Rôle | |---|---| | `executor.py` | `PipelineExecutor` — exécute un `PipelineSpec` step par step, capture `StepResult`, filtre outputs sur `step.output_types` | | `planner.py` | `PipelinePlanner` — résout les `inputs_from`, valide la spec, calcule les métriques aux jonctions | | `validation.py` | Validation statique d'une `PipelineSpec` (types s'enchaînent, pas de cycle) | | `runner.py` | `CorpusRunner` — orchestration corpus-wide (ThreadPool unique), backpressure, timeout, cancellation | | `cache.py`, `cache_helpers.py`, `cache_protocol.py` | Reprise par hash via `ArtifactCachePort` | | `yaml_io.py` | Sérialisation YAML déterministe d'une `PipelineSpec` | | `llm_pipeline_builder.py` | `make_ocr_llm_pipeline_spec` (3 modes : text_only, text_and_image, zero_shot) | | `llm_pipeline_config.py` | `OCRLLMPipelineConfig` (container OCR+LLM) | ### `picarones/adapters/` — implémentations concrètes C'est ici que vivent les **dépendances externes** (pytesseract, pero, mistralai, openai, anthropic, google-cloud-vision, …). | Sous-package | Adapters | |---|---| | `ocr/` | TesseractAdapter, PeroOCRAdapter, MistralOCRAdapter, GoogleVisionAdapter, AzureDocIntelAdapter, PrecomputedTextAdapter + factory `ocr_adapter_from_name` | | `llm/` | AnthropicLLMAdapter, OpenAILLMAdapter, MistralLLMAdapter, OllamaLLMAdapter | | `vlm/` | AnthropicVLMAdapter, OpenAIVLMAdapter, MistralVLMAdapter, OllamaVLMAdapter (héritage multiple `BaseVLMAdapter + BaseLLMAdapter`, MRO guard) | | `corpus/` | local folder, IIIF, Gallica, HTR-United, HuggingFace Datasets, eScriptorium | | `storage/` | `InMemoryArtifactStore`, `FilesystemArtifactStore`, `JobStore` (SQLite avec schema versioning) | | `output_paths.py` | Helper partagé `resolve_output_path` (workspace-aware, read-only-mount-safe) | | `_retry.py` | Helper partagé `call_with_retry` (3 retries, backoff 2/4/8s, sur 429+5xx+timeout réseau) | **Règle** : un adapter peut importer le domain et ses libs externes. Il ne doit **jamais** importer `app/` ou `interfaces/`. Il n'a aucune logique d'évaluation (un OCR adapter ne calcule pas le CER — il produit un artefact texte que `evaluation/` consommera). **Anti-SSRF** : `corpus/_http.py:validate_http_url` refuse loopback, lien-local, RFC 1918, métadonnées cloud (AWS `169.254.169.254`, GCP `metadata.google.internal`). Verrouillé par `tests/security/test_ssrf_attack.py`. ### `picarones/app/` — services applicatifs Orchestration entre adapters et evaluation. | Module | Rôle | |---|---| | `services/run_orchestrator.py` | `RunOrchestrator.execute(RunSpec)` — point d'entrée d'un run complet | | `services/benchmark_service.py` | `BenchmarkService.run` — exécute pipelines × vues × corpus, produit `RunResult` | | `services/benchmark_runner.py` | Façade `run_benchmark_via_service` consommée par CLI/web | | `services/job_runner.py` | `JobRunner` — soumission asynchrone (thread daemon) avec persistance `JobStore` | | `services/corpus_service.py` | Loading + sandboxing + extraction ZIP avec ZIP slip protection | | `services/dependencies.py` | `capture_dependencies_lock()` via `importlib.metadata` pour le `RunManifest` | | `services/path_security.py` | `WorkspaceManager` — sandboxe par session | | `services/registry_service.py` | Découverte des adapters et vues canoniques | | `services/partial_store.py` | Persistance NDJSON des résultats partiels (reprise sur interruption) | | `schemas/run_spec.py` | `RunSpec`, `StepSpec` — modèles YAML user-facing | | `results.py` | `RunResult`, `RunDocumentResult`, `ReportRenderer` (alias type unique) | **Anti ZIP slip** : `corpus_service._extract_safely` rejette les chemins absolus, `..`, octets nuls, symlinks ZIP entries (mode UNIX 0xA000), avec garde-fou final `target.resolve().relative_to(extract_dir)`. Verrouillé par `tests/security/test_zip_slip_attack.py`. ### `picarones/reports/` — rendu déterministe | Sous-package | Rôle | |---|---| | `csv/render.py` | `CsvReportRenderer` — un CSV plat (`run_id, doc, pipeline, view, metric, value, status`) | | `json/render.py` | `JsonReportRenderer` — manifest + documents en JSON déterministe | | `html/render.py` | `HtmlReportRenderer` — rapport autonome (TextView, AltoView, SearchView) — minimaliste | | `html/generator.py` | `ReportGenerator` — rapport interactif riche (~37 renderers + 4 onglets XerOCR + 5 modules de vues thématiques internes) consommé par CLI/web | | `narrative/` | Moteur narratif (20 détecteurs) — synthèse factuelle déterministe | | `glossary/`, `i18n/` | Glossaire + i18n FR/EN | Le rendu est strict : pas de JS dynamique côté serveur, pas d'I/O hors écriture finale, déterministe bit-for-bit à entrée constante. Permet à un relecteur 5 ans plus tard de hasher un rapport et de le citer. **Anti-XSS** : `html/generator.py` utilise `autoescape=select_autoescape(['html', 'j2', 'xml'])` (Jinja2) + helper `_safe_json_for_script_tag` qui encode `<>&` en `<>&` pour le JSON injecté dans `