docs(standards): align layering docs with the port seam; split runtime vs wiring diagrams
This commit is contained in:
@@ -553,9 +553,9 @@ live in the reference pages.</p>
|
||||
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
|
||||
|
||||
@@ -578,11 +578,11 @@ live in the reference pages.</p>
|
||||
RP --> DB
|
||||
AD --- CR</code></pre>
|
||||
<p>The rule holds at every edge: code in an outer ring may depend on a ring closer
|
||||
to the core, never the reverse. <code>domain/</code> never imports a web framework or a
|
||||
database driver.</p>
|
||||
to the core, never the reverse. <code>domain/</code> never imports a web framework;
|
||||
only its <code>ports/</code> seam reaches into <code>infra/</code> to bind adapters.</p>
|
||||
<blockquote>
|
||||
<p><strong>Legend</strong> — solid arrows are compile-time calls; dotted arrows are the
|
||||
port/adapter contract, resolved by the composition root at startup.</p>
|
||||
port/adapter binding, performed by the composition root at startup.</p>
|
||||
</blockquote>
|
||||
<hr />
|
||||
<h2 id="the-one-way-dependency-rule">🔄 The one-way dependency rule</h2>
|
||||
@@ -602,8 +602,8 @@ port/adapter contract, resolved by the composition root at startup.</p>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>domain/</code></td>
|
||||
<td>nothing but its own ports</td>
|
||||
<td>web frameworks, drivers, <code>infra</code></td>
|
||||
<td>its own models + its <code>ports/</code> seam (which binds <code>infra</code>/libraries)</td>
|
||||
<td>web frameworks, <code>interfaces/</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>infra/</code></td>
|
||||
@@ -622,8 +622,9 @@ port/adapter contract, resolved by the composition root at startup.</p>
|
||||
<ol>
|
||||
<li><strong>One-way dependencies.</strong> <code>interfaces -> domain -> infra</code> is allowed; the
|
||||
reverse is forbidden.</li>
|
||||
<li><strong>Ports in the domain, adapters in infra.</strong> A service depends on an abstract
|
||||
port; the composition root injects a concrete adapter.</li>
|
||||
<li><strong>Ports in the domain, adapters in infra.</strong> A service depends on a port; the
|
||||
port's factory binds the concrete adapter, and the composition root calls
|
||||
those factories.</li>
|
||||
<li><strong>One composition root.</strong> Nothing constructs its own dependencies. All
|
||||
wiring lives in one module (<code>init.py</code>) exposed as a flat resource registry.</li>
|
||||
<li><strong>Spec-first.</strong> The HTTP contract is declared in <code>openapi.yaml</code>; route
|
||||
|
||||
@@ -433,6 +433,30 @@
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<nav class="md-nav" aria-label="🗂️ Component model">
|
||||
<ul class="md-nav__list">
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#runtime-flow" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
🔀 Runtime flow
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#composition-root-wiring" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
🧵 Composition-root wiring
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
@@ -595,55 +619,66 @@
|
||||
blueprint's component model — <code>templates/app/</code> is a runnable instance of it.</p>
|
||||
<hr />
|
||||
<h2 id="component-model">🗂️ Component model</h2>
|
||||
<pre class="mermaid"><code>%%{init: {"flowchart": {"nodeSpacing": 55, "rankSpacing": 120}}}%%
|
||||
<p>The blueprint has two orthogonal views: <strong>runtime flow</strong> (who calls whom) and
|
||||
<strong>composition-root wiring</strong> (who constructs whom). Keeping them separate makes
|
||||
the layering visible instead of hiding it behind a hub.</p>
|
||||
<h3 id="runtime-flow">🔀 Runtime flow</h3>
|
||||
<p>Who calls whom at request and job time:</p>
|
||||
<pre class="mermaid"><code>%%{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</code></pre>
|
||||
REPO --> MONGO
|
||||
JREPO --> MONGO
|
||||
IREPO --> MONGO</code></pre>
|
||||
<blockquote>
|
||||
<p><strong>Legend</strong> — solid arrows are direct calls; thick links are wires created by
|
||||
the composition root. Every component is injected, never self-constructed.</p>
|
||||
<p><strong>Legend</strong> — solid arrows are runtime calls. Nothing is self-constructed.</p>
|
||||
</blockquote>
|
||||
<h3 id="composition-root-wiring">🧵 Composition-root wiring</h3>
|
||||
<p>Who constructs what at startup:</p>
|
||||
<pre class="mermaid"><code>%%{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</code></pre>
|
||||
<blockquote>
|
||||
<p><strong>Legend</strong> — <code>init.py</code> is the only place that imports concrete adapters. The
|
||||
<code>ports/</code> factory seams bind <code>infra</code>; the API reads resources from
|
||||
<code>app.state.db_resources</code>, the worker calls <code>init()</code> directly.</p>
|
||||
</blockquote>
|
||||
<hr />
|
||||
<h2 id="component-roles">📦 Component roles</h2>
|
||||
@@ -656,14 +691,15 @@ call a service, and shape the response. No business rules, no I/O.</p>
|
||||
<li>Routes may mount the <code>cache</code> decorator on read endpoints.</li>
|
||||
</ul>
|
||||
<h3 id="2-domain-the-core">2. <code>domain/</code> — the core</h3>
|
||||
<p>Pure models, <strong>ports</strong> (abstract contracts), and services (use cases). No
|
||||
framework imports, no drivers.</p>
|
||||
<p>Models, ports, and services (use cases). No framework imports. The <code>ports/</code>
|
||||
package is the one seam allowed to reach into <code>infra/</code>: each port module declares
|
||||
the document shape and a factory that binds the adapter.</p>
|
||||
<ul>
|
||||
<li><code><aggregate>/models.py</code> — dataclasses, enums, document shapes.</li>
|
||||
<li><code><aggregate>/ports/db.py</code> — repo contract; <code><aggregate>/ports/service.py</code> —
|
||||
service contract.</li>
|
||||
<li>Services compose ports and enforce the business rules; the composition root
|
||||
injects the implementations.</li>
|
||||
<li><code><aggregate>/ports/db.py</code> — document model + <code>create_<aggregate>_repo()</code>.</li>
|
||||
<li><code><aggregate>/ports/service.py</code> — service + <code>get_<aggregate>_service(repo)</code>.</li>
|
||||
<li>Services compose ports and enforce the business rules; the port factories bind
|
||||
the implementations, and the composition root calls them.</li>
|
||||
</ul>
|
||||
<h3 id="3-infra-the-adapters">3. <code>infra/</code> — the adapters</h3>
|
||||
<p>Concrete implementations of the domain ports plus cross-cutting infrastructure:</p>
|
||||
@@ -689,7 +725,7 @@ stops resources on the app lifespan.</p>
|
||||
<pre class="mermaid"><code>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
|
||||
|
||||
|
||||
@@ -573,7 +573,7 @@ blueprint app only when every gate below passes.</p>
|
||||
<h2 id="integration-tests">🚀 Integration tests</h2>
|
||||
<ul>
|
||||
<li>Build the app with <code>TestClient</code>.</li>
|
||||
<li>Replace <code>app.state.resources</code> with fake repositories/services so no database
|
||||
<li>Replace <code>app.state.db_resources</code> with fake repositories/services so no database
|
||||
is required.</li>
|
||||
<li>Patch the auth introspection call (<code>httpx.AsyncClient.post</code>) to return a
|
||||
canned identity.</li>
|
||||
@@ -586,7 +586,7 @@ blueprint app only when every gate below passes.</p>
|
||||
<span class="normal"><a href="#__codelineno-1-4">4</a></span>
|
||||
<span class="normal"><a href="#__codelineno-1-5">5</a></span>
|
||||
<span class="normal"><a href="#__codelineno-1-6">6</a></span></pre></div></td><td class="code"><div><pre><span></span><code><span id="__span-1-1"><a id="__codelineno-1-1" name="__codelineno-1-1"></a><span class="n">client</span> <span class="o">=</span> <span class="n">TestClient</span><span class="p">(</span><span class="n">app</span><span class="p">)</span>
|
||||
</span><span id="__span-1-2"><a id="__codelineno-1-2" name="__codelineno-1-2"></a><span class="n">app</span><span class="o">.</span><span class="n">state</span><span class="o">.</span><span class="n">resources</span> <span class="o">=</span> <span class="p">{</span><span class="s2">"item_svc"</span><span class="p">:</span> <span class="n">FakeItemService</span><span class="p">(),</span> <span class="o">...</span><span class="p">}</span>
|
||||
</span><span id="__span-1-2"><a id="__codelineno-1-2" name="__codelineno-1-2"></a><span class="n">app</span><span class="o">.</span><span class="n">state</span><span class="o">.</span><span class="n">db_resources</span> <span class="o">=</span> <span class="p">{</span><span class="s2">"item_svc"</span><span class="p">:</span> <span class="n">FakeItemService</span><span class="p">(),</span> <span class="o">...</span><span class="p">}</span>
|
||||
</span><span id="__span-1-3"><a id="__codelineno-1-3" name="__codelineno-1-3"></a>
|
||||
</span><span id="__span-1-4"><a id="__codelineno-1-4" name="__codelineno-1-4"></a><span class="k">def</span><span class="w"> </span><span class="nf">test_create_item</span><span class="p">():</span>
|
||||
</span><span id="__span-1-5"><a id="__codelineno-1-5" name="__codelineno-1-5"></a> <span class="n">r</span> <span class="o">=</span> <span class="n">client</span><span class="o">.</span><span class="n">post</span><span class="p">(</span><span class="s2">"/items"</span><span class="p">,</span> <span class="n">json</span><span class="o">=</span><span class="p">{</span><span class="s2">"name"</span><span class="p">:</span> <span class="s2">"x"</span><span class="p">})</span>
|
||||
|
||||
@@ -521,8 +521,8 @@
|
||||
|
||||
<h1 id="standards-the-aetos-app-blueprint">🧩 Standards — the Aetos App Blueprint</h1>
|
||||
<p>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.</p>
|
||||
a spec-first HTTP edge, a framework-free domain core, a pluggable infrastructure
|
||||
layer, and a single composition root.</p>
|
||||
<p>Fill in <strong>two axes only</strong> and a new app is done: your <strong>resource model</strong>, and
|
||||
(if needed) your <strong>pipeline family</strong>. The blueprint itself never names a
|
||||
business domain — expenses, health metrics, inventory, and analytics are all
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user