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

@@ -662,12 +662,14 @@
<span class="normal"><a href="#__codelineno-0-3">3</a></span>
<span class="normal"><a href="#__codelineno-0-4">4</a></span>
<span class="normal"><a href="#__codelineno-0-5">5</a></span>
<span class="normal"><a href="#__codelineno-0-6">6</a></span></pre></div></td><td class="code"><div><pre><span></span><code><span id="__span-0-1"><a id="__codelineno-0-1" name="__codelineno-0-1"></a>interfaces/* -&gt; domain/* (services, ports, models)
<span class="normal"><a href="#__codelineno-0-6">6</a></span>
<span class="normal"><a href="#__codelineno-0-7">7</a></span></pre></div></td><td class="code"><div><pre><span></span><code><span id="__span-0-1"><a id="__codelineno-0-1" name="__codelineno-0-1"></a>interfaces/* -&gt; domain/* (services, ports, models)
</span><span id="__span-0-2"><a id="__codelineno-0-2" name="__codelineno-0-2"></a>interfaces/* -&gt; infra/adapters/* (only read-only value objects)
</span><span id="__span-0-3"><a id="__codelineno-0-3" name="__codelineno-0-3"></a>domain/* -&gt; domain/* (same layer)
</span><span id="__span-0-4"><a id="__codelineno-0-4" name="__codelineno-0-4"></a>domain/* -&gt; pipelines/* (call a pipeline via a port)
</span><span id="__span-0-5"><a id="__codelineno-0-5" name="__codelineno-0-5"></a>infra/adapters/* -&gt; domain/ports/* (implement the interface)
</span><span id="__span-0-6"><a id="__codelineno-0-6" name="__codelineno-0-6"></a>main.py/init.py -&gt; everything (composition root only)
</span><span id="__span-0-5"><a id="__codelineno-0-5" name="__codelineno-0-5"></a>domain/*/ports/* -&gt; infra/* (bind adapters + libraries at the seam)
</span><span id="__span-0-6"><a id="__codelineno-0-6" name="__codelineno-0-6"></a>infra/adapters/* -&gt; domain/ports/* (implement the interface)
</span><span id="__span-0-7"><a id="__codelineno-0-7" name="__codelineno-0-7"></a>main.py/init.py -&gt; everything (composition root only)
</span></code></pre></div></td></tr></table></div>
<h2 id="forbidden-imports">Forbidden imports</h2>
<div class="language-text highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal"><a href="#__codelineno-1-1">1</a></span>
@@ -680,27 +682,32 @@
</span></code></pre></div></td></tr></table></div>
<h2 id="why">Why</h2>
<ul>
<li>A pure domain is testable without a server, a database, or an event loop.</li>
<li>The business logic is testable without a server or an event loop.</li>
<li>Adapters can be swapped (in-memory fake, real database) without touching
business logic.</li>
<li>The HTTP edge stays thin and replaceable.</li>
</ul>
<h2 id="the-portadapter-contract">The port/adapter contract</h2>
<p>A <strong>port</strong> is an abstract class or protocol declared in <code>domain/&lt;x&gt;/ports/</code>.
An <strong>adapter</strong> implements it in <code>infra/adapters/</code>. The composition root binds
the two.</p>
<p>The <code>ports/</code> package is the <strong>seam</strong> where the domain meets infrastructure.
Each <code>domain/&lt;resource&gt;/ports/</code> module declares the resource's models/document
shape and exposes <strong>factory functions</strong> that bind a concrete adapter — so a port
module <em>is</em> allowed to import from <code>infra/</code> and from the persistence libraries.
Everything outside <code>ports/</code> in <code>domain/</code> stays framework- and driver-free.</p>
<div class="language-text highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal"><a href="#__codelineno-2-1">1</a></span>
<span class="normal"><a href="#__codelineno-2-2">2</a></span>
<span class="normal"><a href="#__codelineno-2-3">3</a></span>
<span class="normal"><a href="#__codelineno-2-4">4</a></span></pre></div></td><td class="code"><div><pre><span></span><code><span id="__span-2-1"><a id="__codelineno-2-1" name="__codelineno-2-1"></a>domain/&lt;resource&gt;/ports/db.py -&gt; repository port + document model
</span><span id="__span-2-2"><a id="__codelineno-2-2" name="__codelineno-2-2"></a>domain/&lt;resource&gt;/ports/service.py -&gt; service + factory
<span class="normal"><a href="#__codelineno-2-4">4</a></span></pre></div></td><td class="code"><div><pre><span></span><code><span id="__span-2-1"><a id="__codelineno-2-1" name="__codelineno-2-1"></a>domain/&lt;resource&gt;/ports/db.py -&gt; document model + create_&lt;resource&gt;_repo()
</span><span id="__span-2-2"><a id="__codelineno-2-2" name="__codelineno-2-2"></a>domain/&lt;resource&gt;/ports/service.py -&gt; service + get_&lt;resource&gt;_service()
</span><span id="__span-2-3"><a id="__codelineno-2-3" name="__codelineno-2-3"></a>infra/db/repos/&lt;vendor&gt;.py -&gt; repository adapter factory
</span><span id="__span-2-4"><a id="__codelineno-2-4" name="__codelineno-2-4"></a>infra/adapters/service.py -&gt; generic CRUD service base
</span></code></pre></div></td></tr></table></div>
<p>The composition root calls the port factories; nothing else constructs an
adapter.</p>
<h2 id="enforcement">Enforcement</h2>
<ul>
<li>Keep every domain module importable with only the standard library and
Pydantic. If a domain import pulls in <code>fastapi</code>, the layering is broken.</li>
<li><code>domain/</code> must never import the web framework (<code>fastapi</code> / <code>starlette</code>) or
<code>interfaces/</code>. Only the <code>ports/</code> seam, <code>infra/</code>, and the composition root
touch drivers.</li>
<li>Reviewers (and CI) reject <code>import fastapi</code> inside <code>domain/</code> or <code>infra/</code>.</li>
</ul>

View File

@@ -651,7 +651,7 @@ registry</strong> and exposes it to the HTTP layer and to scripts.</p>
<li><code>progress_bus</code> — progress/event bus.</li>
<li><code>&lt;vendor&gt;_&lt;kind&gt;</code> — fetchers, clients, stores.</li>
</ul>
<p>The API stores the dict on <code>app.state.resources</code> during lifespan startup.
<p>The API stores the dict on <code>app.state.db_resources</code> during lifespan startup.
Handlers read from it; nothing imports concrete adapters.</p>
<h2 id="shape">Shape</h2>
<div class="language-python highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal"><a href="#__codelineno-0-1"> 1</a></span>
@@ -747,7 +747,7 @@ typing without importing concrete classes:</p>
<ul>
<li><code>init()</code> is <strong>idempotent</strong>.</li>
<li>The composition root is the only place that imports concrete adapters.</li>
<li>Handlers never call <code>init()</code>; they read <code>request.app.state.resources</code>.</li>
<li>Handlers never call <code>init()</code>; they read <code>request.app.state.db_resources</code>.</li>
<li>Scripts call <code>init()</code> / <code>shutdown()</code> around their work.</li>
</ul>

View File

@@ -711,7 +711,7 @@ Handlers are bound to it by name; there is no decorator router.</p>
<span class="normal"><a href="#__codelineno-1-4">4</a></span>
<span class="normal"><a href="#__codelineno-1-5">5</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="nd">@asynccontextmanager</span>
</span><span id="__span-1-2"><a id="__codelineno-1-2" name="__codelineno-1-2"></a><span class="k">async</span> <span class="k">def</span><span class="w"> </span><span class="nf">lifespan</span><span class="p">(</span><span class="n">app</span><span class="p">):</span>
</span><span id="__span-1-3"><a id="__codelineno-1-3" name="__codelineno-1-3"></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="k">await</span> <span class="n">init</span><span class="p">()</span>
</span><span id="__span-1-3"><a id="__codelineno-1-3" name="__codelineno-1-3"></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="k">await</span> <span class="n">init</span><span class="p">()</span>
</span><span id="__span-1-4"><a id="__codelineno-1-4" name="__codelineno-1-4"></a> <span class="k">yield</span>
</span><span id="__span-1-5"><a id="__codelineno-1-5" name="__codelineno-1-5"></a> <span class="k">await</span> <span class="n">shutdown</span><span class="p">()</span>
</span></code></pre></div></td></tr></table></div>
@@ -729,7 +729,7 @@ can resolve <code>operationId</code> across the whole app:</p>
<div class="language-python highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal"><a href="#__codelineno-3-1">1</a></span>
<span class="normal"><a href="#__codelineno-3-2">2</a></span>
<span class="normal"><a href="#__codelineno-3-3">3</a></span></pre></div></td><td class="code"><div><pre><span></span><code><span id="__span-3-1"><a id="__codelineno-3-1" name="__codelineno-3-1"></a><span class="k">async</span> <span class="k">def</span><span class="w"> </span><span class="nf">create_item</span><span class="p">(</span><span class="n">request</span><span class="p">:</span> <span class="n">Request</span><span class="p">,</span> <span class="n">body</span><span class="p">:</span> <span class="nb">dict</span><span class="p">):</span>
</span><span id="__span-3-2"><a id="__codelineno-3-2" name="__codelineno-3-2"></a> <span class="n">svc</span> <span class="o">=</span> <span class="n">request</span><span class="o">.</span><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="p">[</span><span class="s2">&quot;item_svc&quot;</span><span class="p">]</span>
</span><span id="__span-3-2"><a id="__codelineno-3-2" name="__codelineno-3-2"></a> <span class="n">svc</span> <span class="o">=</span> <span class="n">request</span><span class="o">.</span><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="p">[</span><span class="s2">&quot;item_svc&quot;</span><span class="p">]</span>
</span><span id="__span-3-3"><a id="__codelineno-3-3" name="__codelineno-3-3"></a> <span class="k">return</span> <span class="k">await</span> <span class="n">svc</span><span class="o">.</span><span class="n">create</span><span class="p">(</span><span class="n">Item</span><span class="o">.</span><span class="n">model_validate</span><span class="p">(</span><span class="n">body</span><span class="p">))</span>
</span></code></pre></div></td></tr></table></div>
<h2 id="conventions">Conventions</h2>

View File

@@ -729,7 +729,7 @@ swapped for a durable broker. Emit from the orchestrator at each stage:</p>
<span class="normal"><a href="#__codelineno-4-13">13</a></span>
<span class="normal"><a href="#__codelineno-4-14">14</a></span>
<span class="normal"><a href="#__codelineno-4-15">15</a></span></pre></div></td><td class="code"><div><pre><span></span><code><span id="__span-4-1"><a id="__codelineno-4-1" name="__codelineno-4-1"></a><span class="k">async</span> <span class="k">def</span><span class="w"> </span><span class="nf">stream_job_events</span><span class="p">(</span><span class="n">request</span><span class="p">,</span> <span class="nb">id</span><span class="p">):</span>
</span><span id="__span-4-2"><a id="__codelineno-4-2" name="__codelineno-4-2"></a> <span class="n">bus</span> <span class="o">=</span> <span class="n">request</span><span class="o">.</span><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="n">get</span><span class="p">(</span><span class="s2">&quot;progress_bus&quot;</span><span class="p">)</span>
</span><span id="__span-4-2"><a id="__codelineno-4-2" name="__codelineno-4-2"></a> <span class="n">bus</span> <span class="o">=</span> <span class="n">request</span><span class="o">.</span><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="n">get</span><span class="p">(</span><span class="s2">&quot;progress_bus&quot;</span><span class="p">)</span>
</span><span id="__span-4-3"><a id="__codelineno-4-3" name="__codelineno-4-3"></a> <span class="k">if</span> <span class="n">bus</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
</span><span id="__span-4-4"><a id="__codelineno-4-4" name="__codelineno-4-4"></a> <span class="k">return</span> <span class="p">{</span><span class="s2">&quot;events&quot;</span><span class="p">:</span> <span class="p">[]}</span>
</span><span id="__span-4-5"><a id="__codelineno-4-5" name="__codelineno-4-5"></a>

View File

@@ -734,7 +734,7 @@ injects it into the pipeline so that stage can skip re-raising:</p>
<span class="normal"><a href="#__codelineno-4-12">12</a></span>
<span class="normal"><a href="#__codelineno-4-13">13</a></span>
<span class="normal"><a href="#__codelineno-4-14">14</a></span></pre></div></td><td class="code"><div><pre><span></span><code><span id="__span-4-1"><a id="__codelineno-4-1" name="__codelineno-4-1"></a><span class="k">async</span> <span class="k">def</span><span class="w"> </span><span class="nf">resolve_question</span><span class="p">(</span><span class="n">request</span><span class="p">,</span> <span class="nb">id</span><span class="p">,</span> <span class="n">body</span><span class="p">):</span>
</span><span id="__span-4-2"><a id="__codelineno-4-2" name="__codelineno-4-2"></a> <span class="n">svc</span> <span class="o">=</span> <span class="n">request</span><span class="o">.</span><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="p">[</span><span class="s2">&quot;question_svc&quot;</span><span class="p">]</span>
</span><span id="__span-4-2"><a id="__codelineno-4-2" name="__codelineno-4-2"></a> <span class="n">svc</span> <span class="o">=</span> <span class="n">request</span><span class="o">.</span><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="p">[</span><span class="s2">&quot;question_svc&quot;</span><span class="p">]</span>
</span><span id="__span-4-3"><a id="__codelineno-4-3" name="__codelineno-4-3"></a> <span class="n">q</span> <span class="o">=</span> <span class="k">await</span> <span class="n">svc</span><span class="o">.</span><span class="n">read</span><span class="p">(</span><span class="nb">id</span><span class="p">)</span>
</span><span id="__span-4-4"><a id="__codelineno-4-4" name="__codelineno-4-4"></a> <span class="k">await</span> <span class="n">svc</span><span class="o">.</span><span class="n">update</span><span class="p">(</span><span class="nb">id</span><span class="p">,</span> <span class="p">{</span><span class="s2">&quot;chosen&quot;</span><span class="p">:</span> <span class="n">body</span><span class="p">[</span><span class="s2">&quot;chosen&quot;</span><span class="p">],</span> <span class="s2">&quot;status&quot;</span><span class="p">:</span> <span class="s2">&quot;resolved&quot;</span><span class="p">,</span>
</span><span id="__span-4-5"><a id="__codelineno-4-5" name="__codelineno-4-5"></a> <span class="s2">&quot;resolved_at&quot;</span><span class="p">:</span> <span class="n">now</span><span class="p">()})</span>
@@ -742,7 +742,7 @@ injects it into the pipeline so that stage can skip re-raising:</p>
</span><span id="__span-4-7"><a id="__codelineno-4-7" name="__codelineno-4-7"></a> <span class="n">pending</span> <span class="o">=</span> <span class="k">await</span> <span class="n">svc</span><span class="o">.</span><span class="n">search</span><span class="p">(</span><span class="nb">filter</span><span class="o">=</span><span class="p">{</span><span class="s2">&quot;job_id&quot;</span><span class="p">:</span> <span class="n">q</span><span class="o">.</span><span class="n">job_id</span><span class="p">,</span> <span class="s2">&quot;status&quot;</span><span class="p">:</span> <span class="s2">&quot;pending&quot;</span><span class="p">},</span>
</span><span id="__span-4-8"><a id="__codelineno-4-8" name="__codelineno-4-8"></a> <span class="n">limit</span><span class="o">=</span><span class="mi">1</span><span class="p">)</span>
</span><span id="__span-4-9"><a id="__codelineno-4-9" name="__codelineno-4-9"></a> <span class="k">if</span> <span class="ow">not</span> <span class="n">pending</span><span class="p">:</span>
</span><span id="__span-4-10"><a id="__codelineno-4-10" name="__codelineno-4-10"></a> <span class="n">jobs</span> <span class="o">=</span> <span class="n">request</span><span class="o">.</span><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="p">[</span><span class="s2">&quot;job_svc&quot;</span><span class="p">]</span>
</span><span id="__span-4-10"><a id="__codelineno-4-10" name="__codelineno-4-10"></a> <span class="n">jobs</span> <span class="o">=</span> <span class="n">request</span><span class="o">.</span><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="p">[</span><span class="s2">&quot;job_svc&quot;</span><span class="p">]</span>
</span><span id="__span-4-11"><a id="__codelineno-4-11" name="__codelineno-4-11"></a> <span class="n">job</span> <span class="o">=</span> <span class="k">await</span> <span class="n">jobs</span><span class="o">.</span><span class="n">read</span><span class="p">(</span><span class="n">q</span><span class="o">.</span><span class="n">job_id</span><span class="p">)</span>
</span><span id="__span-4-12"><a id="__codelineno-4-12" name="__codelineno-4-12"></a> <span class="k">if</span> <span class="n">job</span> <span class="ow">and</span> <span class="n">job</span><span class="o">.</span><span class="n">status</span> <span class="o">==</span> <span class="s2">&quot;paused&quot;</span><span class="p">:</span>
</span><span id="__span-4-13"><a id="__codelineno-4-13" name="__codelineno-4-13"></a> <span class="k">await</span> <span class="n">jobs</span><span class="o">.</span><span class="n">update_status</span><span class="p">(</span><span class="n">q</span><span class="o">.</span><span class="n">job_id</span><span class="p">,</span> <span class="s2">&quot;pending&quot;</span><span class="p">)</span>

File diff suppressed because one or more lines are too long