docs(standards): align layering docs with the port seam; split runtime vs wiring diagrams

This commit is contained in:
2026-10-12 13:15:35 +05:30
parent 8a10c4768c
commit 7d1694148a
25 changed files with 230 additions and 159 deletions

View File

@@ -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

View File

@@ -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/&lt;resource&gt;/routes.py"]
RT --> SVC["&lt;resource&gt;_svc"]
SVC --> REPO["&lt;resource&gt;_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/&lt;family&gt;"]
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

View File

@@ -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"})

View File

@@ -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