arclith-cli 0.21.0__tar.gz → 0.22.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. {arclith_cli-0.21.0 → arclith_cli-0.22.0}/PKG-INFO +53 -10
  2. {arclith_cli-0.21.0 → arclith_cli-0.22.0}/README.md +51 -8
  3. arclith_cli-0.22.0/arclith_cli/__init__.py +1 -0
  4. arclith_cli-0.22.0/arclith_cli/adapter_blueprints.py +398 -0
  5. {arclith_cli-0.21.0 → arclith_cli-0.22.0}/arclith_cli/adapter_generator.py +95 -19
  6. {arclith_cli-0.21.0 → arclith_cli-0.22.0}/arclith_cli/adapter_selection.py +63 -1
  7. {arclith_cli-0.21.0 → arclith_cli-0.22.0}/arclith_cli/adapter_templates.py +6 -37
  8. {arclith_cli-0.21.0 → arclith_cli-0.22.0}/arclith_cli/add_adapter.py +39 -21
  9. arclith_cli-0.22.0/arclith_cli/binding_cli.py +65 -0
  10. arclith_cli-0.22.0/arclith_cli/binding_contract.py +287 -0
  11. arclith_cli-0.22.0/arclith_cli/binding_options.py +78 -0
  12. arclith_cli-0.22.0/arclith_cli/binding_rendering.py +187 -0
  13. {arclith_cli-0.21.0 → arclith_cli-0.22.0}/arclith_cli/capabilities.py +19 -1
  14. {arclith_cli-0.21.0 → arclith_cli-0.22.0}/arclith_cli/catalogs/ai.py +0 -38
  15. {arclith_cli-0.21.0 → arclith_cli-0.22.0}/arclith_cli/catalogs/transports.py +2 -0
  16. {arclith_cli-0.21.0 → arclith_cli-0.22.0}/arclith_cli/core_scaffold.py +28 -0
  17. {arclith_cli-0.21.0 → arclith_cli-0.22.0}/arclith_cli/init_project.py +96 -33
  18. {arclith_cli-0.21.0 → arclith_cli-0.22.0}/arclith_cli/main.py +29 -7
  19. {arclith_cli-0.21.0 → arclith_cli-0.22.0}/arclith_cli/new_project.py +39 -0
  20. {arclith_cli-0.21.0 → arclith_cli-0.22.0}/arclith_cli/recipe.py +6 -0
  21. arclith_cli-0.22.0/arclith_cli/templates/adapters/agent/agent.py.tmpl +10 -0
  22. arclith_cli-0.22.0/arclith_cli/templates/adapters/agent/context.py.tmpl +10 -0
  23. arclith_cli-0.22.0/arclith_cli/templates/adapters/agent/dependencies.py.tmpl +5 -0
  24. arclith_cli-0.22.0/arclith_cli/templates/adapters/agent/graph.py.tmpl +19 -0
  25. arclith_cli-0.22.0/arclith_cli/templates/adapters/agent/nodes/example.py.tmpl +12 -0
  26. arclith_cli-0.22.0/arclith_cli/templates/adapters/agent/state.py.tmpl +11 -0
  27. arclith_cli-0.22.0/arclith_cli/templates/adapters/api/register.py.tmpl +10 -0
  28. arclith_cli-0.22.0/arclith_cli/templates/adapters/api/routers/v1/router.py.tmpl +5 -0
  29. arclith_cli-0.22.0/arclith_cli/templates/adapters/command-bus/consumer.py.tmpl +5 -0
  30. arclith_cli-0.22.0/arclith_cli/templates/adapters/command-bus/register.py.tmpl +7 -0
  31. arclith_cli-0.22.0/arclith_cli/templates/adapters/mcp/register.py.tmpl +10 -0
  32. arclith_cli-0.22.0/arclith_cli/usecase_binding.py +397 -0
  33. {arclith_cli-0.21.0 → arclith_cli-0.22.0}/pyproject.toml +3 -2
  34. {arclith_cli-0.21.0 → arclith_cli-0.22.0}/tests/e2e_manual.sh +1 -1
  35. arclith_cli-0.22.0/tests/test_adapter_blueprints.py +216 -0
  36. {arclith_cli-0.21.0 → arclith_cli-0.22.0}/tests/test_add_adapter.py +90 -40
  37. {arclith_cli-0.21.0 → arclith_cli-0.22.0}/tests/test_capabilities.py +0 -2
  38. {arclith_cli-0.21.0 → arclith_cli-0.22.0}/tests/test_core_scaffold.py +55 -4
  39. {arclith_cli-0.21.0 → arclith_cli-0.22.0}/tests/test_e2e_scaffold.py +2 -4
  40. {arclith_cli-0.21.0 → arclith_cli-0.22.0}/tests/test_embedding_capability.py +3 -3
  41. {arclith_cli-0.21.0 → arclith_cli-0.22.0}/tests/test_recipe.py +22 -2
  42. {arclith_cli-0.21.0 → arclith_cli-0.22.0}/tests/test_scaffold_cli.py +76 -0
  43. arclith_cli-0.22.0/tests/test_usecase_binding.py +497 -0
  44. {arclith_cli-0.21.0 → arclith_cli-0.22.0}/tests/test_vector_store_capability.py +1 -1
  45. arclith_cli-0.22.0/uv.lock +2811 -0
  46. arclith_cli-0.21.0/arclith_cli/__init__.py +0 -1
  47. arclith_cli-0.21.0/uv.lock +0 -1420
  48. {arclith_cli-0.21.0 → arclith_cli-0.22.0}/.gitignore +0 -0
  49. {arclith_cli-0.21.0 → arclith_cli-0.22.0}/LICENSE +0 -0
  50. {arclith_cli-0.21.0 → arclith_cli-0.22.0}/Makefile +0 -0
  51. {arclith_cli-0.21.0 → arclith_cli-0.22.0}/arclith_cli/adapter_config.py +0 -0
  52. {arclith_cli-0.21.0 → arclith_cli-0.22.0}/arclith_cli/adapter_parameters.py +0 -0
  53. {arclith_cli-0.21.0 → arclith_cli-0.22.0}/arclith_cli/adapter_rendering.py +0 -0
  54. {arclith_cli-0.21.0 → arclith_cli-0.22.0}/arclith_cli/capability_models.py +0 -0
  55. {arclith_cli-0.21.0 → arclith_cli-0.22.0}/arclith_cli/catalogs/__init__.py +0 -0
  56. {arclith_cli-0.21.0 → arclith_cli-0.22.0}/arclith_cli/catalogs/channels.py +0 -0
  57. {arclith_cli-0.21.0 → arclith_cli-0.22.0}/arclith_cli/catalogs/core.py +0 -0
  58. {arclith_cli-0.21.0 → arclith_cli-0.22.0}/arclith_cli/catalogs/observability.py +0 -0
  59. {arclith_cli-0.21.0 → arclith_cli-0.22.0}/arclith_cli/catalogs/persistence.py +0 -0
  60. {arclith_cli-0.21.0 → arclith_cli-0.22.0}/arclith_cli/catalogs/repository_facets.py +0 -0
  61. {arclith_cli-0.21.0 → arclith_cli-0.22.0}/arclith_cli/catalogs/repository_postgresql.py +0 -0
  62. {arclith_cli-0.21.0 → arclith_cli-0.22.0}/arclith_cli/catalogs/security.py +0 -0
  63. {arclith_cli-0.21.0 → arclith_cli-0.22.0}/arclith_cli/entity_scanner.py +0 -0
  64. {arclith_cli-0.21.0 → arclith_cli-0.22.0}/arclith_cli/export_config.py +0 -0
  65. {arclith_cli-0.21.0 → arclith_cli-0.22.0}/arclith_cli/project_paths.py +0 -0
  66. {arclith_cli-0.21.0 → arclith_cli-0.22.0}/arclith_cli/recipe_cli.py +0 -0
  67. {arclith_cli-0.21.0 → arclith_cli-0.22.0}/arclith_cli/recipe_models.py +0 -0
  68. {arclith_cli-0.21.0 → arclith_cli-0.22.0}/arclith_cli/rename.py +0 -0
  69. {arclith_cli-0.21.0 → arclith_cli-0.22.0}/arclith_cli/runtime_templates.py +0 -0
  70. {arclith_cli-0.21.0 → arclith_cli-0.22.0}/arclith_cli/scaffold.py +0 -0
  71. {arclith_cli-0.21.0 → arclith_cli-0.22.0}/arclith_cli/scaffold_interactive.py +0 -0
  72. {arclith_cli-0.21.0 → arclith_cli-0.22.0}/arclith_cli/scaffold_templates.py +0 -0
  73. {arclith_cli-0.21.0 → arclith_cli-0.22.0}/arclith_cli/updater.py +0 -0
  74. {arclith_cli-0.21.0 → arclith_cli-0.22.0}/tests/__init__.py +0 -0
  75. {arclith_cli-0.21.0 → arclith_cli-0.22.0}/tests/test_adapter_templates.py +0 -0
  76. {arclith_cli-0.21.0 → arclith_cli-0.22.0}/tests/test_project_paths.py +0 -0
  77. {arclith_cli-0.21.0 → arclith_cli-0.22.0}/tests/test_rename.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: arclith-cli
3
- Version: 0.21.0
3
+ Version: 0.22.0
4
4
  Summary: CLI scaffolding tool for arclith — hexagonal architecture framework
5
5
  Author-email: Killian KOPP <killiankopp@gmail.com>
6
6
  License: Apache License
@@ -186,7 +186,7 @@ License: Apache License
186
186
  License-File: LICENSE
187
187
  Keywords: arclith,cli,ddd,hexagonal-architecture,scaffold
188
188
  Requires-Python: >=3.13
189
- Requires-Dist: arclith>=0.24.0
189
+ Requires-Dist: arclith>=0.25.0
190
190
  Requires-Dist: httpx>=0.27.0
191
191
  Requires-Dist: rich>=13.0.0
192
192
  Requires-Dist: typer>=0.15.0
@@ -194,7 +194,9 @@ Description-Content-Type: text/markdown
194
194
 
195
195
  # arclith-cli
196
196
 
197
- `arclith-cli` génère instantanément un projet Python en architecture hexagonale prêt à démarrer, en téléchargeant le template officiel `_sample` depuis GitHub et en remplaçant l'entité de démo `Ingredient` par le nom de votre choix. Tout type de projet peut être scaffoldé — service REST, agent IA, API MCP — avec les ports, le nom de projet et le backend de persistance configurés d'emblée.
197
+ `arclith-cli` construit un projet Python hexagonal par étapes. `init` pose uniquement le socle
198
+ installable ; chaque adapter, transport et dépendance optionnelle est ensuite ajouté explicitement.
199
+ `new` reste disponible pour générer un projet depuis le template officiel `_sample`.
198
200
 
199
201
  ## Installation
200
202
 
@@ -204,6 +206,8 @@ uv tool install "git+https://github.com/karned-rekipe/arclith.git#subdirectory=c
204
206
 
205
207
  ## Commandes
206
208
 
209
+ Exécuter `arclith-cli` sans argument affiche cette liste de commandes et termine sans erreur.
210
+
207
211
  ### `init` — Initialiser un projet minimal
208
212
 
209
213
  Crée un projet Arclith vide de métier, avec le layout canonique `src/<package>/...`, une
@@ -221,6 +225,39 @@ arclith-cli init todo-list-service --dir ~/projects
221
225
 
222
226
  Cette commande ne crée aucune entité, aucun CRUD et aucun endpoint métier. Elle sert quand on veut
223
227
  construire le projet étape par étape avec `add-entity`, `add-usecase`, puis `add-adapter`.
228
+ Elle ne crée pas non plus FastAPI ou FastMCP et n'installe aucun de leurs extras.
229
+
230
+ #### Pourquoi `src/<package>/...` ?
231
+
232
+ `src/` est la racine des imports du projet installé ; `<package>` est le namespace propre au
233
+ service. Les couches restent donc importées comme `todo_service.domain` ou
234
+ `todo_service.application`, au lieu de créer des packages Python globaux et génériques nommés
235
+ `domain`, `application` et `adapters`. Cette structure évite les collisions, empêche les tests
236
+ d'importer accidentellement le dépôt courant à la place du package installé et suit le `src layout`
237
+ standard de l'écosystème Python.
238
+
239
+ #### Parcours complet vers une API
240
+
241
+ ```bash
242
+ arclith-cli init todo-api
243
+ cd todo-api
244
+ arclith-cli add-entity Todo
245
+ arclith-cli add-usecase CreateTodo --entity Todo
246
+
247
+ # Choisir explicitement la persistance et le transport utilisés.
248
+ arclith-cli add-adapter --capability repository --adapter memory --entity Todo --yes
249
+ arclith-cli add-adapter --capability api --adapter fastapi --yes
250
+
251
+ # Après avoir défini les champs et implémenté CreateTodoUseCase.execute :
252
+ arclith-cli expose-usecase create-todo --via fastapi --feature todos \
253
+ --path /v1/todos --method POST --status-code 201
254
+ uv sync
255
+ MODE=api uv run python main.py
256
+ ```
257
+
258
+ Le registre de binding généré reste à appeler depuis le composition root avec l'instance typée du
259
+ use case ; le CLI ne devine pas ses dépendances métier. Voir le
260
+ [guide des bindings](https://karned-rekipe.github.io/arclith/deep-dives/use-case-bindings/).
224
261
 
225
262
  ---
226
263
 
@@ -350,7 +387,9 @@ logique métier.
350
387
 
351
388
  ### `add-adapter` — Ajouter un adapter
352
389
 
353
- Wizard interactif à lancer **depuis la racine du projet cible**. Scaffold le code Python et/ou les fichiers de configuration pour un nouvel adapter. Par défaut, la capacité cible est `repository`.
390
+ Wizard interactif à lancer **depuis la racine du projet cible**. Sans option, il affiche toutes les
391
+ capabilities et leurs adapters, notamment `api/fastapi` et `mcp/fastmcp`, puis demande le choix.
392
+ Il scaffold uniquement le code, la configuration et l'extra de dépendance du choix effectué.
354
393
 
355
394
  ```bash
356
395
  cd my-recipe-service
@@ -377,9 +416,10 @@ arclith-cli add-adapter --capability repository --adapter memory --entity Recipe
377
416
 
378
417
  **Étapes du wizard :**
379
418
 
380
- 1. **Type d'adapter** — selon la capacité : `memory` · `mongodb` · `duckdb` · `mariadb` · `fastapi` · `fastmcp` · `rabbitmq` · `docker-image` · `lmstudio` · `openai` · `anthropic` · `langgraph` · `langsmith` · `opentelemetry`
381
- 2. **Entité(s) cible(s)** — détectées automatiquement pour les adapters entity-scoped ; ignorées pour les transports globaux, `cache/*`, `llm/*`, `agent/langgraph`, `runtime/docker-image` et les adapters d'observability
382
- 3. **Paramètres** — questions spécifiques à l'adapter :
419
+ 1. **Capability** — toutes les capabilities du catalogue sont proposées avec leurs adapters
420
+ 2. **Type d'adapter** — selon la capability : `memory` · `mongodb` · `duckdb` · `mariadb` · `fastapi` · `fastmcp` · `rabbitmq` · `docker-image` · `lmstudio` · `openai` · `anthropic` · `langgraph` · `langsmith` · `opentelemetry`
421
+ 3. **Entité(s) cible(s)** — détectées automatiquement pour les adapters entity-scoped ; ignorées pour les transports globaux, `cache/*`, `llm/*`, `agent/langgraph`, `runtime/docker-image` et les adapters d'observability
422
+ 4. **Paramètres** — questions spécifiques à l'adapter :
383
423
  - `mongodb` → `db_name`, `collection_name`, `multitenant`
384
424
  - `duckdb` → `path`
385
425
  - `mariadb` → `host`, `port`, `database`, `user`, `driver`, `table_prefix`
@@ -397,12 +437,12 @@ arclith-cli add-adapter --capability repository --adapter memory --entity Recipe
397
437
  - `command-bus/rabbitmq` → `url`, `exchange`, `exchange_type`, `queue`, `routing_key`, `prefetch`, `consumer_name`, `concurrency`, `publisher_confirms`, `durable`, `retry_enabled`, `retry_requeue`, `dead_letter_exchange`, `dead_letter_routing_key`
398
438
  - `runtime/docker-image` → `uv_version`, `api_port`, `mcp_port`, `probe_port`, `agent_port`
399
439
  - `repository/memory` → aucun paramètre
400
- 4. **Activation** — met à jour `config/adapters/adapters.yaml` pour les capacités activables (`repository: <adapter>` ou `observability.enabled: [<adapter>, ...]`) ; `api/fastapi`, `mcp/fastmcp`, `cache/*`, `llm/*`, `agent/langgraph`, `command-bus/rabbitmq` et `runtime/docker-image` sont exposés par leurs fichiers dédiés
401
- 5. **Récapitulatif** — liste des fichiers créés ou remplacés avant confirmation
440
+ 5. **Activation** — met à jour `config/adapters/adapters.yaml` pour les capacités activables (`repository: <adapter>` ou `observability.enabled: [<adapter>, ...]`) ; `api/fastapi`, `mcp/fastmcp`, `cache/*`, `llm/*`, `agent/langgraph`, `command-bus/rabbitmq` et `runtime/docker-image` sont exposés par leurs fichiers dédiés
441
+ 6. **Récapitulatif** — liste des fichiers créés ou remplacés avant confirmation
402
442
 
403
443
  | Option | Défaut | Description |
404
444
  |--------|--------|-------------|
405
- | `--capability` | `repository` | Capacité cible du catalogue standardisé (`repository`, `cache`, `api`, `mcp`, `http`, `command-bus`, `runtime`, `llm`, `agent`, `observability`) |
445
+ | `--capability` | interactif | Capacité cible du catalogue standardisé (`repository`, `cache`, `api`, `mcp`, `http`, `command-bus`, `runtime`, `llm`, `agent`, `observability`) |
406
446
  | `--adapter` / `-a` | interactif | Adapter du catalogue : `memory`, `mongodb`, `duckdb`, `mariadb`, `fastapi`, `fastmcp`, `idempotency`, `etag`, `cache-control`, `rabbitmq`, `docker-image`, `lmstudio`, `openai`, `anthropic`, `langgraph`, `langsmith`, `opentelemetry` |
407
447
  | `--entity` / `-e` | auto si une seule entité | Entité cible, liste séparée par virgule acceptée |
408
448
  | `--all-entities` | `false` | Génère l'adapter pour toutes les entités détectées |
@@ -461,6 +501,9 @@ LM Studio ou tout endpoint OpenAI-compatible avec `base_url`.
461
501
  L'adapter `repository/mongodb` génère `config/adapters/outbound/mongodb.yaml` avec `uri: null`, puis
462
502
  mappe `adapters.mongodb.uri` vers `MONGODB_URI` dans `config/secrets.yaml`. L'URI réelle reste dans
463
503
  l'environnement, un fichier local de secrets ou Vault selon le resolver choisi.
504
+ Il ne génère pas de repository `memory` applicatif. L'implémentation mémoire du framework peut être
505
+ utilisée directement dans des tests ; une spécialisation mémoire du projet n'est créée que par un
506
+ choix explicite de `repository/memory`.
464
507
 
465
508
  L'adapter `agent/langgraph` génère `langgraph.json`, `config/adapters/inbound/langgraph.yaml` et
466
509
  `src/<package>/adapters/inbound/langgraph/agent.py`. Le projet ne modifie ensuite que ce fichier pour
@@ -1,6 +1,8 @@
1
1
  # arclith-cli
2
2
 
3
- `arclith-cli` génère instantanément un projet Python en architecture hexagonale prêt à démarrer, en téléchargeant le template officiel `_sample` depuis GitHub et en remplaçant l'entité de démo `Ingredient` par le nom de votre choix. Tout type de projet peut être scaffoldé — service REST, agent IA, API MCP — avec les ports, le nom de projet et le backend de persistance configurés d'emblée.
3
+ `arclith-cli` construit un projet Python hexagonal par étapes. `init` pose uniquement le socle
4
+ installable ; chaque adapter, transport et dépendance optionnelle est ensuite ajouté explicitement.
5
+ `new` reste disponible pour générer un projet depuis le template officiel `_sample`.
4
6
 
5
7
  ## Installation
6
8
 
@@ -10,6 +12,8 @@ uv tool install "git+https://github.com/karned-rekipe/arclith.git#subdirectory=c
10
12
 
11
13
  ## Commandes
12
14
 
15
+ Exécuter `arclith-cli` sans argument affiche cette liste de commandes et termine sans erreur.
16
+
13
17
  ### `init` — Initialiser un projet minimal
14
18
 
15
19
  Crée un projet Arclith vide de métier, avec le layout canonique `src/<package>/...`, une
@@ -27,6 +31,39 @@ arclith-cli init todo-list-service --dir ~/projects
27
31
 
28
32
  Cette commande ne crée aucune entité, aucun CRUD et aucun endpoint métier. Elle sert quand on veut
29
33
  construire le projet étape par étape avec `add-entity`, `add-usecase`, puis `add-adapter`.
34
+ Elle ne crée pas non plus FastAPI ou FastMCP et n'installe aucun de leurs extras.
35
+
36
+ #### Pourquoi `src/<package>/...` ?
37
+
38
+ `src/` est la racine des imports du projet installé ; `<package>` est le namespace propre au
39
+ service. Les couches restent donc importées comme `todo_service.domain` ou
40
+ `todo_service.application`, au lieu de créer des packages Python globaux et génériques nommés
41
+ `domain`, `application` et `adapters`. Cette structure évite les collisions, empêche les tests
42
+ d'importer accidentellement le dépôt courant à la place du package installé et suit le `src layout`
43
+ standard de l'écosystème Python.
44
+
45
+ #### Parcours complet vers une API
46
+
47
+ ```bash
48
+ arclith-cli init todo-api
49
+ cd todo-api
50
+ arclith-cli add-entity Todo
51
+ arclith-cli add-usecase CreateTodo --entity Todo
52
+
53
+ # Choisir explicitement la persistance et le transport utilisés.
54
+ arclith-cli add-adapter --capability repository --adapter memory --entity Todo --yes
55
+ arclith-cli add-adapter --capability api --adapter fastapi --yes
56
+
57
+ # Après avoir défini les champs et implémenté CreateTodoUseCase.execute :
58
+ arclith-cli expose-usecase create-todo --via fastapi --feature todos \
59
+ --path /v1/todos --method POST --status-code 201
60
+ uv sync
61
+ MODE=api uv run python main.py
62
+ ```
63
+
64
+ Le registre de binding généré reste à appeler depuis le composition root avec l'instance typée du
65
+ use case ; le CLI ne devine pas ses dépendances métier. Voir le
66
+ [guide des bindings](https://karned-rekipe.github.io/arclith/deep-dives/use-case-bindings/).
30
67
 
31
68
  ---
32
69
 
@@ -156,7 +193,9 @@ logique métier.
156
193
 
157
194
  ### `add-adapter` — Ajouter un adapter
158
195
 
159
- Wizard interactif à lancer **depuis la racine du projet cible**. Scaffold le code Python et/ou les fichiers de configuration pour un nouvel adapter. Par défaut, la capacité cible est `repository`.
196
+ Wizard interactif à lancer **depuis la racine du projet cible**. Sans option, il affiche toutes les
197
+ capabilities et leurs adapters, notamment `api/fastapi` et `mcp/fastmcp`, puis demande le choix.
198
+ Il scaffold uniquement le code, la configuration et l'extra de dépendance du choix effectué.
160
199
 
161
200
  ```bash
162
201
  cd my-recipe-service
@@ -183,9 +222,10 @@ arclith-cli add-adapter --capability repository --adapter memory --entity Recipe
183
222
 
184
223
  **Étapes du wizard :**
185
224
 
186
- 1. **Type d'adapter** — selon la capacité : `memory` · `mongodb` · `duckdb` · `mariadb` · `fastapi` · `fastmcp` · `rabbitmq` · `docker-image` · `lmstudio` · `openai` · `anthropic` · `langgraph` · `langsmith` · `opentelemetry`
187
- 2. **Entité(s) cible(s)** — détectées automatiquement pour les adapters entity-scoped ; ignorées pour les transports globaux, `cache/*`, `llm/*`, `agent/langgraph`, `runtime/docker-image` et les adapters d'observability
188
- 3. **Paramètres** — questions spécifiques à l'adapter :
225
+ 1. **Capability** — toutes les capabilities du catalogue sont proposées avec leurs adapters
226
+ 2. **Type d'adapter** — selon la capability : `memory` · `mongodb` · `duckdb` · `mariadb` · `fastapi` · `fastmcp` · `rabbitmq` · `docker-image` · `lmstudio` · `openai` · `anthropic` · `langgraph` · `langsmith` · `opentelemetry`
227
+ 3. **Entité(s) cible(s)** — détectées automatiquement pour les adapters entity-scoped ; ignorées pour les transports globaux, `cache/*`, `llm/*`, `agent/langgraph`, `runtime/docker-image` et les adapters d'observability
228
+ 4. **Paramètres** — questions spécifiques à l'adapter :
189
229
  - `mongodb` → `db_name`, `collection_name`, `multitenant`
190
230
  - `duckdb` → `path`
191
231
  - `mariadb` → `host`, `port`, `database`, `user`, `driver`, `table_prefix`
@@ -203,12 +243,12 @@ arclith-cli add-adapter --capability repository --adapter memory --entity Recipe
203
243
  - `command-bus/rabbitmq` → `url`, `exchange`, `exchange_type`, `queue`, `routing_key`, `prefetch`, `consumer_name`, `concurrency`, `publisher_confirms`, `durable`, `retry_enabled`, `retry_requeue`, `dead_letter_exchange`, `dead_letter_routing_key`
204
244
  - `runtime/docker-image` → `uv_version`, `api_port`, `mcp_port`, `probe_port`, `agent_port`
205
245
  - `repository/memory` → aucun paramètre
206
- 4. **Activation** — met à jour `config/adapters/adapters.yaml` pour les capacités activables (`repository: <adapter>` ou `observability.enabled: [<adapter>, ...]`) ; `api/fastapi`, `mcp/fastmcp`, `cache/*`, `llm/*`, `agent/langgraph`, `command-bus/rabbitmq` et `runtime/docker-image` sont exposés par leurs fichiers dédiés
207
- 5. **Récapitulatif** — liste des fichiers créés ou remplacés avant confirmation
246
+ 5. **Activation** — met à jour `config/adapters/adapters.yaml` pour les capacités activables (`repository: <adapter>` ou `observability.enabled: [<adapter>, ...]`) ; `api/fastapi`, `mcp/fastmcp`, `cache/*`, `llm/*`, `agent/langgraph`, `command-bus/rabbitmq` et `runtime/docker-image` sont exposés par leurs fichiers dédiés
247
+ 6. **Récapitulatif** — liste des fichiers créés ou remplacés avant confirmation
208
248
 
209
249
  | Option | Défaut | Description |
210
250
  |--------|--------|-------------|
211
- | `--capability` | `repository` | Capacité cible du catalogue standardisé (`repository`, `cache`, `api`, `mcp`, `http`, `command-bus`, `runtime`, `llm`, `agent`, `observability`) |
251
+ | `--capability` | interactif | Capacité cible du catalogue standardisé (`repository`, `cache`, `api`, `mcp`, `http`, `command-bus`, `runtime`, `llm`, `agent`, `observability`) |
212
252
  | `--adapter` / `-a` | interactif | Adapter du catalogue : `memory`, `mongodb`, `duckdb`, `mariadb`, `fastapi`, `fastmcp`, `idempotency`, `etag`, `cache-control`, `rabbitmq`, `docker-image`, `lmstudio`, `openai`, `anthropic`, `langgraph`, `langsmith`, `opentelemetry` |
213
253
  | `--entity` / `-e` | auto si une seule entité | Entité cible, liste séparée par virgule acceptée |
214
254
  | `--all-entities` | `false` | Génère l'adapter pour toutes les entités détectées |
@@ -267,6 +307,9 @@ LM Studio ou tout endpoint OpenAI-compatible avec `base_url`.
267
307
  L'adapter `repository/mongodb` génère `config/adapters/outbound/mongodb.yaml` avec `uri: null`, puis
268
308
  mappe `adapters.mongodb.uri` vers `MONGODB_URI` dans `config/secrets.yaml`. L'URI réelle reste dans
269
309
  l'environnement, un fichier local de secrets ou Vault selon le resolver choisi.
310
+ Il ne génère pas de repository `memory` applicatif. L'implémentation mémoire du framework peut être
311
+ utilisée directement dans des tests ; une spécialisation mémoire du projet n'est créée que par un
312
+ choix explicite de `repository/memory`.
270
313
 
271
314
  L'adapter `agent/langgraph` génère `langgraph.json`, `config/adapters/inbound/langgraph.yaml` et
272
315
  `src/<package>/adapters/inbound/langgraph/agent.py`. Le projet ne modifie ensuite que ce fichier pour
@@ -0,0 +1 @@
1
+ __version__ = "0.22.0"
@@ -0,0 +1,398 @@
1
+ """Versioned, complete adapter layouts shared by generation and architecture checks.
2
+
3
+ Every declared extension point is materialized when an adapter is installed.
4
+ Developer-owned files are created once: replay fills missing files, never rewrites
5
+ implementation. Templates deliberately register no business operation.
6
+ """
7
+
8
+ from dataclasses import dataclass
9
+ from hashlib import sha256
10
+ from importlib.resources import files
11
+ from pathlib import Path, PurePosixPath
12
+ from string import Template
13
+
14
+ from arclith_cli.capability_models import AdapterSpec
15
+ from arclith_cli.entity_scanner import scan_entities
16
+ from arclith_cli.project_paths import ProjectPaths
17
+
18
+ BLUEPRINT_VERSION = "1"
19
+
20
+
21
+ @dataclass(frozen=True)
22
+ class AdapterBlueprint:
23
+ """A technology's complete extension points and developer-owned modules."""
24
+
25
+ capability: str
26
+ adapter: str
27
+ layer: str
28
+ roles: tuple[str, ...]
29
+ modules: tuple[str, ...]
30
+ reference: str
31
+ version: str = BLUEPRINT_VERSION
32
+
33
+ @property
34
+ def directory(self) -> str:
35
+ return self.adapter.replace("-", "_")
36
+
37
+ @property
38
+ def root_parts(self) -> tuple[str, ...]:
39
+ if self.layer == "runtime":
40
+ return ("infrastructure", "runtime", self.directory)
41
+ return ("adapters", self.layer, self.directory)
42
+
43
+
44
+ # Capability-level roles are intentionally independent of provider SDK internals.
45
+ # A provider may share a directory (e.g. memory repository and memory cache); the
46
+ # role sets are additive and no existing implementation is replaced.
47
+ _PROFILES: dict[str, tuple[tuple[str, ...], tuple[str, ...]]] = {
48
+ "repository": (
49
+ ("repositories", "models", "mappers", "indexes", "migrations"),
50
+ ("dependencies", "errors"),
51
+ ),
52
+ "storage": (
53
+ ("transfers", "metadata", "policies"),
54
+ ("client", "dependencies", "errors"),
55
+ ),
56
+ "vector-store": (
57
+ ("collections", "mappers", "indexes", "migrations"),
58
+ ("client", "dependencies", "errors"),
59
+ ),
60
+ "cache": (
61
+ ("codecs", "keys", "invalidation", "locks"),
62
+ ("client", "dependencies", "errors"),
63
+ ),
64
+ "logger": (("formatters", "filters", "sinks"), ("setup", "correlation")),
65
+ "secrets": (
66
+ ("resolvers", "rotation", "policies"),
67
+ ("provider", "settings", "errors"),
68
+ ),
69
+ "api": (
70
+ ("middleware", "contracts", "routers", "routers/v1"),
71
+ ("register", "dependencies", "errors", "routers/v1/router"),
72
+ ),
73
+ "mcp": (
74
+ ("middleware", "contracts", "features"),
75
+ ("register", "dependencies", "errors"),
76
+ ),
77
+ "probe": (
78
+ ("checks", "diagnostics"),
79
+ ("register", "health", "readiness", "dependencies"),
80
+ ),
81
+ "http": (
82
+ ("middleware", "policies", "schemas"),
83
+ ("register", "dependencies", "errors"),
84
+ ),
85
+ "command-bus": (
86
+ ("bindings", "contracts", "schemas", "policies"),
87
+ ("register", "codec", "topology", "consumer", "publisher", "dependencies"),
88
+ ),
89
+ "channel": (
90
+ (
91
+ "inbound",
92
+ "outbound",
93
+ "schemas",
94
+ "mappers",
95
+ "presenters",
96
+ "attachments",
97
+ "policies",
98
+ ),
99
+ ("register", "client", "dependencies", "security", "errors"),
100
+ ),
101
+ "runtime": (("bootstrap", "health", "policies"), ("settings", "lifecycle")),
102
+ "auth": (("principals", "mappers", "policies"), ("dependencies", "errors")),
103
+ "tenant": (
104
+ ("resolvers", "mappers", "policies"),
105
+ ("dependencies", "context", "errors"),
106
+ ),
107
+ "license": (("mappers", "policies"), ("dependencies", "errors")),
108
+ "llm": (("models", "mappers", "policies"), ("client", "dependencies", "errors")),
109
+ "embedding": (
110
+ ("models", "mappers", "batching", "policies"),
111
+ ("client", "dependencies", "errors"),
112
+ ),
113
+ "agent": (
114
+ (
115
+ "nodes",
116
+ "contracts",
117
+ "capabilities",
118
+ "subgraphs",
119
+ "tools",
120
+ "prompts",
121
+ "parsers",
122
+ "presenters",
123
+ "policies",
124
+ "persistence",
125
+ "shared",
126
+ ),
127
+ ("agent", "graph", "state", "context", "dependencies", "routing"),
128
+ ),
129
+ "agent-persistence": (
130
+ (
131
+ "persistence",
132
+ "persistence/checkpoints",
133
+ "persistence/stores",
134
+ "persistence/migrations",
135
+ ),
136
+ (),
137
+ ),
138
+ "observability": (
139
+ ("instrumentation", "exporters", "sampling"),
140
+ ("setup", "correlation", "dependencies"),
141
+ ),
142
+ }
143
+
144
+ _REFERENCES = {
145
+ "api": "https://fastapi.tiangolo.com/tutorial/bigger-applications/",
146
+ "mcp": "https://gofastmcp.com/servers/composition",
147
+ "agent": "https://docs.langchain.com/oss/python/langgraph/application-structure",
148
+ "agent-persistence": "https://docs.langchain.com/oss/python/langgraph/persistence",
149
+ "command-bus": "https://www.rabbitmq.com/docs/reliability",
150
+ }
151
+
152
+ _ROLE_GUIDANCE = {
153
+ "nodes": "Thin state-to-application adapters. Return patches; inject services through composition closures. Public context contains only serializable invocation values.",
154
+ "capabilities": "Group one conversational capability per package: intents, workflow, mapping and presentation.",
155
+ "subgraphs": "Only compiled multi-node graphs with an explicit state and lifecycle belong here.",
156
+ "tools": "One cohesive tool or tool family per module. Translate input into a typed application request.",
157
+ "resources": "Read-only MCP resources and resource templates. Keep public URIs stable.",
158
+ "prompts": "Versioned transport-owned prompt templates; application prompts stay in application.",
159
+ "persistence": "Runtime checkpoints and stores, separate from domain persistence. Version persisted state.",
160
+ "shared": "Only primitives used by at least two capabilities. Avoid generic helpers or utils modules.",
161
+ "routers": "Explicit APIRouter composition by API version and feature. Declare status_code and responses.",
162
+ "middleware": "Transport-wide request concerns. Keep business policies in application or domain.",
163
+ "bindings": "Decode and validate the wire message, then invoke the same typed application port as other transports.",
164
+ "repositories": "Implement outbound ports. Keep queries in the port contract and SDK types inside this adapter.",
165
+ "mappers": "Pure translation functions between transport/provider models and application contracts.",
166
+ "presenters": "Render application results without making business decisions or performing I/O.",
167
+ "policies": "Technical retry, timeout and idempotency policies. Retry only safe/idempotent operations.",
168
+ "migrations": "Explicit version-to-version migrations. Preserve compatibility with already persisted data.",
169
+ }
170
+
171
+
172
+ def get_adapter_blueprint(adapter: AdapterSpec) -> AdapterBlueprint:
173
+ """Fail closed if a new catalog capability has no declared layout."""
174
+ if adapter.capability not in _PROFILES:
175
+ raise ValueError(f"No adapter blueprint for capability {adapter.capability!r}")
176
+ roles, modules = _PROFILES[adapter.capability]
177
+ return AdapterBlueprint(
178
+ capability=adapter.capability,
179
+ adapter=adapter.name,
180
+ layer=adapter.layer,
181
+ roles=roles,
182
+ modules=modules,
183
+ reference=_REFERENCES.get(
184
+ adapter.capability, "https://karned-rekipe.github.io/arclith/capabilities/"
185
+ ),
186
+ )
187
+
188
+
189
+ def _module_guide(role: str) -> str:
190
+ return _ROLE_GUIDANCE.get(
191
+ role,
192
+ f"Keep cohesive {role.replace('_', ' ')} responsibilities in named modules.",
193
+ )
194
+
195
+
196
+ def _package_files(role: str, reference: str) -> dict[str, str]:
197
+ return {
198
+ f"{role}/__init__.py": '"""Developer-owned extension point; explicit imports only."""\n',
199
+ f"{role}/README.md": (
200
+ f"# {role}\n\n{_module_guide(PurePosixPath(role).name)}\n\n"
201
+ "This package is created immediately by Arclith. Add named modules here; "
202
+ "do not move this responsibility to a global `utils.py` or `nodes.py`.\n\n"
203
+ f"[Reference]({reference}). Files are developer-owned and preserved on replay.\n"
204
+ ),
205
+ }
206
+
207
+
208
+ def _template(name: str, variables: dict[str, str]) -> str:
209
+ source = (
210
+ files("arclith_cli")
211
+ .joinpath("templates", "adapters", name)
212
+ .read_text(encoding="utf-8")
213
+ )
214
+ return Template(source).substitute(variables)
215
+
216
+
217
+ def render_feature_blueprint(
218
+ kind: str, feature: str, package_name: str
219
+ ) -> dict[str, str]:
220
+ """Render a full inert feature relative to its adapter root.
221
+
222
+ ``kind`` accepts FastAPI/FastMCP names or their capability names. No public
223
+ endpoint, tool, resource or prompt is registered by this skeleton.
224
+ """
225
+ if not feature.isidentifier() or feature.startswith("_"):
226
+ raise ValueError("feature must be a public Python identifier")
227
+ roles: tuple[str, ...]
228
+ modules: tuple[str, ...]
229
+ if kind in {"api", "fastapi"}:
230
+ base = f"routers/v1/{feature}"
231
+ roles = ("routes",)
232
+ modules = ("router", "schemas", "mappers", "presenters", "openapi")
233
+ reference = _REFERENCES["api"]
234
+ elif kind in {"mcp", "fastmcp"}:
235
+ base = f"features/{feature}"
236
+ roles = ("tools", "resources", "prompts")
237
+ modules = ("register", "schemas", "mappers", "presenters")
238
+ reference = _REFERENCES["mcp"]
239
+ else:
240
+ raise ValueError(f"Unsupported feature blueprint {kind!r}")
241
+ result = _package_files(base, reference)
242
+ for role in roles:
243
+ result.update(_package_files(f"{base}/{role}", reference))
244
+ for module in modules:
245
+ result[f"{base}/{module}.py"] = f'"""{feature}: {_module_guide(module)}"""\n'
246
+ if kind in {"api", "fastapi"}:
247
+ result[f"{base}/router.py"] += (
248
+ "\nfrom fastapi import APIRouter\n\nrouter = APIRouter()\n"
249
+ )
250
+ return result
251
+
252
+
253
+ def render_adapter_blueprint(
254
+ blueprint: AdapterBlueprint,
255
+ package_name: str,
256
+ features: tuple[str, ...] = (),
257
+ *,
258
+ graph_name: str = "agent",
259
+ ) -> dict[str, str]:
260
+ """Render the complete file contract relative to ``blueprint.root_parts``."""
261
+ result = {
262
+ "__init__.py": '"""Project-specific adapter; dependencies are supplied by the composition root."""\n'
263
+ }
264
+ for role in blueprint.roles:
265
+ result.update(_package_files(role, blueprint.reference))
266
+ for module in blueprint.modules:
267
+ result[f"{module}.py"] = (
268
+ f'"""{_module_guide(PurePosixPath(module).name)} See README.md."""\n'
269
+ )
270
+ prefix = (
271
+ ".".join((package_name, *blueprint.root_parts))
272
+ if package_name
273
+ else ".".join(blueprint.root_parts)
274
+ )
275
+ variables = {
276
+ "adapter_import": prefix,
277
+ "package_name": package_name,
278
+ "graph_name": graph_name,
279
+ }
280
+ templates: dict[str, tuple[str, ...]] = {
281
+ "api": ("register", "routers/v1/router"),
282
+ "mcp": ("register",),
283
+ "agent": (
284
+ "agent",
285
+ "graph",
286
+ "state",
287
+ "context",
288
+ "dependencies",
289
+ "nodes/example",
290
+ ),
291
+ "command-bus": ("register", "consumer"),
292
+ }
293
+ for module in templates.get(blueprint.capability, ()):
294
+ result[f"{module}.py"] = _template(
295
+ f"{blueprint.capability}/{module}.py.tmpl", variables
296
+ )
297
+ if blueprint.capability in {"api", "mcp"}:
298
+ for feature in features or ("example",):
299
+ result.update(
300
+ render_feature_blueprint(blueprint.capability, feature, package_name)
301
+ )
302
+ layout = "\n".join(f"- `{name}`" for name in sorted(result))
303
+ boundaries = {
304
+ "inbound": "Input -> transport mapper -> typed application Command/Query -> inbound port -> Result -> presenter. Do not import concrete outbound adapters here.",
305
+ "outbound": "Implement the application's outbound port. Keep provider SDK types and persistence mappings inside this adapter; business rules stay in application/domain.",
306
+ "bidirectional": "Separate inbound decoding/validation from outbound publishing. Inbound messages invoke typed application ports; outbound messages implement outbound ports.",
307
+ "runtime": "Assemble application dependencies and manage process startup/shutdown. Runtime configuration does not contain business rules.",
308
+ }
309
+ result["README.md"] = (
310
+ f"# {blueprint.adapter} adapter\n\nBlueprint version: `{blueprint.version}`.\n\n"
311
+ "All folders and role files are created at installation. `example` is an inert reference feature; "
312
+ "it exposes no business operation. Register features explicitly from the composition root.\n\n"
313
+ f"{boundaries[blueprint.layer]} Domain logic belongs to the application/domain.\n\n"
314
+ "Files are developer-owned: rerunning the scaffold fills missing files and preserves existing code. "
315
+ "Do not register components through import side effects in `__init__.py`.\n\n"
316
+ f"[Official reference]({blueprint.reference}).\n\n## Complete layout\n\n{layout}\n"
317
+ )
318
+ return result
319
+
320
+
321
+ def blueprint_digest(blueprint: AdapterBlueprint) -> str:
322
+ """Digest the template contract, independent of project/feature names."""
323
+ rendered = render_adapter_blueprint(blueprint, "application_package")
324
+ payload = "\n".join(
325
+ f"{path}\0{content}" for path, content in sorted(rendered.items())
326
+ )
327
+ return "sha256:" + sha256(payload.encode("utf-8")).hexdigest()
328
+
329
+
330
+ def write_missing_files(root: Path, rendered: dict[str, str]) -> tuple[Path, ...]:
331
+ """Create developer-owned files once. Reject paths outside the declared root."""
332
+ created: list[Path] = []
333
+ for relative, content in rendered.items():
334
+ relative_path = PurePosixPath(relative)
335
+ if relative_path.is_absolute() or ".." in relative_path.parts:
336
+ raise ValueError(f"Unsafe blueprint path: {relative}")
337
+ path = root / relative
338
+ if path.exists():
339
+ continue
340
+ path.parent.mkdir(parents=True, exist_ok=True)
341
+ path.write_text(content, encoding="utf-8")
342
+ created.append(path)
343
+ return tuple(created)
344
+
345
+
346
+ def assert_no_module_shadowing(root: Path, blueprint: AdapterBlueprint) -> None:
347
+ """Do not silently hide a legacy nodes.py/router.py behind a new package."""
348
+ conflicts = [
349
+ str(root / f"{role}.py")
350
+ for role in blueprint.roles
351
+ if (root / f"{role}.py").is_file()
352
+ and not (root / role / "__init__.py").exists()
353
+ ]
354
+ if conflicts:
355
+ raise ValueError(
356
+ "Migrate legacy modules before creating packages with the same import name: "
357
+ + ", ".join(conflicts)
358
+ )
359
+
360
+
361
+ def scaffold_adapter_blueprint(
362
+ project_dir: Path,
363
+ paths: ProjectPaths,
364
+ adapter: AdapterSpec,
365
+ *,
366
+ graph_name: str = "agent",
367
+ ) -> tuple[Path, ...]:
368
+ """Install the selected technology's full layout and native feature roles."""
369
+ blueprint = get_adapter_blueprint(adapter)
370
+ root = paths.package_root.joinpath(*blueprint.root_parts)
371
+ assert_no_module_shadowing(root, blueprint)
372
+ features = tuple(entity.snake for entity in scan_entities(project_dir))
373
+ rendered = render_adapter_blueprint(
374
+ blueprint, paths.package_name or "", features, graph_name=graph_name
375
+ )
376
+ created = write_missing_files(root, rendered)
377
+ # Metadata is template provenance, not a claim that edited code matches it.
378
+ metadata = {
379
+ f"{adapter.capability}-{adapter.name}.yaml": (
380
+ f"blueprint_version: {blueprint.version!r}\n"
381
+ f"template_digest: {blueprint_digest(blueprint)!r}\n"
382
+ f"capability: {adapter.capability}\nadapter: {adapter.name}\n"
383
+ "ownership: developer\n"
384
+ )
385
+ }
386
+ write_missing_files(project_dir / ".arclith" / "blueprints", metadata)
387
+ return created
388
+
389
+
390
+ def validate_adapter_blueprint(
391
+ root: Path, blueprint: AdapterBlueprint
392
+ ) -> tuple[str, ...]:
393
+ """Return missing native files; feature names remain project-specific."""
394
+ expected = {"__init__.py", "README.md"}
395
+ expected.update(f"{module}.py" for module in blueprint.modules)
396
+ for role in blueprint.roles:
397
+ expected.update((f"{role}/__init__.py", f"{role}/README.md"))
398
+ return tuple(sorted(path for path in expected if not (root / path).is_file()))