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

@@ -553,9 +553,9 @@ live in the reference pages.</p>
RT["&lt;resource&gt;/routes.py&lt;br/&gt;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 --&gt; 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 -&gt; domain -&gt; 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

View File

@@ -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&lt;br/&gt;operationId"]) --&gt; RT["interfaces/&amp;lt;resource&amp;gt;/routes.py"]
RT --&gt; SVC["&amp;lt;resource&amp;gt;_svc"]
SVC --&gt; REPO["&amp;lt;resource&amp;gt;_repo"]
end
subgraph CORE["domain/ — business core"]
RI["&lt;resource&gt; · models · ports · services"]
JI["&lt;job/worker&gt; · models · ports · services"]
PI["pipelines/&lt;family&gt;"]
subgraph WRK["Background path — worker process"]
direction LR
W["worker.py"] --&gt; JSVC["job_svc"]
W --&gt; PIPE["pipelines/&amp;lt;family&amp;gt;"]
PIPE --&gt; ISVC["item_svc"]
JSVC --&gt; JREPO["job_repo"]
ISVC --&gt; 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 --&gt; RU
OAS --&gt; JU
RU --&gt; RI
JU --&gt; JI
JI --&gt; PI
PI --&gt; DB
PI --&gt; LL
RI --&gt; CA
RI --&gt; DB
JI --&gt; ST
W --&gt; JI
W --&gt; PI
CR === RU
CR === JU
CR === RI
CR === JI
CR === PI
CR === W
CR === DB
CR === CA
CR === ST</code></pre>
REPO --&gt; MONGO
JREPO --&gt; MONGO
IREPO --&gt; 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/*&lt;br/&gt;factory seams"]
REPOS["repos&lt;br/&gt;infra/db/repos/mongo.py"]
SVCES["services&lt;br/&gt;CrudsServiceAdaptor"]
STATE["app.state.db_resources"]
HEXA["hexa.build"]
RT["routes.py"]
WK["worker.py"]
INIT --&gt; SETUP
INIT --&gt; FACT
FACT --&gt; REPOS
FACT --&gt; SVCES
REPOS --&gt; SVCES
HEXA -.-&gt; SVCES
INIT --&gt; STATE
STATE --&gt; RT
INIT --&gt; 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>&lt;aggregate&gt;/models.py</code> — dataclasses, enums, document shapes.</li>
<li><code>&lt;aggregate&gt;/ports/db.py</code> — repo contract; <code>&lt;aggregate&gt;/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>&lt;aggregate&gt;/ports/db.py</code> — document model + <code>create_&lt;aggregate&gt;_repo()</code>.</li>
<li><code>&lt;aggregate&gt;/ports/service.py</code> — service + <code>get_&lt;aggregate&gt;_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

View File

@@ -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">&quot;item_svc&quot;</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">&quot;item_svc&quot;</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">&quot;/items&quot;</span><span class="p">,</span> <span class="n">json</span><span class="o">=</span><span class="p">{</span><span class="s2">&quot;name&quot;</span><span class="p">:</span> <span class="s2">&quot;x&quot;</span><span class="p">})</span>

View File

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