docs(standards): align layering docs with the port seam; split runtime vs wiring diagrams
This commit is contained in:
@@ -17,9 +17,9 @@ flowchart LR
|
||||
RT["<resource>/routes.py<br/>thin handlers"]
|
||||
end
|
||||
|
||||
subgraph CORE["domain/ — pure business core"]
|
||||
subgraph CORE["domain/ — business core"]
|
||||
MD["models"]
|
||||
PT["ports (abstract)"]
|
||||
PT["ports (adapter binding seam)"]
|
||||
SV["services"]
|
||||
end
|
||||
|
||||
@@ -44,11 +44,11 @@ flowchart LR
|
||||
```
|
||||
|
||||
The rule holds at every edge: code in an outer ring may depend on a ring closer
|
||||
to the core, never the reverse. `domain/` never imports a web framework or a
|
||||
database driver.
|
||||
to the core, never the reverse. `domain/` never imports a web framework;
|
||||
only its `ports/` seam reaches into `infra/` to bind adapters.
|
||||
|
||||
> **Legend** — solid arrows are compile-time calls; dotted arrows are the
|
||||
> port/adapter contract, resolved by the composition root at startup.
|
||||
> port/adapter binding, performed by the composition root at startup.
|
||||
|
||||
---
|
||||
|
||||
@@ -57,7 +57,7 @@ database driver.
|
||||
| Ring | May import | Never imports |
|
||||
|-------------|------------------------------------|----------------------------------|
|
||||
| `interfaces/` | `domain`, framework glue (glue only) | business logic, `infra` directly |
|
||||
| `domain/` | nothing but its own ports | web frameworks, drivers, `infra` |
|
||||
| `domain/` | its own models + its `ports/` seam (which binds `infra`/libraries) | web frameworks, `interfaces/` |
|
||||
| `infra/` | `domain` ports, drivers, libraries | — |
|
||||
| `init.py` | everything (the wiring point) | — |
|
||||
|
||||
@@ -67,8 +67,9 @@ database driver.
|
||||
|
||||
1. **One-way dependencies.** `interfaces -> domain -> infra` is allowed; the
|
||||
reverse is forbidden.
|
||||
2. **Ports in the domain, adapters in infra.** A service depends on an abstract
|
||||
port; the composition root injects a concrete adapter.
|
||||
2. **Ports in the domain, adapters in infra.** A service depends on a port; the
|
||||
port's factory binds the concrete adapter, and the composition root calls
|
||||
those factories.
|
||||
3. **One composition root.** Nothing constructs its own dependencies. All
|
||||
wiring lives in one module (`init.py`) exposed as a flat resource registry.
|
||||
4. **Spec-first.** The HTTP contract is declared in `openapi.yaml`; route
|
||||
|
||||
@@ -7,57 +7,75 @@ blueprint's component model — `templates/app/` is a runnable instance of it.
|
||||
|
||||
## 🗂️ Component model
|
||||
|
||||
The blueprint has two orthogonal views: **runtime flow** (who calls whom) and
|
||||
**composition-root wiring** (who constructs whom). Keeping them separate makes
|
||||
the layering visible instead of hiding it behind a hub.
|
||||
|
||||
### 🔀 Runtime flow
|
||||
|
||||
Who calls whom at request and job time:
|
||||
|
||||
```mermaid
|
||||
%%{init: {"flowchart": {"nodeSpacing": 55, "rankSpacing": 120}}}%%
|
||||
%%{init: {"flowchart": {"nodeSpacing": 50, "rankSpacing": 80}}}%%
|
||||
flowchart TB
|
||||
subgraph EDGE["interfaces/ — HTTP edge"]
|
||||
OAS["openapi.yaml"]
|
||||
RU["resource routes"]
|
||||
JU["job routes"]
|
||||
subgraph REQ["Request path — API process"]
|
||||
direction LR
|
||||
CL(["Client<br/>operationId"]) --> RT["interfaces/<resource>/routes.py"]
|
||||
RT --> SVC["<resource>_svc"]
|
||||
SVC --> REPO["<resource>_repo"]
|
||||
end
|
||||
|
||||
subgraph CORE["domain/ — business core"]
|
||||
RI["<resource> · models · ports · services"]
|
||||
JI["<job/worker> · models · ports · services"]
|
||||
PI["pipelines/<family>"]
|
||||
subgraph WRK["Background path — worker process"]
|
||||
direction LR
|
||||
W["worker.py"] --> JSVC["job_svc"]
|
||||
W --> PIPE["pipelines/<family>"]
|
||||
PIPE --> ISVC["item_svc"]
|
||||
JSVC --> JREPO["job_repo"]
|
||||
ISVC --> IREPO["item_repo"]
|
||||
end
|
||||
|
||||
subgraph INFRA["infra/ — adapters"]
|
||||
DB[("MongoDB repos")]
|
||||
CA["redis cache"]
|
||||
ST["stores · progress bus"]
|
||||
LL["llm clients"]
|
||||
MONGO[("MongoDB / mongo_ops")]
|
||||
end
|
||||
|
||||
W["worker.py"]
|
||||
CR["init.py — composition root"]
|
||||
|
||||
OAS --> RU
|
||||
OAS --> JU
|
||||
RU --> RI
|
||||
JU --> JI
|
||||
JI --> PI
|
||||
PI --> DB
|
||||
PI --> LL
|
||||
RI --> CA
|
||||
RI --> DB
|
||||
JI --> ST
|
||||
W --> JI
|
||||
W --> PI
|
||||
|
||||
CR === RU
|
||||
CR === JU
|
||||
CR === RI
|
||||
CR === JI
|
||||
CR === PI
|
||||
CR === W
|
||||
CR === DB
|
||||
CR === CA
|
||||
CR === ST
|
||||
REPO --> MONGO
|
||||
JREPO --> MONGO
|
||||
IREPO --> MONGO
|
||||
```
|
||||
|
||||
> **Legend** — solid arrows are direct calls; thick links are wires created by
|
||||
> the composition root. Every component is injected, never self-constructed.
|
||||
> **Legend** — solid arrows are runtime calls. Nothing is self-constructed.
|
||||
|
||||
### 🧵 Composition-root wiring
|
||||
|
||||
Who constructs what at startup:
|
||||
|
||||
```mermaid
|
||||
%%{init: {"flowchart": {"nodeSpacing": 50, "rankSpacing": 80}}}%%
|
||||
flowchart TB
|
||||
INIT["init.py — composition root"]
|
||||
SETUP["infra/db/setup.py"]
|
||||
FACT["domain/*/ports/*<br/>factory seams"]
|
||||
REPOS["repos<br/>infra/db/repos/mongo.py"]
|
||||
SVCES["services<br/>CrudsServiceAdaptor"]
|
||||
STATE["app.state.db_resources"]
|
||||
HEXA["hexa.build"]
|
||||
RT["routes.py"]
|
||||
WK["worker.py"]
|
||||
|
||||
INIT --> SETUP
|
||||
INIT --> FACT
|
||||
FACT --> REPOS
|
||||
FACT --> SVCES
|
||||
REPOS --> SVCES
|
||||
HEXA -.-> SVCES
|
||||
INIT --> STATE
|
||||
STATE --> RT
|
||||
INIT --> WK
|
||||
```
|
||||
|
||||
> **Legend** — `init.py` is the only place that imports concrete adapters. The
|
||||
> `ports/` factory seams bind `infra`; the API reads resources from
|
||||
> `app.state.db_resources`, the worker calls `init()` directly.
|
||||
|
||||
---
|
||||
|
||||
@@ -74,14 +92,15 @@ call a service, and shape the response. No business rules, no I/O.
|
||||
|
||||
### 2. `domain/` — the core
|
||||
|
||||
Pure models, **ports** (abstract contracts), and services (use cases). No
|
||||
framework imports, no drivers.
|
||||
Models, ports, and services (use cases). No framework imports. The `ports/`
|
||||
package is the one seam allowed to reach into `infra/`: each port module declares
|
||||
the document shape and a factory that binds the adapter.
|
||||
|
||||
- `<aggregate>/models.py` — dataclasses, enums, document shapes.
|
||||
- `<aggregate>/ports/db.py` — repo contract; `<aggregate>/ports/service.py` —
|
||||
service contract.
|
||||
- Services compose ports and enforce the business rules; the composition root
|
||||
injects the implementations.
|
||||
- `<aggregate>/ports/db.py` — document model + `create_<aggregate>_repo()`.
|
||||
- `<aggregate>/ports/service.py` — service + `get_<aggregate>_service(repo)`.
|
||||
- Services compose ports and enforce the business rules; the port factories bind
|
||||
the implementations, and the composition root calls them.
|
||||
|
||||
### 3. `infra/` — the adapters
|
||||
|
||||
@@ -117,7 +136,7 @@ stops resources on the app lifespan.
|
||||
sequenceDiagram
|
||||
participant C as Client
|
||||
participant R as route (interfaces)
|
||||
participant S as service (domain)
|
||||
participant S as service (port factory / adapter)
|
||||
participant P as repo (infra)
|
||||
participant DB as MongoDB
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ tests/
|
||||
## 🚀 Integration tests
|
||||
|
||||
- Build the app with `TestClient`.
|
||||
- Replace `app.state.resources` with fake repositories/services so no database
|
||||
- Replace `app.state.db_resources` with fake repositories/services so no database
|
||||
is required.
|
||||
- Patch the auth introspection call (`httpx.AsyncClient.post`) to return a
|
||||
canned identity.
|
||||
@@ -29,7 +29,7 @@ tests/
|
||||
|
||||
```python
|
||||
client = TestClient(app)
|
||||
app.state.resources = {"item_svc": FakeItemService(), ...}
|
||||
app.state.db_resources = {"item_svc": FakeItemService(), ...}
|
||||
|
||||
def test_create_item():
|
||||
r = client.post("/items", json={"name": "x"})
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# 🧩 Standards — the Aetos App Blueprint
|
||||
|
||||
One reusable architecture for every backend application in the Aetos ecosystem:
|
||||
a spec-first HTTP edge, a pure domain core, a pluggable infrastructure layer,
|
||||
and a single composition root.
|
||||
a spec-first HTTP edge, a framework-free domain core, a pluggable infrastructure
|
||||
layer, and a single composition root.
|
||||
|
||||
Fill in **two axes only** and a new app is done: your **resource model**, and
|
||||
(if needed) your **pipeline family**. The blueprint itself never names a
|
||||
|
||||
Reference in New Issue
Block a user