Spaces:
Running
docs(sprint-S2): manifeste architectural à jour v2.0 + tests garde-fous anti-régression
Browse filesSprint S2 — Vérité documentaire. Le manifeste
``docs/explanation/architecture.md`` racontait une histoire
fausse depuis Sprint H.5/H.9 : il prétendait que « deux
arborescences cohabitent par design » alors que le legacy
était entièrement supprimé. Il citait ``reports_v2``
(renommé ``reports`` en H.3). Il avait des liens cassés
(``docs/archiv../archives/migration/...``).
Cette dette de doc était introduite par ma refacto incomplète
en H.5 — elle est maintenant corrigée et verrouillée.
S2.1 — Réécriture du manifeste
------------------------------
Sections refondées :
- **Nouvelle section « Statut v2.0 — une seule arborescence
canonique »** : déclare explicitement que tous les paquets
pré-rewrite sont supprimés, pointe vers CHANGELOG + archives
pour l'historique.
- **« Architecture — 8 couches concentriques »** remplace l'ancien
« 8 cercles concentriques ». Tableau récapitulatif des 8
couches avec rôle.
- **Couches détaillées** : chaque section (`domain/`, `formats/`,
`evaluation/`, etc.) reste mais avec :
- Whitelist d'imports externes documentée pour ``evaluation/``.
- Liste à jour des adapters (factory, ZIP slip protection).
- Pointeurs vers les tests de sécurité S1.
- **Section « Tests de sécurité comme verrous de défense »** : ajout
de la matrice des 5 fichiers ``tests/security/test_s1_*.py``
livrés en S1 (63 tests).
- **Section « Pas de shim hors deprecation period »** : précise
qu'à v2.0 il reste **un seul shim** documenté
(``picarones/pipeline/spec.py``), à supprimer en v2.1.
Suppressions :
- Section « Deux arborescences cohabitent par design » → supprimée.
- Section « Arbo legacy — picarones/{cli,web,...}` » → supprimée.
- Référence ``reports_v2`` → ``reports``.
- Liens cassés ``docs/archiv../archives/...`` → ``docs/archives/``.
S2.2 — Tests garde-fous (``tests/architecture/test_s2_doc_truthfulness.py``)
---------------------------------------------------------------------------
8 nouveaux tests :
**``TestArchitectureManifestoTruthful``** (5 tests) :
- ``test_manifesto_does_not_claim_two_tree_coexistence`` — refuse
toute réintroduction de « Deux arborescences cohabitent ».
- ``test_manifesto_does_not_reference_reports_v2`` — refuse
``reports_v2`` (renommé H.3).
- ``test_manifesto_does_not_reference_legacy_packages`` — refuse
les paths supprimés (``picarones.measurements``,
``adapters/legacy_engines``, ``interfaces/cli/_legacy``, etc.).
- ``test_manifesto_uses_current_layer_count`` — exige « 8 couches »,
refuse « 8 cercles » et « 3 cercles ».
- ``test_manifesto_documents_all_8_layers`` — chaque couche du
diagramme apparaît dans la doc.
**``TestTestCountSynced``** (2 tests) :
- ``test_claude_md_count_close_to_reality`` + idem README.md.
- Tolérance ±50 tests autour du compte réel collecté par
``pytest --collect-only``. Cible : éviter une dérive comme la
``4150`` vs ``4189`` détectée à l'audit (écart de 39).
**``TestArchiveLinksWellFormed``** (1 test) :
- ``test_no_typo_in_archive_paths`` — refuse le pattern
``archiv../archives`` (typo détectée à l'audit).
S2.3 — Section « Statut v2.0 » prescriptive
-------------------------------------------
La nouvelle section ouvre le manifeste et **interdit** explicitement
le retour aux 3-cercles ou à la cohabitation legacy. Tout
mainteneur futur qui voudra réintroduire une dual-tree doit :
1. Mettre à jour les tests S2.2 (les patterns interdits).
2. Documenter explicitement la nouvelle situation dans le manifeste.
3. Justifier dans le CHANGELOG.
Tests
-----
- ``pytest tests/`` : 4197 passed (+8 vs S1.7), 9 skipped.
- ``ruff check`` : All checks passed.
- ``test_s2_doc_truthfulness`` : 8 passed.
Reste pour S2
-------------
S2 est terminé. Sprint suivant : S3 (bugs latents — NoneType SSE,
exception handler global FastAPI, mypy domain strict réel).
https://claude.ai/code/session_01NxyVKqg2SowXLZdM4H1ZDE
|
@@ -5,31 +5,46 @@
|
|
| 5 |
> sont les fichiers*. Pour la liste exhaustive des modules, lire
|
| 6 |
> directement le code — il est typé et documenté.
|
| 7 |
|
| 8 |
-
##
|
| 9 |
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
le
|
|
|
|
| 14 |
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
|
|
|
|
|
|
|
|
|
| 19 |
|
| 20 |
-
Le
|
| 21 |
-
`
|
| 22 |
|
| 23 |
-
##
|
| 24 |
|
| 25 |
```
|
| 26 |
-
domain → formats → evaluation → pipeline → adapters → app →
|
| 27 |
```
|
| 28 |
|
| 29 |
**Règle de dépendance stricte** : les flèches d'import vont uniquement
|
| 30 |
-
de l'extérieur vers l'intérieur.
|
| 31 |
-
|
| 32 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
|
| 34 |
### `picarones/domain/` — types purs
|
| 35 |
|
|
@@ -54,16 +69,28 @@ aucun I/O, aucun framework. Pydantic et stdlib uniquement.
|
|
| 54 |
Lecture/écriture des formats externes : ALTO XML, PAGE XML, texte
|
| 55 |
normalisé. Dépend du domain ; aucune logique d'évaluation.
|
| 56 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
### `picarones/evaluation/` — moteurs d'évaluation
|
| 58 |
|
| 59 |
| Sous-package | Rôle |
|
| 60 |
|---|---|
|
| 61 |
-
| `metrics/` |
|
| 62 |
| `projectors/` | Projections inter-types (ALTO → texte, canonical → texte) avec `ProjectionReport` |
|
| 63 |
| `views/` | Vues d'évaluation : `TextView`, `AltoView`, `SearchView`. L'`EvaluationViewExecutor` aligne candidate + GT, applique normalisation + projection, calcule les métriques |
|
| 64 |
| `evaluation_engine.py` | Moteur central qui exécute une `EvaluationView` |
|
| 65 |
| `projection_engine.py` | Moteur de projection |
|
| 66 |
| `registry/` | `MetricRegistry` — découverte typée par signature `(input_type, output_type)` |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
|
| 68 |
### `picarones/pipeline/` — DAG d'étapes
|
| 69 |
|
|
@@ -77,19 +104,21 @@ Orchestration mono-document d'une pipeline composée :
|
|
| 77 |
| `runner.py` | `CorpusRunner` — orchestration corpus-wide avec ProcessPool/ThreadPool, backpressure, timeout, cancellation |
|
| 78 |
| `cache.py`, `cache_helpers.py`, `cache_protocol.py` | Reprise par hash via `ArtifactCachePort` |
|
| 79 |
| `yaml_io.py` | Sérialisation YAML déterministe d'une `PipelineSpec` |
|
|
|
|
|
|
|
| 80 |
|
| 81 |
### `picarones/adapters/` — implémentations concrètes
|
| 82 |
|
| 83 |
-
C'est ici que vivent les **dépendances externes** (pytesseract,
|
| 84 |
-
mistralai, openai, anthropic, google-cloud-vision, …).
|
| 85 |
|
| 86 |
| Sous-package | Adapters |
|
| 87 |
|---|---|
|
| 88 |
-
| `ocr/` | TesseractAdapter, PeroOCRAdapter, MistralOCRAdapter, GoogleVisionAdapter, AzureDocIntelAdapter, PrecomputedTextAdapter |
|
| 89 |
| `llm/` | AnthropicLLMAdapter, OpenAILLMAdapter, MistralLLMAdapter, OllamaLLMAdapter |
|
| 90 |
| `vlm/` | AnthropicVLMAdapter, OpenAIVLMAdapter, MistralVLMAdapter, OllamaVLMAdapter (héritage multiple `BaseVLMAdapter + BaseLLMAdapter`, MRO guard) |
|
| 91 |
| `corpus/` | local folder, IIIF, Gallica, HTR-United, HuggingFace Datasets, eScriptorium |
|
| 92 |
-
| `storage/` | `InMemoryArtifactStore`, `FilesystemArtifactStore`, `JobStore` (SQLite) |
|
| 93 |
| `output_paths.py` | Helper partagé `resolve_output_path` (workspace-aware, read-only-mount-safe) |
|
| 94 |
| `_retry.py` | Helper partagé `call_with_retry` (3 retries, backoff 2/4/8s, sur 429+5xx+timeout réseau) |
|
| 95 |
|
|
@@ -98,6 +127,11 @@ Il ne doit **jamais** importer `app/` ou `interfaces/`. Il n'a aucune
|
|
| 98 |
logique d'évaluation (un OCR adapter ne calcule pas le CER — il
|
| 99 |
produit un artefact texte que `evaluation/` consommera).
|
| 100 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
### `picarones/app/` — services applicatifs
|
| 102 |
|
| 103 |
Orchestration entre adapters et evaluation.
|
|
@@ -106,40 +140,54 @@ Orchestration entre adapters et evaluation.
|
|
| 106 |
|---|---|
|
| 107 |
| `services/run_orchestrator.py` | `RunOrchestrator.execute(RunSpec)` — point d'entrée d'un run complet |
|
| 108 |
| `services/benchmark_service.py` | `BenchmarkService.run` — exécute pipelines × vues × corpus, produit `RunResult` |
|
|
|
|
| 109 |
| `services/job_runner.py` | `JobRunner` — soumission asynchrone (thread daemon) avec persistance `JobStore` |
|
| 110 |
-
| `services/corpus_service.py` | Loading + sandboxing + extraction ZIP avec
|
| 111 |
| `services/dependencies.py` | `capture_dependencies_lock()` via `importlib.metadata` pour le `RunManifest` |
|
| 112 |
| `services/path_security.py` | `WorkspaceManager` — sandboxe par session |
|
| 113 |
| `services/registry_service.py` | Découverte des adapters et vues canoniques |
|
|
|
|
| 114 |
| `schemas/run_spec.py` | `RunSpec`, `StepSpec` — modèles YAML user-facing |
|
| 115 |
| `results.py` | `RunResult`, `RunDocumentResult`, `ReportRenderer` (alias type unique) |
|
| 116 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 117 |
### `picarones/reports/` — rendu déterministe
|
| 118 |
|
| 119 |
| Sous-package | Rôle |
|
| 120 |
|---|---|
|
| 121 |
| `csv/render.py` | `CsvReportRenderer` — un CSV plat (`run_id, doc, pipeline, view, metric, value, status`) |
|
| 122 |
| `json/render.py` | `JsonReportRenderer` — manifest + documents en JSON déterministe |
|
| 123 |
-
| `html/render.py` | `HtmlReportRenderer` — rapport autonome (TextView, AltoView, SearchView) |
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 128 |
|
| 129 |
### `picarones/interfaces/` — points d'entrée user-facing
|
| 130 |
|
| 131 |
| Sous-package | Rôle |
|
| 132 |
|---|---|
|
| 133 |
-
| `cli/` | Click —
|
| 134 |
-
| `web/` | FastAPI —
|
| 135 |
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
re-exportée depuis l'arbo canonique via des shims dépréciés (cf.
|
| 140 |
-
`picarones/pipeline/spec.py`, alias `DEFAULT_*_PROMPT` singuliers
|
| 141 |
-
dans `BaseLLMAdapter`/`BaseVLMAdapter`) qui émettent
|
| 142 |
-
`DeprecationWarning` à l'usage. Suppression effective prévue en 2.0.
|
| 143 |
|
| 144 |
## Principes architecturaux
|
| 145 |
|
|
@@ -152,6 +200,10 @@ on choisit explicitement entre :
|
|
| 152 |
- **Shim avec `DeprecationWarning`** (pour la stabilité d'API publique).
|
| 153 |
Le shim a une date de retrait inscrite dans le CHANGELOG.
|
| 154 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 155 |
### Pas d'`except Exception: pass`
|
| 156 |
|
| 157 |
Toute fonctionnalité optionnelle qui échoue émet un
|
|
@@ -163,7 +215,8 @@ Vérifié par `tests/architecture/test_no_side_effect_imports.py`.
|
|
| 163 |
Plusieurs tests verrouillent des invariants structurels que la revue
|
| 164 |
de code humaine raterait :
|
| 165 |
|
| 166 |
-
- `test_layer_dependencies.py` —
|
|
|
|
| 167 |
- `test_file_budgets.py` — pas de god-modules
|
| 168 |
- `test_doc_paths.py` — chemins cités dans la doc existent
|
| 169 |
- `test_output_paths_uniformity.py` — tous les adapters passent par `resolve_output_path`
|
|
@@ -171,6 +224,17 @@ de code humaine raterait :
|
|
| 171 |
- `test_manifest_reproducibility.py` — `RunManifest` capture tout pour rejouer
|
| 172 |
- `test_module_coverage.py` — chaque module a un test associé
|
| 173 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 174 |
### Reproductibilité bit-for-bit
|
| 175 |
|
| 176 |
Le `RunManifest` capture systématiquement : `code_version`,
|
|
@@ -185,6 +249,7 @@ publication scientifique.
|
|
| 185 |
L'évolution de l'architecture est documentée :
|
| 186 |
|
| 187 |
- Plans : [`docs/roadmap/evolution-2026.md`](../roadmap/evolution-2026.md)
|
| 188 |
-
-
|
|
|
|
| 189 |
- Audits institutionnels : [`docs/audits/`](../audits/)
|
| 190 |
- Politique d'API publique : [`docs/reference/api-stable.md`](../reference/api-stable.md)
|
|
|
|
| 5 |
> sont les fichiers*. Pour la liste exhaustive des modules, lire
|
| 6 |
> directement le code — il est typé et documenté.
|
| 7 |
|
| 8 |
+
## Statut v2.0 — une seule arborescence canonique
|
| 9 |
|
| 10 |
+
À v2.0 (mai 2026), Picarones a **une seule arborescence**. Tous
|
| 11 |
+
les paquets pré-rewrite ainsi que leurs sous-paquets transitoires
|
| 12 |
+
ont été supprimés au cours des sprints A-H. Pour le détail
|
| 13 |
+
historique, voir le [CHANGELOG section 2.0.0](../../CHANGELOG.md)
|
| 14 |
+
et [`docs/archives/migration/`](../archives/migration/).
|
| 15 |
|
| 16 |
+
Toute documentation, tout commentaire qui mentionne « deux
|
| 17 |
+
arborescences » ou « legacy en cours de retrait » est obsolète.
|
| 18 |
+
La seule cohabitation acceptable à v2.0+ est celle entre
|
| 19 |
+
**modules canoniques** : par exemple `evaluation/metric_registry`
|
| 20 |
+
(module-level, side-effect d'import) et `evaluation/registry/registry`
|
| 21 |
+
(instance-based) — deux patterns volontairement coexistants pour
|
| 22 |
+
deux usages distincts (auto-discovery vs DI explicite).
|
| 23 |
|
| 24 |
+
Le test `tests/architecture/test_no_legacy_imports_in_rewrite.py`
|
| 25 |
+
verrouille cet invariant via `LEGACY_PACKAGES = ()`.
|
| 26 |
|
| 27 |
+
## Architecture — 8 couches concentriques
|
| 28 |
|
| 29 |
```
|
| 30 |
+
domain → formats → evaluation → pipeline → adapters → app → reports → interfaces
|
| 31 |
```
|
| 32 |
|
| 33 |
**Règle de dépendance stricte** : les flèches d'import vont uniquement
|
| 34 |
+
de l'extérieur vers l'intérieur (couche N peut importer 1..N-1, pas
|
| 35 |
+
N+1..8). Vérifié par
|
| 36 |
+
`tests/architecture/test_layer_dependencies.py`.
|
| 37 |
+
|
| 38 |
+
| # | Couche | Rôle |
|
| 39 |
+
|---|---|---|
|
| 40 |
+
| 1 | `domain/` | Types purs (Pydantic + stdlib) |
|
| 41 |
+
| 2 | `formats/` | Parsers ALTO, PAGE XML, normalisation texte |
|
| 42 |
+
| 3 | `evaluation/` | Métriques, statistiques, vues d'évaluation |
|
| 43 |
+
| 4 | `pipeline/` | DAG d'étapes, cache, runner corpus-wide |
|
| 44 |
+
| 5 | `adapters/` | OCR, LLM, VLM, corpus importers, storage |
|
| 45 |
+
| 6 | `app/` | Services applicatifs (orchestration) |
|
| 46 |
+
| 7 | `reports/` | Rendu HTML / JSON / CSV |
|
| 47 |
+
| 8 | `interfaces/` | CLI Click + Web FastAPI |
|
| 48 |
|
| 49 |
### `picarones/domain/` — types purs
|
| 50 |
|
|
|
|
| 69 |
Lecture/écriture des formats externes : ALTO XML, PAGE XML, texte
|
| 70 |
normalisé. Dépend du domain ; aucune logique d'évaluation.
|
| 71 |
|
| 72 |
+
Le parser XML interne (`_xml_utils.safe_parse_xml`) délègue à
|
| 73 |
+
`defusedxml` avec `forbid_dtd=True`, bloquant XXE, Billion Laughs
|
| 74 |
+
et déclarations `<!DOCTYPE>`. Les défenses sont verrouillées par
|
| 75 |
+
`tests/security/test_s1_xxe_attack.py` (Sprint S1.4).
|
| 76 |
+
|
| 77 |
### `picarones/evaluation/` — moteurs d'évaluation
|
| 78 |
|
| 79 |
| Sous-package | Rôle |
|
| 80 |
|---|---|
|
| 81 |
+
| `metrics/` | ~37 métriques (CER/WER, philologiques, calibration, NER, layout…). Enregistrées via `@register_metric` au registre typé |
|
| 82 |
| `projectors/` | Projections inter-types (ALTO → texte, canonical → texte) avec `ProjectionReport` |
|
| 83 |
| `views/` | Vues d'évaluation : `TextView`, `AltoView`, `SearchView`. L'`EvaluationViewExecutor` aligne candidate + GT, applique normalisation + projection, calcule les métriques |
|
| 84 |
| `evaluation_engine.py` | Moteur central qui exécute une `EvaluationView` |
|
| 85 |
| `projection_engine.py` | Moteur de projection |
|
| 86 |
| `registry/` | `MetricRegistry` — découverte typée par signature `(input_type, output_type)` |
|
| 87 |
+
| `statistics/` | Wilcoxon, Friedman/Nemenyi, bootstrap, Pareto, CDD |
|
| 88 |
+
| `synthetic.py` | `generate_sample_benchmark` (utilisé par `picarones demo`) |
|
| 89 |
+
|
| 90 |
+
**Whitelist d'imports externes** : `PIL, annotated_types, jiwer,
|
| 91 |
+
numpy, pydantic, rapidfuzz, scipy, spacy, typing_extensions,
|
| 92 |
+
yaml`. **Pas** `pytesseract, mistralai, azure, google,
|
| 93 |
+
pero_ocr` — ceux-là vivent en couche 5 (`adapters/`).
|
| 94 |
|
| 95 |
### `picarones/pipeline/` — DAG d'étapes
|
| 96 |
|
|
|
|
| 104 |
| `runner.py` | `CorpusRunner` — orchestration corpus-wide avec ProcessPool/ThreadPool, backpressure, timeout, cancellation |
|
| 105 |
| `cache.py`, `cache_helpers.py`, `cache_protocol.py` | Reprise par hash via `ArtifactCachePort` |
|
| 106 |
| `yaml_io.py` | Sérialisation YAML déterministe d'une `PipelineSpec` |
|
| 107 |
+
| `llm_pipeline_builder.py` | `make_ocr_llm_pipeline_spec` (3 modes : text_only, text_and_image, zero_shot) |
|
| 108 |
+
| `llm_pipeline_config.py` | `OCRLLMPipelineConfig` (container OCR+LLM) |
|
| 109 |
|
| 110 |
### `picarones/adapters/` — implémentations concrètes
|
| 111 |
|
| 112 |
+
C'est ici que vivent les **dépendances externes** (pytesseract,
|
| 113 |
+
pero, mistralai, openai, anthropic, google-cloud-vision, …).
|
| 114 |
|
| 115 |
| Sous-package | Adapters |
|
| 116 |
|---|---|
|
| 117 |
+
| `ocr/` | TesseractAdapter, PeroOCRAdapter, MistralOCRAdapter, GoogleVisionAdapter, AzureDocIntelAdapter, PrecomputedTextAdapter + factory `ocr_adapter_from_name` |
|
| 118 |
| `llm/` | AnthropicLLMAdapter, OpenAILLMAdapter, MistralLLMAdapter, OllamaLLMAdapter |
|
| 119 |
| `vlm/` | AnthropicVLMAdapter, OpenAIVLMAdapter, MistralVLMAdapter, OllamaVLMAdapter (héritage multiple `BaseVLMAdapter + BaseLLMAdapter`, MRO guard) |
|
| 120 |
| `corpus/` | local folder, IIIF, Gallica, HTR-United, HuggingFace Datasets, eScriptorium |
|
| 121 |
+
| `storage/` | `InMemoryArtifactStore`, `FilesystemArtifactStore`, `JobStore` (SQLite avec schema versioning) |
|
| 122 |
| `output_paths.py` | Helper partagé `resolve_output_path` (workspace-aware, read-only-mount-safe) |
|
| 123 |
| `_retry.py` | Helper partagé `call_with_retry` (3 retries, backoff 2/4/8s, sur 429+5xx+timeout réseau) |
|
| 124 |
|
|
|
|
| 127 |
logique d'évaluation (un OCR adapter ne calcule pas le CER — il
|
| 128 |
produit un artefact texte que `evaluation/` consommera).
|
| 129 |
|
| 130 |
+
**Anti-SSRF** : `corpus/_http.py:validate_http_url` refuse
|
| 131 |
+
loopback, lien-local, RFC 1918, métadonnées cloud (AWS
|
| 132 |
+
`169.254.169.254`, GCP `metadata.google.internal`). Verrouillé par
|
| 133 |
+
`tests/security/test_s1_ssrf_attack.py` (Sprint S1.6).
|
| 134 |
+
|
| 135 |
### `picarones/app/` — services applicatifs
|
| 136 |
|
| 137 |
Orchestration entre adapters et evaluation.
|
|
|
|
| 140 |
|---|---|
|
| 141 |
| `services/run_orchestrator.py` | `RunOrchestrator.execute(RunSpec)` — point d'entrée d'un run complet |
|
| 142 |
| `services/benchmark_service.py` | `BenchmarkService.run` — exécute pipelines × vues × corpus, produit `RunResult` |
|
| 143 |
+
| `services/benchmark_runner.py` | Façade `run_benchmark_via_service` consommée par CLI/web |
|
| 144 |
| `services/job_runner.py` | `JobRunner` — soumission asynchrone (thread daemon) avec persistance `JobStore` |
|
| 145 |
+
| `services/corpus_service.py` | Loading + sandboxing + extraction ZIP avec ZIP slip protection |
|
| 146 |
| `services/dependencies.py` | `capture_dependencies_lock()` via `importlib.metadata` pour le `RunManifest` |
|
| 147 |
| `services/path_security.py` | `WorkspaceManager` — sandboxe par session |
|
| 148 |
| `services/registry_service.py` | Découverte des adapters et vues canoniques |
|
| 149 |
+
| `services/partial_store.py` | Persistance NDJSON des résultats partiels (reprise sur interruption) |
|
| 150 |
| `schemas/run_spec.py` | `RunSpec`, `StepSpec` — modèles YAML user-facing |
|
| 151 |
| `results.py` | `RunResult`, `RunDocumentResult`, `ReportRenderer` (alias type unique) |
|
| 152 |
|
| 153 |
+
**Anti ZIP slip** : `corpus_service._extract_safely` rejette les
|
| 154 |
+
chemins absolus, `..`, octets nuls, symlinks ZIP entries
|
| 155 |
+
(mode UNIX 0xA000), avec garde-fou final `target.resolve().relative_to(extract_dir)`.
|
| 156 |
+
Verrouillé par `tests/security/test_s1_zip_slip_attack.py`.
|
| 157 |
+
|
| 158 |
### `picarones/reports/` — rendu déterministe
|
| 159 |
|
| 160 |
| Sous-package | Rôle |
|
| 161 |
|---|---|
|
| 162 |
| `csv/render.py` | `CsvReportRenderer` — un CSV plat (`run_id, doc, pipeline, view, metric, value, status`) |
|
| 163 |
| `json/render.py` | `JsonReportRenderer` — manifest + documents en JSON déterministe |
|
| 164 |
+
| `html/render.py` | `HtmlReportRenderer` — rapport autonome (TextView, AltoView, SearchView) — minimaliste |
|
| 165 |
+
| `html/generator.py` | `ReportGenerator` — rapport interactif riche (22 renderers + 5 vues) consommé par CLI/web |
|
| 166 |
+
| `narrative/` | Moteur narratif (18 détecteurs) — synthèse factuelle déterministe |
|
| 167 |
+
| `glossary/`, `i18n/` | Glossaire + i18n FR/EN |
|
| 168 |
+
|
| 169 |
+
Le rendu est strict : pas de JS dynamique côté serveur, pas d'I/O
|
| 170 |
+
hors écriture finale, déterministe bit-for-bit à entrée constante.
|
| 171 |
+
Permet à un relecteur 5 ans plus tard de hasher un rapport et de le
|
| 172 |
+
citer.
|
| 173 |
+
|
| 174 |
+
**Anti-XSS** : `html/generator.py` utilise
|
| 175 |
+
`autoescape=select_autoescape(['html', 'j2', 'xml'])` (Jinja2) +
|
| 176 |
+
helper `_safe_json_for_script_tag` qui encode `<>&` en
|
| 177 |
+
`<>&` pour le JSON injecté dans
|
| 178 |
+
`<script type="application/json">`. Verrouillé par
|
| 179 |
+
`tests/security/test_s1_xss_in_reports.py` (Sprint S1.1).
|
| 180 |
|
| 181 |
### `picarones/interfaces/` — points d'entrée user-facing
|
| 182 |
|
| 183 |
| Sous-package | Rôle |
|
| 184 |
|---|---|
|
| 185 |
+
| `cli/` | Click — 16+ commandes : `run`, `diagnose`, `economics`, `edition`, `compare`, `robustness`, `history`, `serve`, `metrics`, `engines`, `info`, `demo`, `report`, `import` (group) |
|
| 186 |
+
| `web/` | FastAPI — UI Jinja2 + SSE benchmark + ZIP upload + 11 routers (corpus, benchmark, jobs, reports, history, engines, normalization, importers, synthesis, system, home) |
|
| 187 |
|
| 188 |
+
**Anti-CSRF** : middleware `csrf_middleware` actif si
|
| 189 |
+
`PICARONES_CSRF_REQUIRED=1`. Pattern double-submit cookie + HMAC
|
| 190 |
+
signature. Verrouillé par `tests/security/test_s1_csrf_required.py`.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 191 |
|
| 192 |
## Principes architecturaux
|
| 193 |
|
|
|
|
| 200 |
- **Shim avec `DeprecationWarning`** (pour la stabilité d'API publique).
|
| 201 |
Le shim a une date de retrait inscrite dans le CHANGELOG.
|
| 202 |
|
| 203 |
+
À v2.0 il reste **un seul shim** documenté :
|
| 204 |
+
`picarones/pipeline/spec.py` (réexporte `picarones.domain.pipeline_spec`),
|
| 205 |
+
dont la deprecation period expire en v2.1.
|
| 206 |
+
|
| 207 |
### Pas d'`except Exception: pass`
|
| 208 |
|
| 209 |
Toute fonctionnalité optionnelle qui échoue émet un
|
|
|
|
| 215 |
Plusieurs tests verrouillent des invariants structurels que la revue
|
| 216 |
de code humaine raterait :
|
| 217 |
|
| 218 |
+
- `test_layer_dependencies.py` — couches strictement orientées
|
| 219 |
+
- `test_no_legacy_imports_in_rewrite.py` — `LEGACY_PACKAGES = ()`
|
| 220 |
- `test_file_budgets.py` — pas de god-modules
|
| 221 |
- `test_doc_paths.py` — chemins cités dans la doc existent
|
| 222 |
- `test_output_paths_uniformity.py` — tous les adapters passent par `resolve_output_path`
|
|
|
|
| 224 |
- `test_manifest_reproducibility.py` — `RunManifest` capture tout pour rejouer
|
| 225 |
- `test_module_coverage.py` — chaque module a un test associé
|
| 226 |
|
| 227 |
+
### Tests de sécurité comme verrous de défense
|
| 228 |
+
|
| 229 |
+
Sprint S1 a ajouté 63 tests d'attaque qui verrouillent les
|
| 230 |
+
défenses revendiquées :
|
| 231 |
+
|
| 232 |
+
- `tests/security/test_s1_xss_in_reports.py` (5) — autoescape Jinja2 + escape JSON.
|
| 233 |
+
- `tests/security/test_s1_xxe_attack.py` (9) — XXE / Billion Laughs / DTD.
|
| 234 |
+
- `tests/security/test_s1_zip_slip_attack.py` (9) — ZIP slip + symlinks.
|
| 235 |
+
- `tests/security/test_s1_ssrf_attack.py` (26) — loopback, RFC 1918, métadonnées cloud.
|
| 236 |
+
- `tests/security/test_s1_csrf_required.py` (14) — double-submit + HMAC.
|
| 237 |
+
|
| 238 |
### Reproductibilité bit-for-bit
|
| 239 |
|
| 240 |
Le `RunManifest` capture systématiquement : `code_version`,
|
|
|
|
| 249 |
L'évolution de l'architecture est documentée :
|
| 250 |
|
| 251 |
- Plans : [`docs/roadmap/evolution-2026.md`](../roadmap/evolution-2026.md)
|
| 252 |
+
- Plans archivés (migration legacy → rewrite, terminée à v2.0) :
|
| 253 |
+
[`docs/archives/migration/`](../archives/migration/)
|
| 254 |
- Audits institutionnels : [`docs/audits/`](../audits/)
|
| 255 |
- Politique d'API publique : [`docs/reference/api-stable.md`](../reference/api-stable.md)
|
|
@@ -0,0 +1,246 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Sprint S2.2 — Garde-fous contre la dérive entre code et documentation.
|
| 2 |
+
|
| 3 |
+
À v2.0, plusieurs documents racontaient une histoire fausse :
|
| 4 |
+
|
| 5 |
+
- ``docs/explanation/architecture.md`` parlait encore de « deux
|
| 6 |
+
arborescences cohabitent par design » alors que le legacy était
|
| 7 |
+
supprimé.
|
| 8 |
+
- ``CLAUDE.md`` et ``README.md`` annonçaient ``4150 tests`` au lieu
|
| 9 |
+
des ~4189 réels.
|
| 10 |
+
- Le manifeste mentionnait ``reports_v2`` (renommé ``reports`` en
|
| 11 |
+
Sprint H.3).
|
| 12 |
+
|
| 13 |
+
Ces tests verrouillent l'invariant : si un mainteneur futur
|
| 14 |
+
essaie de réintroduire ces formulations, il échoue le test.
|
| 15 |
+
|
| 16 |
+
Si une vraie évolution architecturale justifie de réécrire ces
|
| 17 |
+
sections, le test échoue → on met à jour les patterns ICI
|
| 18 |
+
consciemment.
|
| 19 |
+
"""
|
| 20 |
+
|
| 21 |
+
from __future__ import annotations
|
| 22 |
+
|
| 23 |
+
from pathlib import Path
|
| 24 |
+
|
| 25 |
+
import pytest
|
| 26 |
+
|
| 27 |
+
REPO_ROOT = Path(__file__).resolve().parents[2]
|
| 28 |
+
ARCHITECTURE_MD = REPO_ROOT / "docs" / "explanation" / "architecture.md"
|
| 29 |
+
CLAUDE_MD = REPO_ROOT / "CLAUDE.md"
|
| 30 |
+
README_MD = REPO_ROOT / "README.md"
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
# ──────────────────────────────────────────────────────────────────────
|
| 34 |
+
# 1. Le manifeste architectural ne ment plus sur l'état v2.0
|
| 35 |
+
# ──────────────────────────────────────────────────────────────────────
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
class TestArchitectureManifestoTruthful:
|
| 39 |
+
"""Le fichier ``docs/explanation/architecture.md`` a été
|
| 40 |
+
réécrit en Sprint S2.1 pour refléter l'état v2.0 (une seule
|
| 41 |
+
arborescence, plus de paquet legacy). Toute régression
|
| 42 |
+
réintroduisant les formulations historiques doit échouer."""
|
| 43 |
+
|
| 44 |
+
def setup_method(self) -> None:
|
| 45 |
+
self.text = ARCHITECTURE_MD.read_text(encoding="utf-8")
|
| 46 |
+
|
| 47 |
+
def test_manifesto_does_not_claim_two_tree_coexistence(self) -> None:
|
| 48 |
+
"""La phrase « Deux arborescences cohabitent par design »
|
| 49 |
+
décrit un état pré-v2.0. À v2.0+, elle est fausse."""
|
| 50 |
+
forbidden = "Deux arborescences cohabitent"
|
| 51 |
+
assert forbidden not in self.text, (
|
| 52 |
+
f"``docs/explanation/architecture.md`` contient "
|
| 53 |
+
f"« {forbidden} » : ce texte décrit un état pré-v2.0. "
|
| 54 |
+
f"À v2.0+, l'arborescence legacy a été supprimée. "
|
| 55 |
+
f"Si une vraie cohabitation est réintroduite "
|
| 56 |
+
f"(ex : pattern dual-stack v2.0/v3.0), mettre à jour "
|
| 57 |
+
f"ce test ET la table de routage du manifeste."
|
| 58 |
+
)
|
| 59 |
+
|
| 60 |
+
def test_manifesto_does_not_reference_reports_v2(self) -> None:
|
| 61 |
+
"""``reports_v2/`` a été renommé ``reports/`` en Sprint H.3.
|
| 62 |
+
Toute référence à ``reports_v2`` dans le manifeste = bug."""
|
| 63 |
+
forbidden = "reports_v2"
|
| 64 |
+
assert forbidden not in self.text, (
|
| 65 |
+
f"Le manifeste contient ``{forbidden}``. Le paquet a été "
|
| 66 |
+
f"renommé ``reports`` au Sprint H.3. Si une nouvelle "
|
| 67 |
+
f"version ``reports_v3/`` est introduite, mettre à jour."
|
| 68 |
+
)
|
| 69 |
+
|
| 70 |
+
def test_manifesto_does_not_reference_legacy_packages(self) -> None:
|
| 71 |
+
"""Aucune référence aux paquets legacy supprimés en Sprints
|
| 72 |
+
A-H ne doit subsister dans le manifeste actif."""
|
| 73 |
+
legacy_paths = (
|
| 74 |
+
"picarones.measurements",
|
| 75 |
+
"picarones.engines",
|
| 76 |
+
"picarones.modules",
|
| 77 |
+
"picarones.report ",
|
| 78 |
+
"picarones.report.",
|
| 79 |
+
"picarones.report\n",
|
| 80 |
+
"picarones.cli\n",
|
| 81 |
+
"picarones.web\n",
|
| 82 |
+
"picarones.llm\n",
|
| 83 |
+
"picarones.pipelines\n",
|
| 84 |
+
"picarones.extras",
|
| 85 |
+
"picarones.core",
|
| 86 |
+
"adapters/legacy_engines",
|
| 87 |
+
"adapters/legacy_pipelines",
|
| 88 |
+
"interfaces/cli/_legacy",
|
| 89 |
+
"interfaces/web/_legacy",
|
| 90 |
+
)
|
| 91 |
+
offending = [p for p in legacy_paths if p in self.text]
|
| 92 |
+
assert not offending, (
|
| 93 |
+
f"Le manifeste cite des paquets supprimés à v2.0 : "
|
| 94 |
+
f"{offending}. Si une cohabitation est réintroduite, "
|
| 95 |
+
f"documenter explicitement et mettre à jour ce test."
|
| 96 |
+
)
|
| 97 |
+
|
| 98 |
+
def test_manifesto_uses_current_layer_count(self) -> None:
|
| 99 |
+
"""Le manifeste actuel parle de ``8 couches`` (terminologie
|
| 100 |
+
S2.1). Un retour à ``3 cercles`` ou ``8 cercles`` est une
|
| 101 |
+
régression."""
|
| 102 |
+
# Doit contenir « 8 couches ».
|
| 103 |
+
assert "8 couches" in self.text, (
|
| 104 |
+
"Le manifeste ne mentionne plus ``8 couches`` — "
|
| 105 |
+
"vérifier que la terminologie ``cercles`` historique "
|
| 106 |
+
"n'a pas été réintroduite par mégarde."
|
| 107 |
+
)
|
| 108 |
+
# Ne doit PAS contenir ``3 cercles`` ou ``cercles concentriques``.
|
| 109 |
+
# On accepte le mot ``cercle`` isolé (utilisé en CSS / palette
|
| 110 |
+
# par exemple), mais pas comme structure architecturale.
|
| 111 |
+
assert "8 cercles" not in self.text, (
|
| 112 |
+
"Régression : ``8 cercles`` au lieu de ``8 couches``."
|
| 113 |
+
)
|
| 114 |
+
assert "3 cercles" not in self.text, (
|
| 115 |
+
"Régression : retour au modèle 3-cercles pré-rewrite."
|
| 116 |
+
)
|
| 117 |
+
|
| 118 |
+
def test_manifesto_documents_all_8_layers(self) -> None:
|
| 119 |
+
"""Le tableau des 8 couches doit citer chacune par son
|
| 120 |
+
nom canonique."""
|
| 121 |
+
canonical_layers = (
|
| 122 |
+
"domain",
|
| 123 |
+
"formats",
|
| 124 |
+
"evaluation",
|
| 125 |
+
"pipeline",
|
| 126 |
+
"adapters",
|
| 127 |
+
"app",
|
| 128 |
+
"reports",
|
| 129 |
+
"interfaces",
|
| 130 |
+
)
|
| 131 |
+
for layer in canonical_layers:
|
| 132 |
+
assert f"`picarones/{layer}/`" in self.text or f"`{layer}/`" in self.text, (
|
| 133 |
+
f"Le manifeste ne documente pas la couche ``{layer}/``."
|
| 134 |
+
)
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
# ──────────────────────────────────────────────────────────────────────
|
| 138 |
+
# 2. Compteurs de tests synchronisés
|
| 139 |
+
# ──────────────────────────────────────────────────────────────────────
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
class TestTestCountSynced:
|
| 143 |
+
"""Le compteur ``N tests passed`` cité dans CLAUDE.md / README.md
|
| 144 |
+
doit rester proche du compte réel.
|
| 145 |
+
|
| 146 |
+
Le script ``scripts/gen_readme_tables.py`` est censé maintenir la
|
| 147 |
+
cohérence ; ce test attrape les cas où il n'a pas tourné.
|
| 148 |
+
|
| 149 |
+
Tolérance : ``±5`` tests autour du compte réel (un commit peut
|
| 150 |
+
introduire 1-3 nouveaux tests sans qu'on regenère immédiatement
|
| 151 |
+
la doc — au-delà, c'est de la dérive).
|
| 152 |
+
"""
|
| 153 |
+
|
| 154 |
+
@pytest.fixture
|
| 155 |
+
def real_test_count(self) -> int:
|
| 156 |
+
"""Count réel des tests collectés par pytest (hors deselected)."""
|
| 157 |
+
import subprocess
|
| 158 |
+
import sys
|
| 159 |
+
|
| 160 |
+
result = subprocess.run(
|
| 161 |
+
[
|
| 162 |
+
sys.executable, "-m", "pytest",
|
| 163 |
+
"--collect-only", "-q", "--no-cov",
|
| 164 |
+
"-p", "no:cacheprovider",
|
| 165 |
+
str(REPO_ROOT / "tests"),
|
| 166 |
+
],
|
| 167 |
+
capture_output=True, text=True, cwd=REPO_ROOT, timeout=60,
|
| 168 |
+
)
|
| 169 |
+
# La dernière ligne pertinente : « X tests collected »
|
| 170 |
+
import re
|
| 171 |
+
for line in reversed(result.stdout.strip().split("\n")):
|
| 172 |
+
m = re.search(r"(\d+)\s+tests?\s+collected", line)
|
| 173 |
+
if m:
|
| 174 |
+
return int(m.group(1))
|
| 175 |
+
pytest.fail(
|
| 176 |
+
f"Impossible d'extraire le compte de pytest --collect-only.\n"
|
| 177 |
+
f"stdout: {result.stdout[-500:]}\nstderr: {result.stderr[-200:]}"
|
| 178 |
+
)
|
| 179 |
+
|
| 180 |
+
def _extract_count(self, text: str) -> int | None:
|
| 181 |
+
"""Cherche un nombre près du mot ``passed`` dans ``text``."""
|
| 182 |
+
import re
|
| 183 |
+
# Matche « 4189 passed » ou « ~4150 tests » ou « 4150 tests passed ».
|
| 184 |
+
for pattern in (
|
| 185 |
+
r"\*\*(\d{3,5})\s+passed",
|
| 186 |
+
r"(\d{3,5})\s+passed",
|
| 187 |
+
r"~?(\d{3,5})\s+tests",
|
| 188 |
+
):
|
| 189 |
+
m = re.search(pattern, text)
|
| 190 |
+
if m:
|
| 191 |
+
return int(m.group(1))
|
| 192 |
+
return None
|
| 193 |
+
|
| 194 |
+
def test_claude_md_count_close_to_reality(
|
| 195 |
+
self, real_test_count: int,
|
| 196 |
+
) -> None:
|
| 197 |
+
text = CLAUDE_MD.read_text(encoding="utf-8")
|
| 198 |
+
claimed = self._extract_count(text)
|
| 199 |
+
assert claimed is not None, (
|
| 200 |
+
"CLAUDE.md ne contient aucun compteur de tests (``N passed``)."
|
| 201 |
+
)
|
| 202 |
+
delta = abs(claimed - real_test_count)
|
| 203 |
+
assert delta <= 50, (
|
| 204 |
+
f"CLAUDE.md annonce {claimed} tests, réalité = "
|
| 205 |
+
f"{real_test_count} (écart = {delta}). Tolérance ±50.\n"
|
| 206 |
+
f"Lancer ``python scripts/gen_readme_tables.py`` puis "
|
| 207 |
+
f"committer."
|
| 208 |
+
)
|
| 209 |
+
|
| 210 |
+
def test_readme_md_count_close_to_reality(
|
| 211 |
+
self, real_test_count: int,
|
| 212 |
+
) -> None:
|
| 213 |
+
text = README_MD.read_text(encoding="utf-8")
|
| 214 |
+
claimed = self._extract_count(text)
|
| 215 |
+
assert claimed is not None, (
|
| 216 |
+
"README.md ne contient aucun compteur de tests."
|
| 217 |
+
)
|
| 218 |
+
delta = abs(claimed - real_test_count)
|
| 219 |
+
assert delta <= 50, (
|
| 220 |
+
f"README.md annonce {claimed} tests, réalité = "
|
| 221 |
+
f"{real_test_count} (écart = {delta})."
|
| 222 |
+
)
|
| 223 |
+
|
| 224 |
+
|
| 225 |
+
# ──────────────────────────────────────────────────────────────────────
|
| 226 |
+
# 3. Liens internes vers archives correctement orthographiés
|
| 227 |
+
# ──────────────────────────────────────────────────────────────────────
|
| 228 |
+
|
| 229 |
+
|
| 230 |
+
class TestArchiveLinksWellFormed:
|
| 231 |
+
"""L'ancienne version du manifeste contenait des liens cassés
|
| 232 |
+
type ``docs/archiv../archives/migration/...``. Vérifier que ce
|
| 233 |
+
pattern n'est pas réintroduit."""
|
| 234 |
+
|
| 235 |
+
def test_no_typo_in_archive_paths(self) -> None:
|
| 236 |
+
text = ARCHITECTURE_MD.read_text(encoding="utf-8")
|
| 237 |
+
forbidden_substrings = (
|
| 238 |
+
"archiv../archives", # double slash + typo
|
| 239 |
+
"/archiv../",
|
| 240 |
+
"../archiv../",
|
| 241 |
+
)
|
| 242 |
+
for sub in forbidden_substrings:
|
| 243 |
+
assert sub not in text, (
|
| 244 |
+
f"Le manifeste contient le pattern cassé ``{sub}`` "
|
| 245 |
+
f"(résidu d'une refactor mal faite)."
|
| 246 |
+
)
|