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.
- {arclith_cli-0.21.0 → arclith_cli-0.22.0}/PKG-INFO +53 -10
- {arclith_cli-0.21.0 → arclith_cli-0.22.0}/README.md +51 -8
- arclith_cli-0.22.0/arclith_cli/__init__.py +1 -0
- arclith_cli-0.22.0/arclith_cli/adapter_blueprints.py +398 -0
- {arclith_cli-0.21.0 → arclith_cli-0.22.0}/arclith_cli/adapter_generator.py +95 -19
- {arclith_cli-0.21.0 → arclith_cli-0.22.0}/arclith_cli/adapter_selection.py +63 -1
- {arclith_cli-0.21.0 → arclith_cli-0.22.0}/arclith_cli/adapter_templates.py +6 -37
- {arclith_cli-0.21.0 → arclith_cli-0.22.0}/arclith_cli/add_adapter.py +39 -21
- arclith_cli-0.22.0/arclith_cli/binding_cli.py +65 -0
- arclith_cli-0.22.0/arclith_cli/binding_contract.py +287 -0
- arclith_cli-0.22.0/arclith_cli/binding_options.py +78 -0
- arclith_cli-0.22.0/arclith_cli/binding_rendering.py +187 -0
- {arclith_cli-0.21.0 → arclith_cli-0.22.0}/arclith_cli/capabilities.py +19 -1
- {arclith_cli-0.21.0 → arclith_cli-0.22.0}/arclith_cli/catalogs/ai.py +0 -38
- {arclith_cli-0.21.0 → arclith_cli-0.22.0}/arclith_cli/catalogs/transports.py +2 -0
- {arclith_cli-0.21.0 → arclith_cli-0.22.0}/arclith_cli/core_scaffold.py +28 -0
- {arclith_cli-0.21.0 → arclith_cli-0.22.0}/arclith_cli/init_project.py +96 -33
- {arclith_cli-0.21.0 → arclith_cli-0.22.0}/arclith_cli/main.py +29 -7
- {arclith_cli-0.21.0 → arclith_cli-0.22.0}/arclith_cli/new_project.py +39 -0
- {arclith_cli-0.21.0 → arclith_cli-0.22.0}/arclith_cli/recipe.py +6 -0
- arclith_cli-0.22.0/arclith_cli/templates/adapters/agent/agent.py.tmpl +10 -0
- arclith_cli-0.22.0/arclith_cli/templates/adapters/agent/context.py.tmpl +10 -0
- arclith_cli-0.22.0/arclith_cli/templates/adapters/agent/dependencies.py.tmpl +5 -0
- arclith_cli-0.22.0/arclith_cli/templates/adapters/agent/graph.py.tmpl +19 -0
- arclith_cli-0.22.0/arclith_cli/templates/adapters/agent/nodes/example.py.tmpl +12 -0
- arclith_cli-0.22.0/arclith_cli/templates/adapters/agent/state.py.tmpl +11 -0
- arclith_cli-0.22.0/arclith_cli/templates/adapters/api/register.py.tmpl +10 -0
- arclith_cli-0.22.0/arclith_cli/templates/adapters/api/routers/v1/router.py.tmpl +5 -0
- arclith_cli-0.22.0/arclith_cli/templates/adapters/command-bus/consumer.py.tmpl +5 -0
- arclith_cli-0.22.0/arclith_cli/templates/adapters/command-bus/register.py.tmpl +7 -0
- arclith_cli-0.22.0/arclith_cli/templates/adapters/mcp/register.py.tmpl +10 -0
- arclith_cli-0.22.0/arclith_cli/usecase_binding.py +397 -0
- {arclith_cli-0.21.0 → arclith_cli-0.22.0}/pyproject.toml +3 -2
- {arclith_cli-0.21.0 → arclith_cli-0.22.0}/tests/e2e_manual.sh +1 -1
- arclith_cli-0.22.0/tests/test_adapter_blueprints.py +216 -0
- {arclith_cli-0.21.0 → arclith_cli-0.22.0}/tests/test_add_adapter.py +90 -40
- {arclith_cli-0.21.0 → arclith_cli-0.22.0}/tests/test_capabilities.py +0 -2
- {arclith_cli-0.21.0 → arclith_cli-0.22.0}/tests/test_core_scaffold.py +55 -4
- {arclith_cli-0.21.0 → arclith_cli-0.22.0}/tests/test_e2e_scaffold.py +2 -4
- {arclith_cli-0.21.0 → arclith_cli-0.22.0}/tests/test_embedding_capability.py +3 -3
- {arclith_cli-0.21.0 → arclith_cli-0.22.0}/tests/test_recipe.py +22 -2
- {arclith_cli-0.21.0 → arclith_cli-0.22.0}/tests/test_scaffold_cli.py +76 -0
- arclith_cli-0.22.0/tests/test_usecase_binding.py +497 -0
- {arclith_cli-0.21.0 → arclith_cli-0.22.0}/tests/test_vector_store_capability.py +1 -1
- arclith_cli-0.22.0/uv.lock +2811 -0
- arclith_cli-0.21.0/arclith_cli/__init__.py +0 -1
- arclith_cli-0.21.0/uv.lock +0 -1420
- {arclith_cli-0.21.0 → arclith_cli-0.22.0}/.gitignore +0 -0
- {arclith_cli-0.21.0 → arclith_cli-0.22.0}/LICENSE +0 -0
- {arclith_cli-0.21.0 → arclith_cli-0.22.0}/Makefile +0 -0
- {arclith_cli-0.21.0 → arclith_cli-0.22.0}/arclith_cli/adapter_config.py +0 -0
- {arclith_cli-0.21.0 → arclith_cli-0.22.0}/arclith_cli/adapter_parameters.py +0 -0
- {arclith_cli-0.21.0 → arclith_cli-0.22.0}/arclith_cli/adapter_rendering.py +0 -0
- {arclith_cli-0.21.0 → arclith_cli-0.22.0}/arclith_cli/capability_models.py +0 -0
- {arclith_cli-0.21.0 → arclith_cli-0.22.0}/arclith_cli/catalogs/__init__.py +0 -0
- {arclith_cli-0.21.0 → arclith_cli-0.22.0}/arclith_cli/catalogs/channels.py +0 -0
- {arclith_cli-0.21.0 → arclith_cli-0.22.0}/arclith_cli/catalogs/core.py +0 -0
- {arclith_cli-0.21.0 → arclith_cli-0.22.0}/arclith_cli/catalogs/observability.py +0 -0
- {arclith_cli-0.21.0 → arclith_cli-0.22.0}/arclith_cli/catalogs/persistence.py +0 -0
- {arclith_cli-0.21.0 → arclith_cli-0.22.0}/arclith_cli/catalogs/repository_facets.py +0 -0
- {arclith_cli-0.21.0 → arclith_cli-0.22.0}/arclith_cli/catalogs/repository_postgresql.py +0 -0
- {arclith_cli-0.21.0 → arclith_cli-0.22.0}/arclith_cli/catalogs/security.py +0 -0
- {arclith_cli-0.21.0 → arclith_cli-0.22.0}/arclith_cli/entity_scanner.py +0 -0
- {arclith_cli-0.21.0 → arclith_cli-0.22.0}/arclith_cli/export_config.py +0 -0
- {arclith_cli-0.21.0 → arclith_cli-0.22.0}/arclith_cli/project_paths.py +0 -0
- {arclith_cli-0.21.0 → arclith_cli-0.22.0}/arclith_cli/recipe_cli.py +0 -0
- {arclith_cli-0.21.0 → arclith_cli-0.22.0}/arclith_cli/recipe_models.py +0 -0
- {arclith_cli-0.21.0 → arclith_cli-0.22.0}/arclith_cli/rename.py +0 -0
- {arclith_cli-0.21.0 → arclith_cli-0.22.0}/arclith_cli/runtime_templates.py +0 -0
- {arclith_cli-0.21.0 → arclith_cli-0.22.0}/arclith_cli/scaffold.py +0 -0
- {arclith_cli-0.21.0 → arclith_cli-0.22.0}/arclith_cli/scaffold_interactive.py +0 -0
- {arclith_cli-0.21.0 → arclith_cli-0.22.0}/arclith_cli/scaffold_templates.py +0 -0
- {arclith_cli-0.21.0 → arclith_cli-0.22.0}/arclith_cli/updater.py +0 -0
- {arclith_cli-0.21.0 → arclith_cli-0.22.0}/tests/__init__.py +0 -0
- {arclith_cli-0.21.0 → arclith_cli-0.22.0}/tests/test_adapter_templates.py +0 -0
- {arclith_cli-0.21.0 → arclith_cli-0.22.0}/tests/test_project_paths.py +0 -0
- {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.
|
|
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.
|
|
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`
|
|
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**.
|
|
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. **
|
|
381
|
-
2. **
|
|
382
|
-
3. **
|
|
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
|
-
|
|
401
|
-
|
|
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` |
|
|
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`
|
|
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**.
|
|
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. **
|
|
187
|
-
2. **
|
|
188
|
-
3. **
|
|
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
|
-
|
|
207
|
-
|
|
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` |
|
|
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()))
|