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

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