feat(standards): codify codegen, ship generic Tier-B infra, add get_app_template + prompts; fix docs:// regressions

P0: fix doc:// -> docs:// across standards src/server/registry and regenerate stale mongo-ops bundle. P1: build recipe + spec-first routing now document the openapi-first codegen fast path (models/routes with --inject-request/--package). P4: CRUD template documents the model-vs-document id rule (BaseDocument owns id). P5: authless guidance (omit security keys). P6: template gains generic infra (http_cache, keyed SSE bus, KV port+in-memory+Redis), interfaces/openapi.py + .dockerignore + generic build test registered as components, and a get_app_template tool; genericity matrix marks LLM pool + human-in-loop as reference-app patterns. P7: intake/build/start_app/trial prompts. P8: template ships tests/integration/test_openapi_app_builds.py. Rebuilt standards + openapi-first bundles and re-collected.
This commit is contained in:
2026-10-13 03:26:51 +05:30
parent ef951a1913
commit fced40af59
109 changed files with 4082 additions and 5885 deletions

View File

@@ -330,6 +330,15 @@
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#the-codegen-fast-path-in-detail" class="md-nav__link">
<span class="md-ellipsis">
🔧 The codegen fast path, in detail
</span>
</a>
</li>
<li class="md-nav__item">
@@ -638,6 +647,15 @@
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#the-codegen-fast-path-in-detail" class="md-nav__link">
<span class="md-ellipsis">
🔧 The codegen fast path, in detail
</span>
</a>
</li>
<li class="md-nav__item">
@@ -677,9 +695,9 @@
<h1 id="build-recipe">Build Recipe</h1>
<h2 id="summary">Summary</h2>
<p>The ordered, deterministic checklist for generating a new application of the
standard shape. Every step is fixed; only the resource model (step 2) and the
pipeline family (step 7) need app-specific authoring.</p>
<p>This page is served by the <code>standards</code> MCP as <code>doc://modules/build_recipe</code>. The
standard shape. Every step is fixed; only the resource model (step 2), the
codegen pass (step 3), and the pipeline family (step 8) need app-specific work.</p>
<p>This page is served by the <code>standards</code> MCP as <code>docs://modules/build_recipe</code>. The
<code>start_app</code> prompt on that server collects the two axes and hands off here; pull
exact files with <code>get_app_component</code>.</p>
<hr />
@@ -777,18 +795,34 @@ environment-only; never commit them.</p>
<span class="normal"><a href="#__codelineno-2-69">69</a></span>
<span class="normal"><a href="#__codelineno-2-70">70</a></span>
<span class="normal"><a href="#__codelineno-2-71">71</a></span>
<span class="normal"><a href="#__codelineno-2-72">72</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><span class="gh"># Build a new application from the Aetos Standards blueprint</span>
<span class="normal"><a href="#__codelineno-2-72">72</a></span>
<span class="normal"><a href="#__codelineno-2-73">73</a></span>
<span class="normal"><a href="#__codelineno-2-74">74</a></span>
<span class="normal"><a href="#__codelineno-2-75">75</a></span>
<span class="normal"><a href="#__codelineno-2-76">76</a></span>
<span class="normal"><a href="#__codelineno-2-77">77</a></span>
<span class="normal"><a href="#__codelineno-2-78">78</a></span>
<span class="normal"><a href="#__codelineno-2-79">79</a></span>
<span class="normal"><a href="#__codelineno-2-80">80</a></span>
<span class="normal"><a href="#__codelineno-2-81">81</a></span>
<span class="normal"><a href="#__codelineno-2-82">82</a></span>
<span class="normal"><a href="#__codelineno-2-83">83</a></span>
<span class="normal"><a href="#__codelineno-2-84">84</a></span>
<span class="normal"><a href="#__codelineno-2-85">85</a></span>
<span class="normal"><a href="#__codelineno-2-86">86</a></span>
<span class="normal"><a href="#__codelineno-2-87">87</a></span>
<span class="normal"><a href="#__codelineno-2-88">88</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><span class="gh"># Build a new application from the Aetos Standards blueprint</span>
</span><span id="__span-2-2"><a id="__codelineno-2-2" name="__codelineno-2-2"></a>
</span><span id="__span-2-3"><a id="__codelineno-2-3" name="__codelineno-2-3"></a>You are coding a complete new backend application conforming to the Aetos
</span><span id="__span-2-4"><a id="__codelineno-2-4" name="__codelineno-2-4"></a>standard app blueprint. Execute every step in order. Only steps 2 and 3 use the
</span><span id="__span-2-4"><a id="__codelineno-2-4" name="__codelineno-2-4"></a>standard app blueprint. Execute every step in order. Only steps 2–4 use the
</span><span id="__span-2-5"><a id="__codelineno-2-5" name="__codelineno-2-5"></a>app-specific details provided by the user; everything else is deterministic.
</span><span id="__span-2-6"><a id="__codelineno-2-6" name="__codelineno-2-6"></a>
</span><span id="__span-2-7"><a id="__codelineno-2-7" name="__codelineno-2-7"></a><span class="gu">## Before writing any code</span>
</span><span id="__span-2-8"><a id="__codelineno-2-8" name="__codelineno-2-8"></a>
</span><span id="__span-2-9"><a id="__codelineno-2-9" name="__codelineno-2-9"></a>Connect to the <span class="sb">`standards`</span> MCP server and read:
</span><span id="__span-2-10"><a id="__codelineno-2-10" name="__codelineno-2-10"></a><span class="k">-</span><span class="w"> </span><span class="sb">`doc://nav`</span> (the module list)
</span><span id="__span-2-11"><a id="__codelineno-2-11" name="__codelineno-2-11"></a><span class="k">-</span><span class="w"> </span><span class="sb">`doc://modules/genericity_matrix`</span> (Tier A library / Tier B template / Tier C authored)
</span><span id="__span-2-12"><a id="__codelineno-2-12" name="__codelineno-2-12"></a><span class="k">-</span><span class="w"> </span><span class="sb">`doc://modules/layering_rules`</span> (the dependency rules)
</span><span id="__span-2-10"><a id="__codelineno-2-10" name="__codelineno-2-10"></a><span class="k">-</span><span class="w"> </span><span class="sb">`docs://nav`</span> (the module list)
</span><span id="__span-2-11"><a id="__codelineno-2-11" name="__codelineno-2-11"></a><span class="k">-</span><span class="w"> </span><span class="sb">`docs://modules/genericity_matrix`</span> (Tier A library / Tier B template / Tier C authored)
</span><span id="__span-2-12"><a id="__codelineno-2-12" name="__codelineno-2-12"></a><span class="k">-</span><span class="w"> </span><span class="sb">`docs://modules/layering_rules`</span> (the dependency rules)
</span><span id="__span-2-13"><a id="__codelineno-2-13" name="__codelineno-2-13"></a>
</span><span id="__span-2-14"><a id="__codelineno-2-14" name="__codelineno-2-14"></a>Then:
</span><span id="__span-2-15"><a id="__codelineno-2-15" name="__codelineno-2-15"></a><span class="k">-</span><span class="w"> </span>call <span class="sb">`find_library`</span> with <span class="sb">`db`</span>, <span class="sb">`di`</span>, <span class="sb">`routing`</span>, <span class="sb">`ingestion`</span>, <span class="sb">`pipeline`</span>, <span class="sb">`docs`</span>
@@ -799,56 +833,72 @@ environment-only; never commit them.</p>
</span><span id="__span-2-20"><a id="__codelineno-2-20" name="__codelineno-2-20"></a>
</span><span id="__span-2-21"><a id="__codelineno-2-21" name="__codelineno-2-21"></a><span class="gu">## Steps</span>
</span><span id="__span-2-22"><a id="__codelineno-2-22" name="__codelineno-2-22"></a>
</span><span id="__span-2-23"><a id="__codelineno-2-23" name="__codelineno-2-23"></a><span class="k">1.</span> <span class="gs">**Scaffold**</span> — copy <span class="sb">`standards/src/templates/app/`</span> into <span class="sb">`apps/&lt;your-app&gt;/`</span>.
</span><span id="__span-2-24"><a id="__codelineno-2-24" name="__codelineno-2-24"></a> Keep the tree: <span class="sb">`domain/`</span>, <span class="sb">`infra/`</span>, <span class="sb">`interfaces/`</span>, <span class="sb">`pipelines/`</span>, <span class="sb">`tests/`</span>,
</span><span id="__span-2-25"><a id="__codelineno-2-25" name="__codelineno-2-25"></a> <span class="sb">`scripts/`</span>, plus <span class="sb">`pyproject.toml`</span>, <span class="sb">`openapi.yaml`</span>, <span class="sb">`config.py`</span>, <span class="sb">`init.py`</span>,
</span><span id="__span-2-26"><a id="__codelineno-2-26" name="__codelineno-2-26"></a> <span class="sb">`main.py`</span>, <span class="sb">`worker.py`</span>, <span class="sb">`Dockerfile`</span>, <span class="sb">`docker-compose.yaml`</span>, <span class="sb">`.env.example`</span>.
</span><span id="__span-2-27"><a id="__codelineno-2-27" name="__codelineno-2-27"></a>
</span><span id="__span-2-28"><a id="__codelineno-2-28" name="__codelineno-2-28"></a><span class="k">2.</span> <span class="gs">**Author the spec (app-specific)**</span> — write <span class="sb">`openapi.yaml`</span>: resources,
</span><span id="__span-2-29"><a id="__codelineno-2-29" name="__codelineno-2-29"></a> schemas, <span class="sb">`operationId`</span>s, security, and extension metadata. This is the
</span><span id="__span-2-30"><a id="__codelineno-2-30" name="__codelineno-2-30"></a> contract and drives all routing.
</span><span id="__span-2-31"><a id="__codelineno-2-31" name="__codelineno-2-31"></a>
</span><span id="__span-2-32"><a id="__codelineno-2-32" name="__codelineno-2-32"></a><span class="k">3.</span> <span class="gs">**Write each CRUD slice (app-specific)**</span> — per resource create the four-file
</span><span id="__span-2-33"><a id="__codelineno-2-33" name="__codelineno-2-33"></a> slice: <span class="sb">`models.py`</span>, <span class="sb">`ports/db.py`</span>, <span class="sb">`ports/service.py`</span>, and optional
</span><span id="__span-2-34"><a id="__codelineno-2-34" name="__codelineno-2-34"></a> <span class="sb">`converters.py`</span>. Follow the CRUD template.
</span><span id="__span-2-35"><a id="__codelineno-2-35" name="__codelineno-2-35"></a>
</span><span id="__span-2-36"><a id="__codelineno-2-36" name="__codelineno-2-36"></a><span class="k">4.</span> <span class="gs">**Wire the composition root**</span> — in <span class="sb">`init.py`</span>, build leaf repositories, then
</span><span id="__span-2-37"><a id="__codelineno-2-37" name="__codelineno-2-37"></a> dependent repositories, then services; add infra adapters (db, cache, stores,
</span><span id="__span-2-38"><a id="__codelineno-2-38" name="__codelineno-2-38"></a> progress bus, fetchers). Expose <span class="sb">`get_svc`</span> overloads.
</span><span id="__span-2-39"><a id="__codelineno-2-39" name="__codelineno-2-39"></a>
</span><span id="__span-2-40"><a id="__codelineno-2-40" name="__codelineno-2-40"></a><span class="k">5.</span> <span class="gs">**Add routes**</span> — create <span class="sb">`interfaces/&lt;resource&gt;/routes.py`</span> thin handlers bound
</span><span id="__span-2-41"><a id="__codelineno-2-41" name="__codelineno-2-41"></a> to <span class="sb">`operationId`</span>s and re-export them in <span class="sb">`interfaces/openapi.py`</span>. Add the cache
</span><span id="__span-2-42"><a id="__codelineno-2-42" name="__codelineno-2-42"></a> decorator to read endpoints.
</span><span id="__span-2-43"><a id="__codelineno-2-43" name="__codelineno-2-43"></a>
</span><span id="__span-2-44"><a id="__codelineno-2-44" name="__codelineno-2-44"></a><span class="k">6.</span> <span class="gs">**Bootstrap app and worker**</span> — confirm <span class="sb">`main.py`</span> runs the OpenAPI-first app
</span><span id="__span-2-45"><a id="__codelineno-2-45" name="__codelineno-2-45"></a> with its lifespan. If the app has long-running jobs, adapt <span class="sb">`worker.py`</span> to poll
</span><span id="__span-2-46"><a id="__codelineno-2-46" name="__codelineno-2-46"></a> the job collection, run the pipeline, and publish progress.
</span><span id="__span-2-47"><a id="__codelineno-2-47" name="__codelineno-2-47"></a>
</span><span id="__span-2-48"><a id="__codelineno-2-48" name="__codelineno-2-48"></a><span class="k">7.</span> <span class="gs">**Add pipeline families (only if data-driven)**</span> — copy <span class="sb">`pipelines/example/`</span>
</span><span id="__span-2-49"><a id="__codelineno-2-49" name="__codelineno-2-49"></a> to <span class="sb">`pipelines/&lt;family&gt;/`</span> with <span class="sb">`abc.py`</span>, <span class="sb">`impl.py`</span>, <span class="sb">`pipeline.yaml`</span>,
</span><span id="__span-2-50"><a id="__codelineno-2-50" name="__codelineno-2-50"></a> <span class="sb">`resources/`</span>, and reuse <span class="sb">`pipelines/shared/`</span> (loader + registry). Register the
</span><span id="__span-2-51"><a id="__codelineno-2-51" name="__codelineno-2-51"></a> root. Add fixtures and a parity test.
</span><span id="__span-2-52"><a id="__codelineno-2-52" name="__codelineno-2-52"></a>
</span><span id="__span-2-53"><a id="__codelineno-2-53" name="__codelineno-2-53"></a><span class="k">8.</span> <span class="gs">**Add infrastructure only where needed**</span> — Tier A capability: use the
</span><span id="__span-2-54"><a id="__codelineno-2-54" name="__codelineno-2-54"></a> library. Tier B capability: copy the template. Never hand-write a Tier A
</span><span id="__span-2-55"><a id="__codelineno-2-55" name="__codelineno-2-55"></a> library.
</span><span id="__span-2-56"><a id="__codelineno-2-56" name="__codelineno-2-56"></a>
</span><span id="__span-2-57"><a id="__codelineno-2-57" name="__codelineno-2-57"></a><span class="k">9.</span> <span class="gs">**Tests**</span> — integration tests with fakes at the HTTP edge; unit tests for
</span><span id="__span-2-58"><a id="__codelineno-2-58" name="__codelineno-2-58"></a> domain and pipeline logic; a config-consistency test for rule-driven
</span><span id="__span-2-59"><a id="__codelineno-2-59" name="__codelineno-2-59"></a> pipelines.
</span><span id="__span-2-60"><a id="__codelineno-2-60" name="__codelineno-2-60"></a>
</span><span id="__span-2-61"><a id="__codelineno-2-61" name="__codelineno-2-61"></a><span class="k">10.</span> <span class="gs">**Scripts and seeds**</span> — operator scripts under <span class="sb">`scripts/`</span> using
</span><span id="__span-2-62"><a id="__codelineno-2-62" name="__codelineno-2-62"></a> <span class="sb">`init()`</span> / <span class="sb">`get_svc()`</span> / <span class="sb">`shutdown()`</span>; run as <span class="sb">`python -m scripts.&lt;name&gt;`</span>.
</span><span id="__span-2-63"><a id="__codelineno-2-63" name="__codelineno-2-63"></a>
</span><span id="__span-2-64"><a id="__codelineno-2-64" name="__codelineno-2-64"></a><span class="k">11.</span> <span class="gs">**Docs**</span> — author GSDFC docstrings, generate <span class="sb">`.pyi`</span> stubs, mark the package
</span><span id="__span-2-65"><a id="__codelineno-2-65" name="__codelineno-2-65"></a> <span class="sb">`py.typed`</span>, and build the docs.
</span><span id="__span-2-66"><a id="__codelineno-2-66" name="__codelineno-2-66"></a>
</span><span id="__span-2-67"><a id="__codelineno-2-67" name="__codelineno-2-67"></a><span class="gs">**Definition of done:**</span>
</span><span id="__span-2-68"><a id="__codelineno-2-68" name="__codelineno-2-68"></a><span class="k">-</span><span class="w"> </span><span class="sb">`python -c &quot;import main&quot;`</span> succeeds.
</span><span id="__span-2-69"><a id="__codelineno-2-69" name="__codelineno-2-69"></a><span class="k">-</span><span class="w"> </span><span class="sb">`pytest`</span> passes.
</span><span id="__span-2-70"><a id="__codelineno-2-70" name="__codelineno-2-70"></a><span class="k">-</span><span class="w"> </span>The app serves every operation in the spec; the worker drains its queue.
</span><span id="__span-2-71"><a id="__codelineno-2-71" name="__codelineno-2-71"></a><span class="k">-</span><span class="w"> </span>No domain module imports a web framework.
</span><span id="__span-2-72"><a id="__codelineno-2-72" name="__codelineno-2-72"></a><span class="k">-</span><span class="w"> </span>No Tier A library was reimplemented.
</span><span id="__span-2-23"><a id="__codelineno-2-23" name="__codelineno-2-23"></a><span class="k">1.</span> <span class="gs">**Scaffold**</span> — copy <span class="sb">`standards/src/templates/app/`</span> into <span class="sb">`apps/&lt;your-app&gt;/`</span>
</span><span id="__span-2-24"><a id="__codelineno-2-24" name="__codelineno-2-24"></a> (or pull the same tree via <span class="sb">`get_app_template`</span>). Keep the tree: <span class="sb">`domain/`</span>,
</span><span id="__span-2-25"><a id="__codelineno-2-25" name="__codelineno-2-25"></a> <span class="sb">`infra/`</span>, <span class="sb">`interfaces/`</span>, <span class="sb">`pipelines/`</span>, <span class="sb">`tests/`</span>, <span class="sb">`scripts/`</span>, plus
</span><span id="__span-2-26"><a id="__codelineno-2-26" name="__codelineno-2-26"></a> <span class="sb">`pyproject.toml`</span>, <span class="sb">`openapi.yaml`</span>, <span class="sb">`config.py`</span>, <span class="sb">`init.py`</span>, <span class="sb">`main.py`</span>,
</span><span id="__span-2-27"><a id="__codelineno-2-27" name="__codelineno-2-27"></a> <span class="sb">`worker.py`</span>, <span class="sb">`Dockerfile`</span>, <span class="sb">`docker-compose.yaml`</span>, <span class="sb">`.env.example`</span>.
</span><span id="__span-2-28"><a id="__codelineno-2-28" name="__codelineno-2-28"></a>
</span><span id="__span-2-29"><a id="__codelineno-2-29" name="__codelineno-2-29"></a><span class="k">2.</span> <span class="gs">**Author the spec (app-specific)**</span> — write <span class="sb">`openapi.yaml`</span>: resources,
</span><span id="__span-2-30"><a id="__codelineno-2-30" name="__codelineno-2-30"></a> schemas, <span class="sb">`operationId`</span>s, security, and extension metadata. This is the
</span><span id="__span-2-31"><a id="__codelineno-2-31" name="__codelineno-2-31"></a> contract and drives all routing. Every operation MUST have an <span class="sb">`operationId`</span>.
</span><span id="__span-2-32"><a id="__codelineno-2-32" name="__codelineno-2-32"></a>
</span><span id="__span-2-33"><a id="__codelineno-2-33" name="__codelineno-2-33"></a><span class="k">3.</span> <span class="gs">**Generate the scaffold (codegen)**</span> — from the spec, generate the wire
</span><span id="__span-2-34"><a id="__codelineno-2-34" name="__codelineno-2-34"></a> models and the exact handler signatures:
</span><span id="__span-2-35"><a id="__codelineno-2-35" name="__codelineno-2-35"></a><span class="sb"> ```bash</span>
</span><span id="__span-2-36"><a id="__codelineno-2-36" name="__codelineno-2-36"></a><span class="w"> </span>openapi-first<span class="w"> </span>models<span class="w"> </span>openapi.yaml<span class="w"> </span>-o<span class="w"> </span>_gen/models.py
</span><span id="__span-2-37"><a id="__codelineno-2-37" name="__codelineno-2-37"></a><span class="w"> </span>openapi-first<span class="w"> </span>routes<span class="w"> </span>openapi.yaml<span class="w"> </span>-o<span class="w"> </span>_gen/routes<span class="w"> </span><span class="se">\</span>
</span><span id="__span-2-38"><a id="__codelineno-2-38" name="__codelineno-2-38"></a><span class="w"> </span>--use-models<span class="w"> </span>--models-module<span class="w"> </span>_gen.models<span class="w"> </span>--inject-request<span class="w"> </span>--package
</span><span id="__span-2-39"><a id="__codelineno-2-39" name="__codelineno-2-39"></a><span class="sb"> ```</span>
</span><span id="__span-2-40"><a id="__codelineno-2-40" name="__codelineno-2-40"></a> <span class="sb">`_gen/models.py`</span> is the <span class="gs">**wire contract**</span> (Pydantic, one class per
</span><span id="__span-2-41"><a id="__codelineno-2-41" name="__codelineno-2-41"></a> <span class="sb">`components.schemas`</span> entry). <span class="sb">`_gen/routes/&lt;resource&gt;/routes.py`</span> lists every
</span><span id="__span-2-42"><a id="__codelineno-2-42" name="__codelineno-2-42"></a> <span class="sb">`operationId`</span> with its exact parameter signature. Treat <span class="sb">`_gen/`</span> as generated
</span><span id="__span-2-43"><a id="__codelineno-2-43" name="__codelineno-2-43"></a> (never hand-edit it) — the canonical handlers live in <span class="sb">`interfaces/`</span>.
</span><span id="__span-2-44"><a id="__codelineno-2-44" name="__codelineno-2-44"></a>
</span><span id="__span-2-45"><a id="__codelineno-2-45" name="__codelineno-2-45"></a><span class="k">4.</span> <span class="gs">**Write each CRUD slice (app-specific)**</span> — per resource create the four-file
</span><span id="__span-2-46"><a id="__codelineno-2-46" name="__codelineno-2-46"></a> slice: <span class="sb">`models.py`</span> (domain model, <span class="gs">**no `id`**</span>), <span class="sb">`ports/db.py`</span>
</span><span id="__span-2-47"><a id="__codelineno-2-47" name="__codelineno-2-47"></a> (<span class="sb">`class XDocument(BaseDocument, X)`</span> — id comes from <span class="sb">`BaseDocument`</span>),
</span><span id="__span-2-48"><a id="__codelineno-2-48" name="__codelineno-2-48"></a> <span class="sb">`ports/service.py`</span>, and optional <span class="sb">`converters.py`</span>. Follow the CRUD template.
</span><span id="__span-2-49"><a id="__codelineno-2-49" name="__codelineno-2-49"></a>
</span><span id="__span-2-50"><a id="__codelineno-2-50" name="__codelineno-2-50"></a><span class="k">5.</span> <span class="gs">**Wire the composition root**</span> — in <span class="sb">`init.py`</span>, build leaf repositories, then
</span><span id="__span-2-51"><a id="__codelineno-2-51" name="__codelineno-2-51"></a> dependent repositories, then services; add infra adapters (db, cache, stores,
</span><span id="__span-2-52"><a id="__codelineno-2-52" name="__codelineno-2-52"></a> progress bus, fetchers). Expose <span class="sb">`get_svc`</span> overloads.
</span><span id="__span-2-53"><a id="__codelineno-2-53" name="__codelineno-2-53"></a>
</span><span id="__span-2-54"><a id="__codelineno-2-54" name="__codelineno-2-54"></a><span class="k">6.</span> <span class="gs">**Add routes**</span> — create <span class="sb">`interfaces/&lt;resource&gt;/routes.py`</span> thin handlers whose
</span><span id="__span-2-55"><a id="__codelineno-2-55" name="__codelineno-2-55"></a> names match the <span class="sb">`operationId`</span>s (use the signatures from step 3 as your
</span><span id="__span-2-56"><a id="__codelineno-2-56" name="__codelineno-2-56"></a> checklist) and re-export them in <span class="sb">`interfaces/openapi.py`</span>. Add the cache
</span><span id="__span-2-57"><a id="__codelineno-2-57" name="__codelineno-2-57"></a> decorator to read endpoints.
</span><span id="__span-2-58"><a id="__codelineno-2-58" name="__codelineno-2-58"></a>
</span><span id="__span-2-59"><a id="__codelineno-2-59" name="__codelineno-2-59"></a><span class="k">7.</span> <span class="gs">**Bootstrap app and worker**</span> — confirm <span class="sb">`main.py`</span> runs the OpenAPI-first app
</span><span id="__span-2-60"><a id="__codelineno-2-60" name="__codelineno-2-60"></a> with its lifespan. If the app has long-running jobs, adapt <span class="sb">`worker.py`</span> to poll
</span><span id="__span-2-61"><a id="__codelineno-2-61" name="__codelineno-2-61"></a> the job collection, run the pipeline, and publish progress.
</span><span id="__span-2-62"><a id="__codelineno-2-62" name="__codelineno-2-62"></a>
</span><span id="__span-2-63"><a id="__codelineno-2-63" name="__codelineno-2-63"></a><span class="k">8.</span> <span class="gs">**Add pipeline families (only if data-driven)**</span> — copy <span class="sb">`pipelines/example/`</span>
</span><span id="__span-2-64"><a id="__codelineno-2-64" name="__codelineno-2-64"></a> to <span class="sb">`pipelines/&lt;family&gt;/`</span> with <span class="sb">`abc.py`</span>, <span class="sb">`impl.py`</span>, <span class="sb">`pipeline.yaml`</span>,
</span><span id="__span-2-65"><a id="__codelineno-2-65" name="__codelineno-2-65"></a> <span class="sb">`resources/`</span>, and reuse <span class="sb">`pipelines/shared/`</span> (loader + registry). Register the
</span><span id="__span-2-66"><a id="__codelineno-2-66" name="__codelineno-2-66"></a> root. Add fixtures and a parity test.
</span><span id="__span-2-67"><a id="__codelineno-2-67" name="__codelineno-2-67"></a>
</span><span id="__span-2-68"><a id="__codelineno-2-68" name="__codelineno-2-68"></a><span class="k">9.</span> <span class="gs">**Add infrastructure only where needed**</span> — Tier A capability: use the
</span><span id="__span-2-69"><a id="__codelineno-2-69" name="__codelineno-2-69"></a> library. Tier B capability: copy the template. Never hand-write a Tier A
</span><span id="__span-2-70"><a id="__codelineno-2-70" name="__codelineno-2-70"></a> library.
</span><span id="__span-2-71"><a id="__codelineno-2-71" name="__codelineno-2-71"></a>
</span><span id="__span-2-72"><a id="__codelineno-2-72" name="__codelineno-2-72"></a><span class="k">10.</span> <span class="gs">**Tests**</span> — integration tests with fakes at the HTTP edge; unit tests for
</span><span id="__span-2-73"><a id="__codelineno-2-73" name="__codelineno-2-73"></a> domain and pipeline logic; a config-consistency test for rule-driven
</span><span id="__span-2-74"><a id="__codelineno-2-74" name="__codelineno-2-74"></a> pipelines. Include <span class="sb">`tests/integration/test_openapi_app_builds.py`</span> (imports
</span><span id="__span-2-75"><a id="__codelineno-2-75" name="__codelineno-2-75"></a> the app, asserts <span class="sb">`import main`</span> binds every operation).
</span><span id="__span-2-76"><a id="__codelineno-2-76" name="__codelineno-2-76"></a>
</span><span id="__span-2-77"><a id="__codelineno-2-77" name="__codelineno-2-77"></a><span class="k">11.</span> <span class="gs">**Scripts and seeds**</span> — operator scripts under <span class="sb">`scripts/`</span> using
</span><span id="__span-2-78"><a id="__codelineno-2-78" name="__codelineno-2-78"></a> <span class="sb">`init()`</span> / <span class="sb">`get_svc()`</span> / <span class="sb">`shutdown()`</span>; run as <span class="sb">`python -m scripts.&lt;name&gt;`</span>.
</span><span id="__span-2-79"><a id="__codelineno-2-79" name="__codelineno-2-79"></a>
</span><span id="__span-2-80"><a id="__codelineno-2-80" name="__codelineno-2-80"></a><span class="k">12.</span> <span class="gs">**Docs**</span> — author GSDFC docstrings, generate <span class="sb">`.pyi`</span> stubs, mark the package
</span><span id="__span-2-81"><a id="__codelineno-2-81" name="__codelineno-2-81"></a> <span class="sb">`py.typed`</span>, and build the docs.
</span><span id="__span-2-82"><a id="__codelineno-2-82" name="__codelineno-2-82"></a>
</span><span id="__span-2-83"><a id="__codelineno-2-83" name="__codelineno-2-83"></a><span class="gs">**Definition of done:**</span>
</span><span id="__span-2-84"><a id="__codelineno-2-84" name="__codelineno-2-84"></a><span class="k">-</span><span class="w"> </span><span class="sb">`python -c &quot;import main&quot;`</span> succeeds.
</span><span id="__span-2-85"><a id="__codelineno-2-85" name="__codelineno-2-85"></a><span class="k">-</span><span class="w"> </span><span class="sb">`pytest`</span> passes.
</span><span id="__span-2-86"><a id="__codelineno-2-86" name="__codelineno-2-86"></a><span class="k">-</span><span class="w"> </span>The app serves every operation in the spec; the worker drains its queue.
</span><span id="__span-2-87"><a id="__codelineno-2-87" name="__codelineno-2-87"></a><span class="k">-</span><span class="w"> </span>No domain module imports a web framework.
</span><span id="__span-2-88"><a id="__codelineno-2-88" name="__codelineno-2-88"></a><span class="k">-</span><span class="w"> </span>No Tier A library was reimplemented.
</span></code></pre></div></td></tr></table></div>
<hr />
<h2 id="where-the-steps-land">🧭 Where the steps land</h2>
@@ -873,53 +923,94 @@ environment-only; never commit them.</p>
<td>—</td>
</tr>
<tr>
<td>3 · CRUD slices</td>
<td>3 · Codegen</td>
<td><code>_gen/</code> (generated)</td>
<td>spec, openapi-first</td>
</tr>
<tr>
<td>4 · CRUD slices</td>
<td><code>domain/&lt;resource&gt;/</code></td>
<td>spec, CRUD template</td>
</tr>
<tr>
<td>4 · Wiring</td>
<td>5 · Wiring</td>
<td><code>init.py</code></td>
<td>every slice</td>
</tr>
<tr>
<td>5 · Routes</td>
<td>6 · Routes</td>
<td><code>interfaces/&lt;resource&gt;/</code></td>
<td>services, openapi route registry</td>
</tr>
<tr>
<td>6 · Bootstrap</td>
<td>7 · Bootstrap</td>
<td><code>main.py</code>, <code>worker.py</code></td>
<td>composition root</td>
</tr>
<tr>
<td>7 · Pipelines</td>
<td>8 · Pipelines</td>
<td><code>pipelines/&lt;family&gt;/</code>, <code>shared/</code></td>
<td>library pipeline engine</td>
</tr>
<tr>
<td>8 · Infra</td>
<td>9 · Infra</td>
<td><code>infra/</code></td>
<td>domain ports</td>
</tr>
<tr>
<td>9 · Tests</td>
<td>10 · Tests</td>
<td><code>tests/</code></td>
<td>app + fixtures</td>
</tr>
<tr>
<td>10 · Scripts</td>
<td>11 · Scripts</td>
<td><code>scripts/</code></td>
<td>composition root</td>
</tr>
<tr>
<td>11 · Docs</td>
<td>12 · Docs</td>
<td>docstrings + docs build</td>
<td>code, Doc Forge</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="the-codegen-fast-path-in-detail">🔧 The codegen fast path, in detail</h2>
<p><code>openapi-first</code> is spec → code only (it never generates a spec). Use it to erase
the two mechanical parts of the build:</p>
<table>
<thead>
<tr>
<th>Generated artifact</th>
<th>Replaces</th>
<th>Notes</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>_gen/models.py</code></td>
<td>hand-writing Pydantic wire models</td>
<td>one class per <code>components.schemas</code> entry</td>
</tr>
<tr>
<td><code>_gen/routes/&lt;resource&gt;/routes.py</code></td>
<td>hand-typing handler signatures</td>
<td>names == <code>operationId</code>s; <code>request: Request</code> injected</td>
</tr>
</tbody>
</table>
<p>Rules of the road:</p>
<ul>
<li><strong>Wire models are not domain models.</strong> <code>_gen/models.py</code> is the HTTP contract;
<code>domain/&lt;resource&gt;/models.py</code> is your business shape. If the spec's schema has
a required <code>id: str</code>, keep it off the domain model and let the document get its
id from <code>BaseDocument</code>.</li>
<li><strong>Resource names are sanitized.</strong> <code>/fetch-requests</code> → <code>fetch_requests.py</code>.</li>
<li><strong>Multipart bodies expand</strong> to <code>UploadFile = File(...)</code> / <code>Form(...)</code> params.</li>
<li><strong><code>_gen/</code> is disposable.</strong> Regenerate on every spec change; <code>git diff</code> shows
exactly what moved.</li>
</ul>
<hr />
<h2 id="definition-of-done">⚠️ Definition of done</h2>
<ul>
<li><code>python -c "import main"</code> succeeds.</li>

View File

@@ -414,6 +414,15 @@
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#authless-apps" class="md-nav__link">
<span class="md-ellipsis">
Authless apps
</span>
</a>
</li>
<li class="md-nav__item">
@@ -665,6 +674,15 @@
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#authless-apps" class="md-nav__link">
<span class="md-ellipsis">
Authless apps
</span>
</a>
</li>
<li class="md-nav__item">
@@ -763,10 +781,33 @@ can resolve <code>operationId</code> across the whole app:</p>
<li>Security is declared once in the spec and enforced by the framework; handlers
contain no auth code.</li>
</ul>
<h2 id="authless-apps">Authless apps</h2>
<p>Auth is <strong>deferred</strong> in the standard: the same spec-first app runs with or
without it, because security lives entirely in the spec, never in handlers.
For an app that needs no auth yet, do <strong>not</strong> invent per-handler auth checks —
just omit the security keys:</p>
<ul>
<li>Remove the top-level <code>security:</code> block.</li>
<li>Remove <code>components.securitySchemes</code>.</li>
<li>Do <strong>not</strong> add <code>security: []</code> to operations. An explicit empty array is a
per-operation <em>override</em> that means "public", and it only matters when a
global <code>security</code> block exists. Leaving every operation without a <code>security</code>
key is the cleanest authless spec.</li>
</ul>
<p>The handlers, services, and composition root are unchanged. When auth is needed
later, add the global <code>security</code> block and an introspecting dependency; only
handlers that must opt out get <code>security: []</code>.</p>
<h2 id="codegen">Codegen</h2>
<p>The <code>openapi_first</code> library can scaffold models and route stubs from the spec,
and ships app templates (CRUD app, model app, service). Prefer codegen over
hand-writing boilerplate.</p>
<p><code>openapi_first</code> scaffolds models and route stubs from the spec (see the
<a href="../01_build_recipe/">Build Recipe</a>, step 3):</p>
<div class="language-bash highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal"><a href="#__codelineno-4-1">1</a></span>
<span class="normal"><a href="#__codelineno-4-2">2</a></span>
<span class="normal"><a href="#__codelineno-4-3">3</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>openapi-first<span class="w"> </span>models<span class="w"> </span>openapi.yaml<span class="w"> </span>-o<span class="w"> </span>_gen/models.py
</span><span id="__span-4-2"><a id="__codelineno-4-2" name="__codelineno-4-2"></a>openapi-first<span class="w"> </span>routes<span class="w"> </span>openapi.yaml<span class="w"> </span>-o<span class="w"> </span>_gen/routes<span class="w"> </span><span class="se">\</span>
</span><span id="__span-4-3"><a id="__codelineno-4-3" name="__codelineno-4-3"></a><span class="w"> </span>--use-models<span class="w"> </span>--models-module<span class="w"> </span>_gen.models<span class="w"> </span>--inject-request<span class="w"> </span>--package
</span></code></pre></div></td></tr></table></div>
<p>Prefer codegen over hand-writing boilerplate. The result is the wire contract
and a set of <code>operationId</code>-named signatures — not the canonical handlers.</p>

View File

@@ -400,6 +400,15 @@
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#the-id-rule-model-vs-document" class="md-nav__link">
<span class="md-ellipsis">
The id rule (model vs. document)
</span>
</a>
</li>
<li class="md-nav__item">
@@ -629,6 +638,15 @@
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#the-id-rule-model-vs-document" class="md-nav__link">
<span class="md-ellipsis">
The id rule (model vs. document)
</span>
</a>
</li>
<li class="md-nav__item">
@@ -710,8 +728,53 @@ makes the codebase predictable and generatable.</p>
</span><span id="__span-0-5"><a id="__codelineno-0-5" name="__codelineno-0-5"></a> service.py # service class + factory
</span><span id="__span-0-6"><a id="__codelineno-0-6" name="__codelineno-0-6"></a> converters.py # (optional) transforms
</span></code></pre></div></td></tr></table></div>
<h2 id="the-id-rule-model-vs-document">The id rule (model vs. document)</h2>
<p>Two different objects, two different jobs:</p>
<table>
<thead>
<tr>
<th>Object</th>
<th>Lives in</th>
<th>Has <code>id</code>?</th>
<th>Purpose</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Domain model</strong></td>
<td><code>domain/&lt;resource&gt;/models.py</code></td>
<td><strong>No</strong></td>
<td>business shape, framework-free</td>
</tr>
<tr>
<td><strong>Document</strong></td>
<td><code>domain/&lt;resource&gt;/ports/db.py</code></td>
<td><strong>Yes</strong> (from <code>BaseDocument</code>)</td>
<td>Mongo persistence shape</td>
</tr>
</tbody>
</table>
<p><code>BaseDocument</code> (mongo-ops) supplies <code>id: Optional[PyObjectId]</code> (aliased to
<code>_id</code>) plus <code>created_at</code> / <code>updated_at</code>. The domain model must <strong>not</strong> declare
<code>id</code>; Mongo owns it.</p>
<blockquote>
<p><strong>Trap — generated wire models.</strong> <code>openapi-first models</code> produces the HTTP
<em>contract</em> (see the <a href="../01_build_recipe/">Build Recipe</a>, step 3). Response
schemas frequently declare a required <code>id: str</code>, so a generated class is
<strong>not</strong> a domain model. If you write
<code>class ItemDocument(Item, BaseDocument)</code> and <code>Item</code> re-exports a required
<code>id: str</code>, that field shadows <code>BaseDocument.id</code> and no Mongo <code>_id</code> can ever be
set. Fix it one of three ways:</p>
<ol>
<li><strong>Preferred:</strong> keep the domain model id-free (hand-author it from the CRUD
template), and keep the required <code>id</code> only on the wire model.</li>
<li>Order the bases so the document wins:
<code>class ItemDocument(BaseDocument, Item)</code>.</li>
<li>Keep a separate document type and translate with <code>ports/converters.py</code>.</li>
</ol>
</blockquote>
<h2 id="modelspy">models.py</h2>
<p>A framework-free Pydantic model describing the business shape.</p>
<p>A framework-free Pydantic model describing the business shape (no <code>id</code>).</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-1-1">1</a></span>
<span class="normal"><a href="#__codelineno-1-2">2</a></span>
<span class="normal"><a href="#__codelineno-1-3">3</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="k">class</span><span class="w"> </span><span class="nc">Item</span><span class="p">(</span><span class="n">BaseModel</span><span class="p">):</span>
@@ -730,16 +793,18 @@ The repository factory selects the right repository flavour.</p>
<span class="normal"><a href="#__codelineno-2-7"> 7</a></span>
<span class="normal"><a href="#__codelineno-2-8"> 8</a></span>
<span class="normal"><a href="#__codelineno-2-9"> 9</a></span>
<span class="normal"><a href="#__codelineno-2-10">10</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><span class="k">class</span><span class="w"> </span><span class="nc">ItemDocument</span><span class="p">(</span><span class="n">Item</span><span class="p">,</span> <span class="n">BaseDocument</span><span class="p">):</span> <span class="c1"># BaseDocument adds id/created_at/...</span>
</span><span id="__span-2-2"><a id="__codelineno-2-2" name="__codelineno-2-2"></a> <span class="k">pass</span>
</span><span id="__span-2-3"><a id="__codelineno-2-3" name="__codelineno-2-3"></a>
</span><span id="__span-2-4"><a id="__codelineno-2-4" name="__codelineno-2-4"></a><span class="k">def</span><span class="w"> </span><span class="nf">create_item_repo</span><span class="p">(</span><span class="o">*</span><span class="p">,</span> <span class="n">owner_repo</span><span class="o">=</span><span class="kc">None</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">ItemRepository</span><span class="p">:</span>
</span><span id="__span-2-5"><a id="__codelineno-2-5" name="__codelineno-2-5"></a> <span class="k">return</span> <span class="n">repo_factory</span><span class="p">(</span>
</span><span id="__span-2-6"><a id="__codelineno-2-6" name="__codelineno-2-6"></a> <span class="n">collection</span><span class="o">=</span><span class="s2">&quot;items&quot;</span><span class="p">,</span>
</span><span id="__span-2-7"><a id="__codelineno-2-7" name="__codelineno-2-7"></a> <span class="n">document</span><span class="o">=</span><span class="n">ItemDocument</span><span class="p">,</span>
</span><span id="__span-2-8"><a id="__codelineno-2-8" name="__codelineno-2-8"></a> <span class="n">populate_rules</span><span class="o">=</span><span class="p">[</span><span class="n">PopulateRule</span><span class="p">(</span><span class="n">field</span><span class="o">=</span><span class="s2">&quot;owner&quot;</span><span class="p">,</span> <span class="n">collection</span><span class="o">=</span><span class="s2">&quot;users&quot;</span><span class="p">)],</span>
</span><span id="__span-2-9"><a id="__codelineno-2-9" name="__codelineno-2-9"></a> <span class="n">cache</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span> <span class="c1"># or a CachedBaseRepository config</span>
</span><span id="__span-2-10"><a id="__codelineno-2-10" name="__codelineno-2-10"></a> <span class="p">)</span>
<span class="normal"><a href="#__codelineno-2-10">10</a></span>
<span class="normal"><a href="#__codelineno-2-11">11</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><span class="c1"># Item is id-free; BaseDocument adds id/created_at/updated_at.</span>
</span><span id="__span-2-2"><a id="__codelineno-2-2" name="__codelineno-2-2"></a><span class="k">class</span><span class="w"> </span><span class="nc">ItemDocument</span><span class="p">(</span><span class="n">Item</span><span class="p">,</span> <span class="n">BaseDocument</span><span class="p">):</span>
</span><span id="__span-2-3"><a id="__codelineno-2-3" name="__codelineno-2-3"></a> <span class="k">pass</span>
</span><span id="__span-2-4"><a id="__codelineno-2-4" name="__codelineno-2-4"></a>
</span><span id="__span-2-5"><a id="__codelineno-2-5" name="__codelineno-2-5"></a><span class="k">def</span><span class="w"> </span><span class="nf">create_item_repo</span><span class="p">(</span><span class="o">*</span><span class="p">,</span> <span class="n">owner_repo</span><span class="o">=</span><span class="kc">None</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">ItemRepository</span><span class="p">:</span>
</span><span id="__span-2-6"><a id="__codelineno-2-6" name="__codelineno-2-6"></a> <span class="k">return</span> <span class="n">repo_factory</span><span class="p">(</span>
</span><span id="__span-2-7"><a id="__codelineno-2-7" name="__codelineno-2-7"></a> <span class="n">collection</span><span class="o">=</span><span class="s2">&quot;items&quot;</span><span class="p">,</span>
</span><span id="__span-2-8"><a id="__codelineno-2-8" name="__codelineno-2-8"></a> <span class="n">document</span><span class="o">=</span><span class="n">ItemDocument</span><span class="p">,</span>
</span><span id="__span-2-9"><a id="__codelineno-2-9" name="__codelineno-2-9"></a> <span class="n">populate_rules</span><span class="o">=</span><span class="p">[</span><span class="n">PopulateRule</span><span class="p">(</span><span class="n">field</span><span class="o">=</span><span class="s2">&quot;owner&quot;</span><span class="p">,</span> <span class="n">collection</span><span class="o">=</span><span class="s2">&quot;users&quot;</span><span class="p">)],</span>
</span><span id="__span-2-10"><a id="__codelineno-2-10" name="__codelineno-2-10"></a> <span class="n">cache</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span> <span class="c1"># or a CachedBaseRepository config</span>
</span><span id="__span-2-11"><a id="__codelineno-2-11" name="__codelineno-2-11"></a> <span class="p">)</span>
</span></code></pre></div></td></tr></table></div>
<h2 id="portsservicepy">ports/service.py</h2>
<p>The service is built through the DI library, injecting the repository. The

View File

@@ -519,6 +519,15 @@
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#tier-b-patterns-port-from-the-reference-app" class="md-nav__link">
<span class="md-ellipsis">
Tier B (patterns) — port from the reference app
</span>
</a>
</li>
<li class="md-nav__item">
@@ -620,6 +629,15 @@
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#tier-b-patterns-port-from-the-reference-app" class="md-nav__link">
<span class="md-ellipsis">
Tier B (patterns) — port from the reference app
</span>
</a>
</li>
<li class="md-nav__item">
@@ -721,13 +739,16 @@ for its API. Writing a private copy of any of these is a defect.</p>
<h2 id="tier-b-template-provided-copy-from-templatesapp-then-adapt">Tier B — Template-provided (copy from <code>templates/app</code>, then adapt)</h2>
<p>These are reusable internal components shipped <strong>inside</strong> the template — they are
not independent libraries. Copy them as-is, then rename/populate only what is
app-specific. They give every app the same wiring shape as the reference app.</p>
app-specific. They give every app the same wiring shape as the reference app.
Fetch them by kind with <code>get_app_component</code>, or the whole tree with
<code>get_app_template</code>.</p>
<table>
<thead>
<tr>
<th>Component</th>
<th>Template location</th>
<th>Action</th>
<th>Shipped</th>
</tr>
</thead>
<tbody>
@@ -735,81 +756,114 @@ app-specific. They give every app the same wiring shape as the reference app.</p
<td>Layering &amp; dependency rule</td>
<td>the package layout + import rules</td>
<td>keep</td>
<td>✅</td>
</tr>
<tr>
<td>Composition root shape</td>
<td><code>init.py</code> (<code>get_svc</code>/<code>get_db</code>, idempotency)</td>
<td>keep</td>
<td>✅</td>
</tr>
<tr>
<td>Spec-first web edge</td>
<td><code>interfaces/openapi.py</code>, thin handlers bound to <code>operationId</code></td>
<td>keep</td>
<td>✅</td>
</tr>
<tr>
<td>CRUD slice pattern</td>
<td><code>domain/&lt;resource&gt;/{models.py, ports/db.py, ports/service.py, converters.py}</code></td>
<td>rename + fill</td>
<td>✅</td>
</tr>
<tr>
<td>Generic CRUD service</td>
<td><code>infra/adapters/service.py</code> (<code>CrudsServiceAdaptor</code>)</td>
<td>keep</td>
<td>✅</td>
</tr>
<tr>
<td>Repository factory</td>
<td><code>infra/db/repos/mongo.py</code> (<code>_repo_factory</code>)</td>
<td>keep</td>
<td>✅</td>
</tr>
<tr>
<td>Worker poll + status loop</td>
<td><code>worker.py</code></td>
<td>keep</td>
<td>✅</td>
</tr>
<tr>
<td>Progress bus + streaming</td>
<td>bus port + in-memory adapter + SSE endpoint</td>
<td>keep</td>
</tr>
<tr>
<td>Human-in-the-loop</td>
<td>durable pause + question document + resolve endpoint + resume map</td>
<td>Progress bus</td>
<td><code>infra/adapters/sse.py</code> (keyed in-memory bus) + an app SSE endpoint</td>
<td>keep</td>
<td>✅</td>
</tr>
<tr>
<td>HTTP response cache</td>
<td><code>cache_response</code> decorator + generation invalidation</td>
<td><code>infra/http_cache.py</code> (<code>cache_response</code> + generation invalidation)</td>
<td>keep</td>
<td>✅</td>
</tr>
<tr>
<td>Key-value store</td>
<td>KV store port + Redis adapter + in-memory fallback</td>
<td>keep</td>
</tr>
<tr>
<td>LLM client pool</td>
<td>provider-select + retry pool scaffold</td>
<td><code>infra/adapters/kv.py</code> (port + in-memory + Redis adaptor)</td>
<td>keep</td>
<td>✅</td>
</tr>
<tr>
<td>Pipeline scaffold</td>
<td><code>pipelines/shared/{ports,loader,registry}.py</code> + <code>pipelines/&lt;family&gt;/</code></td>
<td>rename + fill</td>
<td>✅</td>
</tr>
<tr>
<td>Operator scripts</td>
<td><code>scripts/</code> (<code>init()</code> / <code>get_svc()</code> / <code>shutdown()</code>)</td>
<td>rename</td>
<td>✅</td>
</tr>
<tr>
<td>Testing harness</td>
<td>integration fakes + fixture-parity pattern</td>
<td>keep</td>
<td>✅</td>
</tr>
<tr>
<td>Project scaffolding</td>
<td><code>pyproject.toml</code>, <code>Dockerfile</code>, <code>docker-compose.yaml</code>, <code>.env.example</code>, <code>openapi.yaml</code></td>
<td>rename + fill</td>
<td>✅</td>
</tr>
</tbody>
</table>
<h2 id="tier-b-patterns-port-from-the-reference-app">Tier B (patterns) — port from the reference app</h2>
<p>A few Tier B capabilities are real and reusable, but are <strong>shaped by the app or
the provider</strong>, so the template does not ship them verbatim — copying them
blindly would carry app-specific names. Treat them as patterns: lift the shape
from the reference app and rename per app.</p>
<table>
<thead>
<tr>
<th>Component</th>
<th>Reference location</th>
<th>Action</th>
<th>Shipped</th>
</tr>
</thead>
<tbody>
<tr>
<td>Human-in-the-loop</td>
<td>durable pause + question document + resolve endpoint + resume map</td>
<td>port + reshape</td>
<td>✗</td>
</tr>
<tr>
<td>LLM client pool</td>
<td>provider-select + retry/rotation pool scaffold</td>
<td>port + reshape</td>
<td>✗</td>
</tr>
</tbody>
</table>

View File

@@ -661,7 +661,7 @@ every application. <strong>Use them.</strong> Writing a private copy of any capa
is a defect.</p>
<p>Each library also exposes its own documentation as an MCP server. An AI agent
can read a library's full API by connecting to its MCP and reading
<code>doc://index</code>, <code>doc://nav</code>, and <code>doc://modules/{module}</code>.</p>
<code>docs://index</code>, <code>docs://nav</code>, and <code>docs://modules/{module}</code>.</p>
<hr />
<h2 id="catalog">Catalog</h2>
<table>
@@ -790,8 +790,8 @@ endpoint. Query it before writing any infrastructure.</p>
<h2 id="notes">Notes</h2>
<ul>
<li><strong>Hexa</strong> is a full library with its own MCP server on <code>:8008</code>: read
<code>doc://index</code>, <code>doc://nav</code>, and <code>doc://modules/{module}</code> (e.g.
<code>doc://modules/hexa.container</code>) to discover the DI surface — <code>build(cls, *,
<code>docs://index</code>, <code>docs://nav</code>, and <code>docs://modules/{module}</code> (e.g.
<code>docs://modules/hexa.container</code>) to discover the DI surface — <code>build(cls, *,
instances, config)</code> and the <code>PortNode</code> model.</li>
<li><strong>Auth Server</strong> is deferred: it has no <code>pyproject.toml</code>/importable package and
no MCP bundle. Do not reach for it yet; <code>py-jwt</code> covers token primitives.</li>

File diff suppressed because one or more lines are too long

View File

@@ -36,6 +36,12 @@
"placeholder": "resource",
"description": "Thin HTTP handlers bound to operationIds."
},
{
"kind": "openapi-routes",
"path": "interfaces/openapi.py",
"placeholder": "",
"description": "Route registry: flat re-export of every handler namespace."
},
{
"kind": "test-model",
"path": "tests/unit/test_item_models.py",
@@ -48,6 +54,18 @@
"placeholder": "resource",
"description": "Integration test at the HTTP edge."
},
{
"kind": "test-openapi-build",
"path": "tests/integration/test_openapi_app_builds.py",
"placeholder": "",
"description": "Generic test: every spec operationId resolves and the app builds."
},
{
"kind": "test-pipeline",
"path": "tests/unit/test_item_sync.py",
"placeholder": "",
"description": "Unit test for the smallest pipeline form."
},
{
"kind": "pipeline-abc",
"path": "pipelines/example/abc.py",
@@ -168,6 +186,12 @@
"placeholder": "",
"description": "API + worker image with private-index wiring."
},
{
"kind": "dockerignore",
"path": ".dockerignore",
"placeholder": "",
"description": "Docker build-context exclusions."
},
{
"kind": "compose",
"path": "docker-compose.yaml",
@@ -198,6 +222,30 @@
"placeholder": "",
"description": "Generic CRUD service adaptor."
},
{
"kind": "http-cache",
"path": "infra/http_cache.py",
"placeholder": "",
"description": "Response cache decorator + Redis store (generation invalidation)."
},
{
"kind": "progress-bus",
"path": "infra/adapters/sse.py",
"placeholder": "",
"description": "In-memory SSE progress bus (keyed by an event attribute)."
},
{
"kind": "kv-store",
"path": "infra/adapters/kv.py",
"placeholder": "",
"description": "KV port + in-memory fallback + optional Redis adaptor."
},
{
"kind": "package-init",
"path": "domain/__init__.py",
"placeholder": "",
"description": "Empty package marker; add an __init__.py to every package dir."
},
{
"kind": "script-seed",
"path": "scripts/seed.py",

File diff suppressed because one or more lines are too long

View File

@@ -2,7 +2,7 @@
"module": "crud_template",
"content": {
"path": "crud_template",
"docstring": "# CRUD Template\n\n## Summary\n\nEvery resource follows the same four-file slice. Repetition is intentional: it\nmakes the codebase predictable and generatable.\n\n---\n\n## The slice\n\n```\ndomain/<resource>/\n models.py # pure Pydantic model (no DB fields)\n ports/\n db.py # document model + repository factory\n service.py # service class + factory\n converters.py # (optional) transforms\n```\n\n## models.py\n\nA framework-free Pydantic model describing the business shape.\n\n```python\nclass Item(BaseModel):\n name: str\n quantity: int = 0\n```\n\n## ports/db.py\n\nThe persistence model extends the business model and adds database concerns.\nThe repository factory selects the right repository flavour.\n\n```python\nclass ItemDocument(Item, BaseDocument): # BaseDocument adds id/created_at/...\n pass\n\ndef create_item_repo(*, owner_repo=None) -> ItemRepository:\n return repo_factory(\n collection=\"items\",\n document=ItemDocument,\n populate_rules=[PopulateRule(field=\"owner\", collection=\"users\")],\n cache=None, # or a CachedBaseRepository config\n )\n```\n\n## ports/service.py\n\nThe service is built through the DI library, injecting the repository. The\ngeneric CRUD base provides `create/read/update/patch/delete/search/...`.\n\n```python\nclass ItemService(CrudsServiceAdaptor[Item, ItemDocument]):\n model = Item\n schema = ItemDocument\n\ndef get_item_service(repo) -> ItemService:\n return build(ItemService, instances={\"repo\": repo})\n```\n\n## Naming conventions\n\n| Concept | Convention |\n|---|---|\n| collection | plural snake case (`items`) |\n| document | `<Resource>Document` |\n| repository factory | `create_<resource>_repo` |\n| service | `<Resource>Service` |\n| service factory | `get_<resource>_service` |\n| resource key | `<resource>_svc` |\n| operationId | `list_items`, `create_item`, ... |\n\n## Optional files\n\n- `converters.py` — pure functions mapping between domain types.\n- `ports/enrichment.py` — adapts a pipeline into an async callable the service\n can invoke (used when a resource is populated by a pipeline).\n",
"docstring": "# CRUD Template\n\n## Summary\n\nEvery resource follows the same four-file slice. Repetition is intentional: it\nmakes the codebase predictable and generatable.\n\n---\n\n## The slice\n\n```\ndomain/<resource>/\n models.py # pure Pydantic model (no DB fields)\n ports/\n db.py # document model + repository factory\n service.py # service class + factory\n converters.py # (optional) transforms\n```\n\n## The id rule (model vs. document)\n\nTwo different objects, two different jobs:\n\n| Object | Lives in | Has `id`? | Purpose |\n|---|---|---|---|\n| **Domain model** | `domain/<resource>/models.py` | **No** | business shape, framework-free |\n| **Document** | `domain/<resource>/ports/db.py` | **Yes** (from `BaseDocument`) | Mongo persistence shape |\n\n`BaseDocument` (mongo-ops) supplies `id: Optional[PyObjectId]` (aliased to\n`_id`) plus `created_at` / `updated_at`. The domain model must **not** declare\n`id`; Mongo owns it.\n\n> **Trap — generated wire models.** `openapi-first models` produces the HTTP\n> *contract* (see the [Build Recipe](01_build_recipe.md), step 3). Response\n> schemas frequently declare a required `id: str`, so a generated class is\n> **not** a domain model. If you write\n> `class ItemDocument(Item, BaseDocument)` and `Item` re-exports a required\n> `id: str`, that field shadows `BaseDocument.id` and no Mongo `_id` can ever be\n> set. Fix it one of three ways:\n>\n> 1. **Preferred:** keep the domain model id-free (hand-author it from the CRUD\n> template), and keep the required `id` only on the wire model.\n> 2. Order the bases so the document wins:\n> `class ItemDocument(BaseDocument, Item)`.\n> 3. Keep a separate document type and translate with `ports/converters.py`.\n\n## models.py\n\nA framework-free Pydantic model describing the business shape (no `id`).\n\n```python\nclass Item(BaseModel):\n name: str\n quantity: int = 0\n```\n\n## ports/db.py\n\nThe persistence model extends the business model and adds database concerns.\nThe repository factory selects the right repository flavour.\n\n```python\n# Item is id-free; BaseDocument adds id/created_at/updated_at.\nclass ItemDocument(Item, BaseDocument):\n pass\n\ndef create_item_repo(*, owner_repo=None) -> ItemRepository:\n return repo_factory(\n collection=\"items\",\n document=ItemDocument,\n populate_rules=[PopulateRule(field=\"owner\", collection=\"users\")],\n cache=None, # or a CachedBaseRepository config\n )\n```\n\n## ports/service.py\n\nThe service is built through the DI library, injecting the repository. The\ngeneric CRUD base provides `create/read/update/patch/delete/search/...`.\n\n```python\nclass ItemService(CrudsServiceAdaptor[Item, ItemDocument]):\n model = Item\n schema = ItemDocument\n\ndef get_item_service(repo) -> ItemService:\n return build(ItemService, instances={\"repo\": repo})\n```\n\n## Naming conventions\n\n| Concept | Convention |\n|---|---|\n| collection | plural snake case (`items`) |\n| document | `<Resource>Document` |\n| repository factory | `create_<resource>_repo` |\n| service | `<Resource>Service` |\n| service factory | `get_<resource>_service` |\n| resource key | `<resource>_svc` |\n| operationId | `list_items`, `create_item`, ... |\n\n## Optional files\n\n- `converters.py` — pure functions mapping between domain types.\n- `ports/enrichment.py` — adapts a pipeline into an async callable the service\n can invoke (used when a resource is populated by a pipeline).\n",
"objects": {}
}
}

View File

@@ -2,7 +2,7 @@
"module": "genericity_matrix",
"content": {
"path": "genericity_matrix",
"docstring": "# Genericity Matrix\n\n## Summary\n\nThis page states precisely what is **library-backed**, what is\n**template-provided**, and what is **app-specific and must be authored**. Use it\nto scope a new build and to know, for any given capability, whether to call a\nlibrary, copy template code, or write it yourself.\n\n---\n\n## Tier A — Library-backed (use; never reimplement)\n\nThese capabilities already exist as internal libraries. Reach them through the\nlibrary registry MCP (`find_library(capability)`) and read the library's own MCP\nfor its API. Writing a private copy of any of these is a defect.\n\n| Capability | Library | MCP |\n|---|---|---|\n| Persistence: repositories, cache, populate, transactions | Mongo Ops (`mongo_ops`) | `:8007` |\n| Dependency injection / composition | Hexa (`hexa`) | `:8008` |\n| Deterministic multi-step transformation engine | DAG Pipe (`dagpipe`) | `:8006` |\n| Spec-first HTTP edge, client, codegen | OpenAPI First (`openapi_first`) | `:8004` |\n| Format-agnostic fetch + parse (HTML, PDF, CSV, XLSX) | Omniread (`omniread`) | `:8003` |\n| Read-only email ingestion | Mail Intake (`mail_intake`) | `:8002` |\n| JWT auth primitives (register, login, introspect) | Py JWT (`jwtlib`) | `:8005` |\n| Documentation build (MkDocs + MCP) | Doc Forge (`docforge`) | `:8001` |\n\n## Tier B — Template-provided (copy from `templates/app`, then adapt)\n\nThese are reusable internal components shipped **inside** the template — they are\nnot independent libraries. Copy them as-is, then rename/populate only what is\napp-specific. They give every app the same wiring shape as the reference app.\n\n| Component | Template location | Action |\n|---|---|---|\n| Layering & dependency rule | the package layout + import rules | keep |\n| Composition root shape | `init.py` (`get_svc`/`get_db`, idempotency) | keep |\n| Spec-first web edge | `interfaces/openapi.py`, thin handlers bound to `operationId` | keep |\n| CRUD slice pattern | `domain/<resource>/{models.py, ports/db.py, ports/service.py, converters.py}` | rename + fill |\n| Generic CRUD service | `infra/adapters/service.py` (`CrudsServiceAdaptor`) | keep |\n| Repository factory | `infra/db/repos/mongo.py` (`_repo_factory`) | keep |\n| Worker poll + status loop | `worker.py` | keep |\n| Progress bus + streaming | bus port + in-memory adapter + SSE endpoint | keep |\n| Human-in-the-loop | durable pause + question document + resolve endpoint + resume map | keep |\n| HTTP response cache | `cache_response` decorator + generation invalidation | keep |\n| Key-value store | KV store port + Redis adapter + in-memory fallback | keep |\n| LLM client pool | provider-select + retry pool scaffold | keep |\n| Pipeline scaffold | `pipelines/shared/{ports,loader,registry}.py` + `pipelines/<family>/` | rename + fill |\n| Operator scripts | `scripts/` (`init()` / `get_svc()` / `shutdown()`) | rename |\n| Testing harness | integration fakes + fixture-parity pattern | keep |\n| Project scaffolding | `pyproject.toml`, `Dockerfile`, `docker-compose.yaml`, `.env.example`, `openapi.yaml` | rename + fill |\n\n## Tier C — App-specific (must be authored)\n\n| Artifact | Why |\n|---|---|\n| Resource models | the business shape |\n| Repository populate/cache rules | the relationships |\n| Service business logic | domain behaviour |\n| Pipeline families | the transformation graph |\n| Pattern/rule resources | data-driven policy |\n| Fixtures & golden outputs | test data |\n\n---\n\n## The two axes\n\nEverything app-specific reduces to **two axes**:\n\n1. **Resource model** — the set of resources, their fields, relationships, and\n the OpenAPI contract.\n2. **Pipeline family** — only if inputs require multi-step transformation.\n\nIf you can specify those two axes, the rest of the app follows mechanically:\nTier A is imported, Tier B is copied, and Tier C is the only thing you author.\n",
"docstring": "# Genericity Matrix\n\n## Summary\n\nThis page states precisely what is **library-backed**, what is\n**template-provided**, and what is **app-specific and must be authored**. Use it\nto scope a new build and to know, for any given capability, whether to call a\nlibrary, copy template code, or write it yourself.\n\n---\n\n## Tier A — Library-backed (use; never reimplement)\n\nThese capabilities already exist as internal libraries. Reach them through the\nlibrary registry MCP (`find_library(capability)`) and read the library's own MCP\nfor its API. Writing a private copy of any of these is a defect.\n\n| Capability | Library | MCP |\n|---|---|---|\n| Persistence: repositories, cache, populate, transactions | Mongo Ops (`mongo_ops`) | `:8007` |\n| Dependency injection / composition | Hexa (`hexa`) | `:8008` |\n| Deterministic multi-step transformation engine | DAG Pipe (`dagpipe`) | `:8006` |\n| Spec-first HTTP edge, client, codegen | OpenAPI First (`openapi_first`) | `:8004` |\n| Format-agnostic fetch + parse (HTML, PDF, CSV, XLSX) | Omniread (`omniread`) | `:8003` |\n| Read-only email ingestion | Mail Intake (`mail_intake`) | `:8002` |\n| JWT auth primitives (register, login, introspect) | Py JWT (`jwtlib`) | `:8005` |\n| Documentation build (MkDocs + MCP) | Doc Forge (`docforge`) | `:8001` |\n\n## Tier B — Template-provided (copy from `templates/app`, then adapt)\n\nThese are reusable internal components shipped **inside** the template — they are\nnot independent libraries. Copy them as-is, then rename/populate only what is\napp-specific. They give every app the same wiring shape as the reference app.\nFetch them by kind with `get_app_component`, or the whole tree with\n`get_app_template`.\n\n| Component | Template location | Action | Shipped |\n|---|---|---|---|\n| Layering & dependency rule | the package layout + import rules | keep | ✅ |\n| Composition root shape | `init.py` (`get_svc`/`get_db`, idempotency) | keep | ✅ |\n| Spec-first web edge | `interfaces/openapi.py`, thin handlers bound to `operationId` | keep | ✅ |\n| CRUD slice pattern | `domain/<resource>/{models.py, ports/db.py, ports/service.py, converters.py}` | rename + fill | ✅ |\n| Generic CRUD service | `infra/adapters/service.py` (`CrudsServiceAdaptor`) | keep | ✅ |\n| Repository factory | `infra/db/repos/mongo.py` (`_repo_factory`) | keep | ✅ |\n| Worker poll + status loop | `worker.py` | keep | ✅ |\n| Progress bus | `infra/adapters/sse.py` (keyed in-memory bus) + an app SSE endpoint | keep | ✅ |\n| HTTP response cache | `infra/http_cache.py` (`cache_response` + generation invalidation) | keep | ✅ |\n| Key-value store | `infra/adapters/kv.py` (port + in-memory + Redis adaptor) | keep | ✅ |\n| Pipeline scaffold | `pipelines/shared/{ports,loader,registry}.py` + `pipelines/<family>/` | rename + fill | ✅ |\n| Operator scripts | `scripts/` (`init()` / `get_svc()` / `shutdown()`) | rename | ✅ |\n| Testing harness | integration fakes + fixture-parity pattern | keep | ✅ |\n| Project scaffolding | `pyproject.toml`, `Dockerfile`, `docker-compose.yaml`, `.env.example`, `openapi.yaml` | rename + fill | ✅ |\n\n## Tier B (patterns) — port from the reference app\n\nA few Tier B capabilities are real and reusable, but are **shaped by the app or\nthe provider**, so the template does not ship them verbatim — copying them\nblindly would carry app-specific names. Treat them as patterns: lift the shape\nfrom the reference app and rename per app.\n\n| Component | Reference location | Action | Shipped |\n|---|---|---|---|\n| Human-in-the-loop | durable pause + question document + resolve endpoint + resume map | port + reshape | ✗ |\n| LLM client pool | provider-select + retry/rotation pool scaffold | port + reshape | ✗ |\n\n## Tier C — App-specific (must be authored)\n\n| Artifact | Why |\n|---|---|\n| Resource models | the business shape |\n| Repository populate/cache rules | the relationships |\n| Service business logic | domain behaviour |\n| Pipeline families | the transformation graph |\n| Pattern/rule resources | data-driven policy |\n| Fixtures & golden outputs | test data |\n\n---\n\n## The two axes\n\nEverything app-specific reduces to **two axes**:\n\n1. **Resource model** — the set of resources, their fields, relationships, and\n the OpenAPI contract.\n2. **Pipeline family** — only if inputs require multi-step transformation.\n\nIf you can specify those two axes, the rest of the app follows mechanically:\nTier A is imported, Tier B is copied, and Tier C is the only thing you author.\n",
"objects": {}
}
}

View File

@@ -2,7 +2,7 @@
"module": "spec_first_routing",
"content": {
"path": "spec_first_routing",
"docstring": "# Spec-First Routing\n\n## Summary\n\nThe OpenAPI document is the single source of truth for the HTTP contract.\nHandlers are bound to it by name; there is no decorator router.\n\n---\n\n## The contract\n\n- `openapi.yaml` (or `.json`) declares paths, schemas, parameters, responses,\n and security.\n- A custom `operationId` names the handler function.\n- The framework (`openapi_first`) validates requests/responses against the\n spec and dispatches to the handler whose name equals `operationId`.\n\n## App bootstrap\n\n```python\n# main.py\nfrom openapi_first.app import OpenAPIFirstApp\n\napp = OpenAPIFirstApp(\n openapi_path=\"openapi.yaml\",\n routes_module=interfaces.openapi, # module whose namespace holds handlers\n title=\"<App> API\",\n version=\"0.1.0\",\n lifespan=lifespan,\n)\n```\n\n`lifespan` calls the composition root:\n\n```python\n@asynccontextmanager\nasync def lifespan(app):\n app.state.db_resources = await init()\n yield\n await shutdown()\n```\n\n## Handler registry\n\n`interfaces/openapi.py` is a flat re-export of every handler, so the framework\ncan resolve `operationId` across the whole app:\n\n```python\nfrom interfaces.item.routes import *\nfrom interfaces.wallet.routes import *\nfrom interfaces.progress.sse import *\n```\n\n## Handler shape\n\nHandlers are thin and synchronous with respect to business logic:\n\n```python\nasync def create_item(request: Request, body: dict):\n svc = request.app.state.db_resources[\"item_svc\"]\n return await svc.create(Item.model_validate(body))\n```\n\n## Conventions\n\n- `operationId` naming: `list_<resource>`, `create_<resource>`,\n `get_<resource>`, `update_<resource>`, `delete_<resource>`.\n- Read-only endpoints may use a cache decorator keyed on path + query + user.\n- Validation errors -> `400`; not found -> `404`; delete -> `204`.\n- Security is declared once in the spec and enforced by the framework; handlers\n contain no auth code.\n\n## Codegen\n\nThe `openapi_first` library can scaffold models and route stubs from the spec,\nand ships app templates (CRUD app, model app, service). Prefer codegen over\nhand-writing boilerplate.\n",
"docstring": "# Spec-First Routing\n\n## Summary\n\nThe OpenAPI document is the single source of truth for the HTTP contract.\nHandlers are bound to it by name; there is no decorator router.\n\n---\n\n## The contract\n\n- `openapi.yaml` (or `.json`) declares paths, schemas, parameters, responses,\n and security.\n- A custom `operationId` names the handler function.\n- The framework (`openapi_first`) validates requests/responses against the\n spec and dispatches to the handler whose name equals `operationId`.\n\n## App bootstrap\n\n```python\n# main.py\nfrom openapi_first.app import OpenAPIFirstApp\n\napp = OpenAPIFirstApp(\n openapi_path=\"openapi.yaml\",\n routes_module=interfaces.openapi, # module whose namespace holds handlers\n title=\"<App> API\",\n version=\"0.1.0\",\n lifespan=lifespan,\n)\n```\n\n`lifespan` calls the composition root:\n\n```python\n@asynccontextmanager\nasync def lifespan(app):\n app.state.db_resources = await init()\n yield\n await shutdown()\n```\n\n## Handler registry\n\n`interfaces/openapi.py` is a flat re-export of every handler, so the framework\ncan resolve `operationId` across the whole app:\n\n```python\nfrom interfaces.item.routes import *\nfrom interfaces.wallet.routes import *\nfrom interfaces.progress.sse import *\n```\n\n## Handler shape\n\nHandlers are thin and synchronous with respect to business logic:\n\n```python\nasync def create_item(request: Request, body: dict):\n svc = request.app.state.db_resources[\"item_svc\"]\n return await svc.create(Item.model_validate(body))\n```\n\n## Conventions\n\n- `operationId` naming: `list_<resource>`, `create_<resource>`,\n `get_<resource>`, `update_<resource>`, `delete_<resource>`.\n- Read-only endpoints may use a cache decorator keyed on path + query + user.\n- Validation errors -> `400`; not found -> `404`; delete -> `204`.\n- Security is declared once in the spec and enforced by the framework; handlers\n contain no auth code.\n\n## Authless apps\n\nAuth is **deferred** in the standard: the same spec-first app runs with or\nwithout it, because security lives entirely in the spec, never in handlers.\nFor an app that needs no auth yet, do **not** invent per-handler auth checks —\njust omit the security keys:\n\n- Remove the top-level `security:` block.\n- Remove `components.securitySchemes`.\n- Do **not** add `security: []` to operations. An explicit empty array is a\n per-operation *override* that means \"public\", and it only matters when a\n global `security` block exists. Leaving every operation without a `security`\n key is the cleanest authless spec.\n\nThe handlers, services, and composition root are unchanged. When auth is needed\nlater, add the global `security` block and an introspecting dependency; only\nhandlers that must opt out get `security: []`.\n\n## Codegen\n\n`openapi_first` scaffolds models and route stubs from the spec (see the\n[Build Recipe](01_build_recipe.md), step 3):\n\n```bash\nopenapi-first models openapi.yaml -o _gen/models.py\nopenapi-first routes openapi.yaml -o _gen/routes \\\n --use-models --models-module _gen.models --inject-request --package\n```\n\nPrefer codegen over hand-writing boilerplate. The result is the wire contract\nand a set of `operationId`-named signatures — not the canonical handlers.\n",
"objects": {}
}
}

View File

@@ -0,0 +1,147 @@
"""# Summary
Key-value store port with in-memory and Redis adaptors.
---
Reserve infrastructure for caching small values by key (sessions, locks,
result blobs, rate counters). The in-memory adaptor needs no services and is a
safe default; the Redis adaptor is opt-in and imports ``redis`` lazily so it
never becomes a hard dependency.
Example:
```python
from infra.adapters.kv import InMemoryKV
kv = InMemoryKV()
await kv.set("greeting", "hello", ttl=30)
assert await kv.get("greeting") == "hello"
```
"""
from __future__ import annotations
from abc import ABC, abstractmethod
from typing import Dict, List, Optional
class KVStore(ABC):
"""Abstract key-value store port."""
@abstractmethod
async def get(self, key: str) -> Optional[str]:
"""Return the value at ``key``, or ``None``."""
@abstractmethod
async def set(self, key: str, value: str, ttl: Optional[int] = None) -> None:
"""Store ``value`` at ``key`` with an optional TTL in seconds."""
@abstractmethod
async def delete(self, key: str) -> None:
"""Delete ``key`` if present."""
@abstractmethod
async def scan(self, pattern: str) -> List[str]:
"""Return keys matching a glob ``pattern``."""
async def clear(self, pattern: str = "*") -> None:
"""Delete all keys matching ``pattern``.
Args:
pattern (str): Glob-style key pattern.
"""
for key in await self.scan(pattern):
await self.delete(key)
async def close(self) -> None:
"""Release any underlying resources. Default is a no-op."""
class InMemoryKV(KVStore):
"""Process-local key-value store used as a dependency-free fallback.
Attributes:
_data (Dict[str, str]): In-memory value map.
_expiry (Dict[str, float]): Absolute expiry timestamps by key; ``0.0``
means "no expiry".
"""
def __init__(self) -> None:
"""Initialize an empty store with no imports or connections."""
self._data: Dict[str, str] = {}
self._expiry: Dict[str, float] = {}
def _expired(self, key: str) -> bool:
import time
deadline = self._expiry.get(key, 0.0)
if deadline and deadline < time.time():
self._data.pop(key, None)
self._expiry.pop(key, None)
return True
return False
async def get(self, key: str) -> Optional[str]:
"""Return the value at ``key``, or ``None`` when absent/expired."""
if self._expired(key):
return None
return self._data.get(key)
async def set(self, key: str, value: str, ttl: Optional[int] = None) -> None:
"""Store ``value`` at ``key`` with an optional TTL in seconds."""
import time
self._data[key] = value
self._expiry[key] = (time.time() + ttl) if ttl else 0.0
async def delete(self, key: str) -> None:
"""Delete ``key`` if present."""
self._data.pop(key, None)
self._expiry.pop(key, None)
async def scan(self, pattern: str) -> List[str]:
"""Return live keys matching a glob ``pattern``."""
import fnmatch
return [k for k in list(self._data) if fnmatch.fnmatch(k, pattern) and not self._expired(k)]
class RedisKV(KVStore):
"""Redis-backed key-value store.
Attributes:
_redis: Asynchronous Redis client (imported lazily).
"""
def __init__(self, url: str = "redis://localhost:6379/0", decode_responses: bool = True):
"""Initialize the store with a Redis connection.
Args:
url (str): Redis connection URL.
decode_responses (bool): Whether Redis returns decoded strings.
Raises:
ImportError: When the optional ``redis`` package is not installed.
"""
from redis.asyncio import Redis
self._redis = Redis.from_url(url, decode_responses=decode_responses)
async def get(self, key: str) -> Optional[str]:
"""Return the value at ``key``, or ``None``."""
return await self._redis.get(key)
async def set(self, key: str, value: str, ttl: Optional[int] = None) -> None:
"""Store ``value`` at ``key`` with an optional TTL in seconds."""
await self._redis.set(key, value, ex=ttl)
async def delete(self, key: str) -> None:
"""Delete ``key`` if present."""
await self._redis.delete(key)
async def scan(self, pattern: str) -> List[str]:
"""Return keys matching a glob ``pattern``."""
return [key async for key in self._redis.scan_iter(match=pattern)]
async def close(self) -> None:
"""Close the underlying Redis connection."""
await self._redis.aclose()

View File

@@ -0,0 +1,54 @@
"""# Summary
SSEInMemoryBus — in-memory event bus for SSE progress events.
---
Buffer published progress events so they can be replayed (and streamed) per
correlation key, such as a job, upload, or fetch-request id.
"""
from typing import Generic, List, TypeVar
T = TypeVar("T")
class SSEInMemoryBus(Generic[T]):
"""Buffer SSE progress events in memory, keyed by an event attribute.
Attributes:
_events (List[T]): Events published since the last clear.
_key_attr (str): Event attribute used to correlate a stream.
"""
def __init__(self, key_attr: str = "id"):
"""Initialize an empty event buffer.
Args:
key_attr (str): Attribute name on each event holding the stream
correlation key (for example ``"job_id"``).
"""
self._events: List[T] = []
self._key_attr = key_attr
async def publish(self, event: T) -> None:
"""Append an event to the buffer.
Args:
event (T): Event to append.
"""
self._events.append(event)
def get_events(self, key: str) -> List[T]:
"""Return buffered events for a correlation key.
Args:
key (str): Value matched against the configured ``key_attr``.
Returns:
List[T]: Events whose ``key_attr`` equals ``key``.
"""
return [e for e in self._events if getattr(e, self._key_attr, None) == key]
def clear(self) -> None:
"""Remove all buffered events."""
self._events.clear()

View File

@@ -0,0 +1,229 @@
"""# Summary
Domain-agnostic HTTP response cache.
---
A thin Redis-backed response cache that wraps route handlers as a decorator.
Example:
```python
from infra.http_cache import cache_response
@cache_response(ttl=60)
async def list_items(request, skip: int = 0, limit: int = 100):
...
```
Notes:
The cache operates directly on the endpoint and is independent of any
repository-level cache. Cached responses expire via TTL. When no
``http_cache_store`` is wired on ``request.app.state.db_resources`` the
decorator is a no-op pass-through, so applying/removing it is safe.
"""
import hashlib
import json
from functools import wraps
from typing import Any, Callable, Optional
from fastapi import Request, Response
from fastapi.encoders import jsonable_encoder
CACHE_PREFIX = "apicache:"
NS_PREFIX = CACHE_PREFIX + "ns:"
DEFAULT_TTL = 60
class HttpCacheStore:
"""Store string responses in Redis with TTL under the ``apicache:`` key space.
Attributes:
_redis (Redis): Asynchronous Redis client bound to the configured URL.
"""
def __init__(self, url: str = "redis://localhost:6379/0", decode_responses: bool = True):
"""Initialize the store with a Redis connection.
Args:
url (str): Redis connection URL.
decode_responses (bool): Whether Redis returns decoded strings.
Raises:
ImportError: When the optional ``redis`` package is not installed.
"""
from redis.asyncio import Redis
self._redis = Redis.from_url(url, decode_responses=decode_responses)
async def get(self, key: str) -> Optional[str]:
"""Read the value stored at ``key``.
Args:
key (str): Cache key to read.
Returns:
Optional[str]: Cached value, or ``None`` when absent.
"""
return await self._redis.get(CACHE_PREFIX + key)
async def set(self, key: str, value: str, ttl: int) -> None:
"""Store ``value`` at ``key`` with a TTL.
Args:
key (str): Cache key to write.
value (str): Serialized value to persist.
ttl (int): Lifetime in seconds.
"""
await self._redis.set(CACHE_PREFIX + key, value, ex=ttl)
async def delete(self, key: str) -> None:
"""Delete the value stored at ``key``.
Args:
key (str): Cache key to remove.
"""
await self._redis.delete(CACHE_PREFIX + key)
async def generation(self, resource: str) -> str:
"""Return the current generation counter for a resource namespace.
Args:
resource (str): Resource namespace to read.
Returns:
str: Generation counter, or ``"0"`` when unset.
"""
value = await self._redis.get(NS_PREFIX + resource)
return value if value is not None else "0"
async def invalidate(self, *resources: str) -> None:
"""Invalidate all cached responses for the given resource namespaces.
Args:
*resources (str): Resource namespaces to invalidate.
Notes:
Bumps each namespace's generation counter so existing entries become
unreachable immediately; Redis reclaims them at TTL expiry.
"""
if not resources:
return
pipe = self._redis.pipeline(transaction=False)
for resource in resources:
pipe.incr(NS_PREFIX + resource)
await pipe.execute()
async def close(self) -> None:
"""Close the underlying Redis connection."""
await self._redis.aclose()
def _user_scope(request: Request) -> str:
"""Build a stable per-user cache scope.
Args:
request (Request): Incoming HTTP request.
Returns:
str: Scoped identifier, or ``""`` for auth-free endpoints.
"""
user = getattr(request.state, "user", None)
if isinstance(user, dict):
for field in ("username", "id", "email"):
if user.get(field):
return str(user[field])
token = getattr(request.state, "token", None)
if token:
return hashlib.sha256(str(token).encode()).hexdigest()[:16]
return ""
def _resource_name(request: Request) -> str:
"""Return the first path segment identifying the resource.
Args:
request (Request): Incoming HTTP request.
Returns:
str: First path segment (for example ``"items"``), or ``"root"`` when
the path is empty.
"""
return request.url.path.strip("/").split("/", 1)[0] or "root"
def _cache_key(request: Request, generation: str = "0") -> str:
"""Compute a stable cache key for a request.
Args:
request (Request): Incoming HTTP request.
generation (str): Resource generation counter guarding stale entries.
Returns:
str: SHA-256 digest of the request method, path, query and path
parameters, user scope, and generation.
"""
parts = [
request.method,
request.url.path,
"&".join(f"{k}={v}" for k, v in sorted(request.query_params.items())),
"&".join(f"{k}={v}" for k, v in sorted(request.path_params.items())),
_user_scope(request),
generation,
]
return hashlib.sha256("|".join(parts).encode()).hexdigest()
def cache_response(ttl: Optional[int] = None) -> Callable:
"""Decorate a GET-style route handler to cache its JSON response.
Args:
ttl (Optional[int]): Cache lifetime in seconds; defaults to
``DEFAULT_TTL``.
Returns:
Callable: Decorator that wraps the route handler.
Notes:
The wrapped handler must accept a ``request: Request`` parameter (as
every openapi_first handler does). Responses that are already a
``fastapi.Response`` (for example 204) are returned untouched and
never cached. When no ``http_cache_store`` is wired on
``request.app.state.db_resources`` the decorator passes through to the
handler without caching.
"""
ttl = ttl if ttl is not None else DEFAULT_TTL
def decorator(func: Callable) -> Callable:
@wraps(func)
async def wrapper(*args: Any, **kwargs: Any):
request = kwargs.get("request")
if request is None:
request = next((a for a in args if isinstance(a, Request)), None)
if request is None:
return await func(*args, **kwargs)
db_resources = getattr(request.app.state, "db_resources", None) or {}
store: Optional[HttpCacheStore] = db_resources.get("http_cache_store")
if store is None:
return await func(*args, **kwargs)
key = _cache_key(request, await store.generation(_resource_name(request)))
cached = await store.get(key)
if cached is not None:
return Response(content=cached, media_type="application/json")
result = await func(*args, **kwargs)
if isinstance(result, Response):
return result
payload = json.dumps(
jsonable_encoder(result),
separators=(",", ":"),
ensure_ascii=False,
)
await store.set(key, payload, ttl)
return Response(content=payload, media_type="application/json")
return wrapper
return decorator

View File

@@ -11,6 +11,7 @@ dependencies = [
"uvicorn[standard]",
"pydantic>=2",
"motor",
"redis",
"python-dotenv",
"pyyaml",
"openapi-first",

View File

@@ -0,0 +1,44 @@
import importlib
import yaml
import interfaces.openapi as routes_module
from openapi_first.app import OpenAPIFirstApp
_HTTP_METHODS = {"get", "post", "put", "patch", "delete", "head", "options"}
def _operation_ids(spec: dict) -> list[str]:
"""Collect every declared operationId from an OpenAPI spec.
Args:
spec (dict): Parsed OpenAPI document.
Returns:
list[str]: Operation ids in declaration order.
"""
ids: list[str] = []
for path_item in (spec.get("paths") or {}).values():
for method, operation in path_item.items():
if method.lower() in _HTTP_METHODS and isinstance(operation, dict):
if operation.get("operationId"):
ids.append(operation["operationId"])
return ids
def test_openapi_app_builds():
"""Every operationId in the spec must resolve to a handler, and the app must build."""
spec = yaml.safe_load(open("openapi.yaml", encoding="utf-8"))
module = importlib.import_module(routes_module.__name__)
missing = [oid for oid in _operation_ids(spec) if not hasattr(module, oid)]
assert not missing, f"handlers missing for operationIds: {missing}"
app = OpenAPIFirstApp(
openapi_path="openapi.yaml",
routes_module=module,
title="App API",
version="0.1.0",
)
assert app is not None

View File

@@ -3,10 +3,10 @@
## Summary
The ordered, deterministic checklist for generating a new application of the
standard shape. Every step is fixed; only the resource model (step 2) and the
pipeline family (step 7) need app-specific authoring.
standard shape. Every step is fixed; only the resource model (step 2), the
codegen pass (step 3), and the pipeline family (step 8) need app-specific work.
This page is served by the `standards` MCP as `doc://modules/build_recipe`. The
This page is served by the `standards` MCP as `docs://modules/build_recipe`. The
`start_app` prompt on that server collects the two axes and hands off here; pull
exact files with `get_app_component`.
@@ -42,15 +42,15 @@ pip install --extra-index-url \
# Build a new application from the Aetos Standards blueprint
You are coding a complete new backend application conforming to the Aetos
standard app blueprint. Execute every step in order. Only steps 2 and 3 use the
standard app blueprint. Execute every step in order. Only steps 2–4 use the
app-specific details provided by the user; everything else is deterministic.
## Before writing any code
Connect to the `standards` MCP server and read:
- `doc://nav` (the module list)
- `doc://modules/genericity_matrix` (Tier A library / Tier B template / Tier C authored)
- `doc://modules/layering_rules` (the dependency rules)
- `docs://nav` (the module list)
- `docs://modules/genericity_matrix` (Tier A library / Tier B template / Tier C authored)
- `docs://modules/layering_rules` (the dependency rules)
Then:
- call `find_library` with `db`, `di`, `routing`, `ingestion`, `pipeline`, `docs`
@@ -61,48 +61,64 @@ Then:
## Steps
1. **Scaffold** — copy `standards/src/templates/app/` into `apps/<your-app>/`.
Keep the tree: `domain/`, `infra/`, `interfaces/`, `pipelines/`, `tests/`,
`scripts/`, plus `pyproject.toml`, `openapi.yaml`, `config.py`, `init.py`,
`main.py`, `worker.py`, `Dockerfile`, `docker-compose.yaml`, `.env.example`.
1. **Scaffold** — copy `standards/src/templates/app/` into `apps/<your-app>/`
(or pull the same tree via `get_app_template`). Keep the tree: `domain/`,
`infra/`, `interfaces/`, `pipelines/`, `tests/`, `scripts/`, plus
`pyproject.toml`, `openapi.yaml`, `config.py`, `init.py`, `main.py`,
`worker.py`, `Dockerfile`, `docker-compose.yaml`, `.env.example`.
2. **Author the spec (app-specific)** — write `openapi.yaml`: resources,
schemas, `operationId`s, security, and extension metadata. This is the
contract and drives all routing.
contract and drives all routing. Every operation MUST have an `operationId`.
3. **Write each CRUD slice (app-specific)** — per resource create the four-file
slice: `models.py`, `ports/db.py`, `ports/service.py`, and optional
`converters.py`. Follow the CRUD template.
3. **Generate the scaffold (codegen)** — from the spec, generate the wire
models and the exact handler signatures:
```bash
openapi-first models openapi.yaml -o _gen/models.py
openapi-first routes openapi.yaml -o _gen/routes \
--use-models --models-module _gen.models --inject-request --package
```
`_gen/models.py` is the **wire contract** (Pydantic, one class per
`components.schemas` entry). `_gen/routes/<resource>/routes.py` lists every
`operationId` with its exact parameter signature. Treat `_gen/` as generated
(never hand-edit it) — the canonical handlers live in `interfaces/`.
4. **Wire the composition root** — in `init.py`, build leaf repositories, then
4. **Write each CRUD slice (app-specific)** — per resource create the four-file
slice: `models.py` (domain model, **no `id`**), `ports/db.py`
(`class XDocument(BaseDocument, X)` — id comes from `BaseDocument`),
`ports/service.py`, and optional `converters.py`. Follow the CRUD template.
5. **Wire the composition root** — in `init.py`, build leaf repositories, then
dependent repositories, then services; add infra adapters (db, cache, stores,
progress bus, fetchers). Expose `get_svc` overloads.
5. **Add routes** — create `interfaces/<resource>/routes.py` thin handlers bound
to `operationId`s and re-export them in `interfaces/openapi.py`. Add the cache
6. **Add routes** — create `interfaces/<resource>/routes.py` thin handlers whose
names match the `operationId`s (use the signatures from step 3 as your
checklist) and re-export them in `interfaces/openapi.py`. Add the cache
decorator to read endpoints.
6. **Bootstrap app and worker** — confirm `main.py` runs the OpenAPI-first app
7. **Bootstrap app and worker** — confirm `main.py` runs the OpenAPI-first app
with its lifespan. If the app has long-running jobs, adapt `worker.py` to poll
the job collection, run the pipeline, and publish progress.
7. **Add pipeline families (only if data-driven)** — copy `pipelines/example/`
8. **Add pipeline families (only if data-driven)** — copy `pipelines/example/`
to `pipelines/<family>/` with `abc.py`, `impl.py`, `pipeline.yaml`,
`resources/`, and reuse `pipelines/shared/` (loader + registry). Register the
root. Add fixtures and a parity test.
8. **Add infrastructure only where needed** — Tier A capability: use the
9. **Add infrastructure only where needed** — Tier A capability: use the
library. Tier B capability: copy the template. Never hand-write a Tier A
library.
9. **Tests** — integration tests with fakes at the HTTP edge; unit tests for
domain and pipeline logic; a config-consistency test for rule-driven
pipelines.
10. **Tests** — integration tests with fakes at the HTTP edge; unit tests for
domain and pipeline logic; a config-consistency test for rule-driven
pipelines. Include `tests/integration/test_openapi_app_builds.py` (imports
the app, asserts `import main` binds every operation).
10. **Scripts and seeds** — operator scripts under `scripts/` using
11. **Scripts and seeds** — operator scripts under `scripts/` using
`init()` / `get_svc()` / `shutdown()`; run as `python -m scripts.<name>`.
11. **Docs** — author GSDFC docstrings, generate `.pyi` stubs, mark the package
12. **Docs** — author GSDFC docstrings, generate `.pyi` stubs, mark the package
`py.typed`, and build the docs.
**Definition of done:**
@@ -123,15 +139,39 @@ The steps map onto the layers as follows:
|---------------------|-----------------------------------|---------------------------------------------|
| 1 · Scaffold | whole `templates/app/` | — |
| 2 · Spec | `openapi.yaml` | — |
| 3 · CRUD slices | `domain/<resource>/` | spec, CRUD template |
| 4 · Wiring | `init.py` | every slice |
| 5 · Routes | `interfaces/<resource>/` | services, openapi route registry |
| 6 · Bootstrap | `main.py`, `worker.py` | composition root |
| 7 · Pipelines | `pipelines/<family>/`, `shared/` | library pipeline engine |
| 8 · Infra | `infra/` | domain ports |
| 9 · Tests | `tests/` | app + fixtures |
| 10 · Scripts | `scripts/` | composition root |
| 11 · Docs | docstrings + docs build | code, Doc Forge |
| 3 · Codegen | `_gen/` (generated) | spec, openapi-first |
| 4 · CRUD slices | `domain/<resource>/` | spec, CRUD template |
| 5 · Wiring | `init.py` | every slice |
| 6 · Routes | `interfaces/<resource>/` | services, openapi route registry |
| 7 · Bootstrap | `main.py`, `worker.py` | composition root |
| 8 · Pipelines | `pipelines/<family>/`, `shared/` | library pipeline engine |
| 9 · Infra | `infra/` | domain ports |
| 10 · Tests | `tests/` | app + fixtures |
| 11 · Scripts | `scripts/` | composition root |
| 12 · Docs | docstrings + docs build | code, Doc Forge |
---
## 🔧 The codegen fast path, in detail
`openapi-first` is spec → code only (it never generates a spec). Use it to erase
the two mechanical parts of the build:
| Generated artifact | Replaces | Notes |
|--------------------|----------|-------|
| `_gen/models.py` | hand-writing Pydantic wire models | one class per `components.schemas` entry |
| `_gen/routes/<resource>/routes.py` | hand-typing handler signatures | names == `operationId`s; `request: Request` injected |
Rules of the road:
- **Wire models are not domain models.** `_gen/models.py` is the HTTP contract;
`domain/<resource>/models.py` is your business shape. If the spec's schema has
a required `id: str`, keep it off the domain model and let the document get its
id from `BaseDocument`.
- **Resource names are sanitized.** `/fetch-requests` → `fetch_requests.py`.
- **Multipart bodies expand** to `UploadFile = File(...)` / `Form(...)` params.
- **`_gen/` is disposable.** Regenerate on every spec change; `git diff` shows
exactly what moved.
---

View File

@@ -70,8 +70,34 @@ async def create_item(request: Request, body: dict):
- Security is declared once in the spec and enforced by the framework; handlers
contain no auth code.
## Authless apps
Auth is **deferred** in the standard: the same spec-first app runs with or
without it, because security lives entirely in the spec, never in handlers.
For an app that needs no auth yet, do **not** invent per-handler auth checks —
just omit the security keys:
- Remove the top-level `security:` block.
- Remove `components.securitySchemes`.
- Do **not** add `security: []` to operations. An explicit empty array is a
per-operation *override* that means "public", and it only matters when a
global `security` block exists. Leaving every operation without a `security`
key is the cleanest authless spec.
The handlers, services, and composition root are unchanged. When auth is needed
later, add the global `security` block and an introspecting dependency; only
handlers that must opt out get `security: []`.
## Codegen
The `openapi_first` library can scaffold models and route stubs from the spec,
and ships app templates (CRUD app, model app, service). Prefer codegen over
hand-writing boilerplate.
`openapi_first` scaffolds models and route stubs from the spec (see the
[Build Recipe](01_build_recipe.md), step 3):
```bash
openapi-first models openapi.yaml -o _gen/models.py
openapi-first routes openapi.yaml -o _gen/routes \
--use-models --models-module _gen.models --inject-request --package
```
Prefer codegen over hand-writing boilerplate. The result is the wire contract
and a set of `operationId`-named signatures — not the canonical handlers.

View File

@@ -18,9 +18,36 @@ domain/<resource>/
converters.py # (optional) transforms
```
## The id rule (model vs. document)
Two different objects, two different jobs:
| Object | Lives in | Has `id`? | Purpose |
|---|---|---|---|
| **Domain model** | `domain/<resource>/models.py` | **No** | business shape, framework-free |
| **Document** | `domain/<resource>/ports/db.py` | **Yes** (from `BaseDocument`) | Mongo persistence shape |
`BaseDocument` (mongo-ops) supplies `id: Optional[PyObjectId]` (aliased to
`_id`) plus `created_at` / `updated_at`. The domain model must **not** declare
`id`; Mongo owns it.
> **Trap — generated wire models.** `openapi-first models` produces the HTTP
> *contract* (see the [Build Recipe](01_build_recipe.md), step 3). Response
> schemas frequently declare a required `id: str`, so a generated class is
> **not** a domain model. If you write
> `class ItemDocument(Item, BaseDocument)` and `Item` re-exports a required
> `id: str`, that field shadows `BaseDocument.id` and no Mongo `_id` can ever be
> set. Fix it one of three ways:
>
> 1. **Preferred:** keep the domain model id-free (hand-author it from the CRUD
> template), and keep the required `id` only on the wire model.
> 2. Order the bases so the document wins:
> `class ItemDocument(BaseDocument, Item)`.
> 3. Keep a separate document type and translate with `ports/converters.py`.
## models.py
A framework-free Pydantic model describing the business shape.
A framework-free Pydantic model describing the business shape (no `id`).
```python
class Item(BaseModel):
@@ -34,7 +61,8 @@ The persistence model extends the business model and adds database concerns.
The repository factory selects the right repository flavour.
```python
class ItemDocument(Item, BaseDocument): # BaseDocument adds id/created_at/...
# Item is id-free; BaseDocument adds id/created_at/updated_at.
class ItemDocument(Item, BaseDocument):
pass
def create_item_repo(*, owner_repo=None) -> ItemRepository:

View File

@@ -31,25 +31,37 @@ for its API. Writing a private copy of any of these is a defect.
These are reusable internal components shipped **inside** the template — they are
not independent libraries. Copy them as-is, then rename/populate only what is
app-specific. They give every app the same wiring shape as the reference app.
Fetch them by kind with `get_app_component`, or the whole tree with
`get_app_template`.
| Component | Template location | Action |
|---|---|---|
| Layering & dependency rule | the package layout + import rules | keep |
| Composition root shape | `init.py` (`get_svc`/`get_db`, idempotency) | keep |
| Spec-first web edge | `interfaces/openapi.py`, thin handlers bound to `operationId` | keep |
| CRUD slice pattern | `domain/<resource>/{models.py, ports/db.py, ports/service.py, converters.py}` | rename + fill |
| Generic CRUD service | `infra/adapters/service.py` (`CrudsServiceAdaptor`) | keep |
| Repository factory | `infra/db/repos/mongo.py` (`_repo_factory`) | keep |
| Worker poll + status loop | `worker.py` | keep |
| Progress bus + streaming | bus port + in-memory adapter + SSE endpoint | keep |
| Human-in-the-loop | durable pause + question document + resolve endpoint + resume map | keep |
| HTTP response cache | `cache_response` decorator + generation invalidation | keep |
| Key-value store | KV store port + Redis adapter + in-memory fallback | keep |
| LLM client pool | provider-select + retry pool scaffold | keep |
| Pipeline scaffold | `pipelines/shared/{ports,loader,registry}.py` + `pipelines/<family>/` | rename + fill |
| Operator scripts | `scripts/` (`init()` / `get_svc()` / `shutdown()`) | rename |
| Testing harness | integration fakes + fixture-parity pattern | keep |
| Project scaffolding | `pyproject.toml`, `Dockerfile`, `docker-compose.yaml`, `.env.example`, `openapi.yaml` | rename + fill |
| Component | Template location | Action | Shipped |
|---|---|---|---|
| Layering & dependency rule | the package layout + import rules | keep | ✅ |
| Composition root shape | `init.py` (`get_svc`/`get_db`, idempotency) | keep | ✅ |
| Spec-first web edge | `interfaces/openapi.py`, thin handlers bound to `operationId` | keep | ✅ |
| CRUD slice pattern | `domain/<resource>/{models.py, ports/db.py, ports/service.py, converters.py}` | rename + fill | ✅ |
| Generic CRUD service | `infra/adapters/service.py` (`CrudsServiceAdaptor`) | keep | ✅ |
| Repository factory | `infra/db/repos/mongo.py` (`_repo_factory`) | keep | ✅ |
| Worker poll + status loop | `worker.py` | keep | ✅ |
| Progress bus | `infra/adapters/sse.py` (keyed in-memory bus) + an app SSE endpoint | keep | ✅ |
| HTTP response cache | `infra/http_cache.py` (`cache_response` + generation invalidation) | keep | ✅ |
| Key-value store | `infra/adapters/kv.py` (port + in-memory + Redis adaptor) | keep | ✅ |
| Pipeline scaffold | `pipelines/shared/{ports,loader,registry}.py` + `pipelines/<family>/` | rename + fill | ✅ |
| Operator scripts | `scripts/` (`init()` / `get_svc()` / `shutdown()`) | rename | ✅ |
| Testing harness | integration fakes + fixture-parity pattern | keep | ✅ |
| Project scaffolding | `pyproject.toml`, `Dockerfile`, `docker-compose.yaml`, `.env.example`, `openapi.yaml` | rename + fill | ✅ |
## Tier B (patterns) — port from the reference app
A few Tier B capabilities are real and reusable, but are **shaped by the app or
the provider**, so the template does not ship them verbatim — copying them
blindly would carry app-specific names. Treat them as patterns: lift the shape
from the reference app and rename per app.
| Component | Reference location | Action | Shipped |
|---|---|---|---|
| Human-in-the-loop | durable pause + question document + resolve endpoint + resume map | port + reshape | ✗ |
| LLM client pool | provider-select + retry/rotation pool scaffold | port + reshape | ✗ |
## Tier C — App-specific (must be authored)

View File

@@ -8,7 +8,7 @@ is a defect.
Each library also exposes its own documentation as an MCP server. An AI agent
can read a library's full API by connecting to its MCP and reading
`doc://index`, `doc://nav`, and `doc://modules/{module}`.
`docs://index`, `docs://nav`, and `docs://modules/{module}`.
---
@@ -48,8 +48,8 @@ endpoint. Query it before writing any infrastructure.
## Notes
- **Hexa** is a full library with its own MCP server on `:8008`: read
`doc://index`, `doc://nav`, and `doc://modules/{module}` (e.g.
`doc://modules/hexa.container`) to discover the DI surface — `build(cls, *,
`docs://index`, `docs://nav`, and `docs://modules/{module}` (e.g.
`docs://modules/hexa.container`) to discover the DI surface — `build(cls, *,
instances, config)` and the `PortNode` model.
- **Auth Server** is deferred: it has no `pyproject.toml`/importable package and
no MCP bundle. Do not reach for it yet; `py-jwt` covers token primitives.

View File

@@ -0,0 +1,147 @@
"""# Summary
Key-value store port with in-memory and Redis adaptors.
---
Reserve infrastructure for caching small values by key (sessions, locks,
result blobs, rate counters). The in-memory adaptor needs no services and is a
safe default; the Redis adaptor is opt-in and imports ``redis`` lazily so it
never becomes a hard dependency.
Example:
```python
from infra.adapters.kv import InMemoryKV
kv = InMemoryKV()
await kv.set("greeting", "hello", ttl=30)
assert await kv.get("greeting") == "hello"
```
"""
from __future__ import annotations
from abc import ABC, abstractmethod
from typing import Dict, List, Optional
class KVStore(ABC):
"""Abstract key-value store port."""
@abstractmethod
async def get(self, key: str) -> Optional[str]:
"""Return the value at ``key``, or ``None``."""
@abstractmethod
async def set(self, key: str, value: str, ttl: Optional[int] = None) -> None:
"""Store ``value`` at ``key`` with an optional TTL in seconds."""
@abstractmethod
async def delete(self, key: str) -> None:
"""Delete ``key`` if present."""
@abstractmethod
async def scan(self, pattern: str) -> List[str]:
"""Return keys matching a glob ``pattern``."""
async def clear(self, pattern: str = "*") -> None:
"""Delete all keys matching ``pattern``.
Args:
pattern (str): Glob-style key pattern.
"""
for key in await self.scan(pattern):
await self.delete(key)
async def close(self) -> None:
"""Release any underlying resources. Default is a no-op."""
class InMemoryKV(KVStore):
"""Process-local key-value store used as a dependency-free fallback.
Attributes:
_data (Dict[str, str]): In-memory value map.
_expiry (Dict[str, float]): Absolute expiry timestamps by key; ``0.0``
means "no expiry".
"""
def __init__(self) -> None:
"""Initialize an empty store with no imports or connections."""
self._data: Dict[str, str] = {}
self._expiry: Dict[str, float] = {}
def _expired(self, key: str) -> bool:
import time
deadline = self._expiry.get(key, 0.0)
if deadline and deadline < time.time():
self._data.pop(key, None)
self._expiry.pop(key, None)
return True
return False
async def get(self, key: str) -> Optional[str]:
"""Return the value at ``key``, or ``None`` when absent/expired."""
if self._expired(key):
return None
return self._data.get(key)
async def set(self, key: str, value: str, ttl: Optional[int] = None) -> None:
"""Store ``value`` at ``key`` with an optional TTL in seconds."""
import time
self._data[key] = value
self._expiry[key] = (time.time() + ttl) if ttl else 0.0
async def delete(self, key: str) -> None:
"""Delete ``key`` if present."""
self._data.pop(key, None)
self._expiry.pop(key, None)
async def scan(self, pattern: str) -> List[str]:
"""Return live keys matching a glob ``pattern``."""
import fnmatch
return [k for k in list(self._data) if fnmatch.fnmatch(k, pattern) and not self._expired(k)]
class RedisKV(KVStore):
"""Redis-backed key-value store.
Attributes:
_redis: Asynchronous Redis client (imported lazily).
"""
def __init__(self, url: str = "redis://localhost:6379/0", decode_responses: bool = True):
"""Initialize the store with a Redis connection.
Args:
url (str): Redis connection URL.
decode_responses (bool): Whether Redis returns decoded strings.
Raises:
ImportError: When the optional ``redis`` package is not installed.
"""
from redis.asyncio import Redis
self._redis = Redis.from_url(url, decode_responses=decode_responses)
async def get(self, key: str) -> Optional[str]:
"""Return the value at ``key``, or ``None``."""
return await self._redis.get(key)
async def set(self, key: str, value: str, ttl: Optional[int] = None) -> None:
"""Store ``value`` at ``key`` with an optional TTL in seconds."""
await self._redis.set(key, value, ex=ttl)
async def delete(self, key: str) -> None:
"""Delete ``key`` if present."""
await self._redis.delete(key)
async def scan(self, pattern: str) -> List[str]:
"""Return keys matching a glob ``pattern``."""
return [key async for key in self._redis.scan_iter(match=pattern)]
async def close(self) -> None:
"""Close the underlying Redis connection."""
await self._redis.aclose()

View File

@@ -0,0 +1,54 @@
"""# Summary
SSEInMemoryBus — in-memory event bus for SSE progress events.
---
Buffer published progress events so they can be replayed (and streamed) per
correlation key, such as a job, upload, or fetch-request id.
"""
from typing import Generic, List, TypeVar
T = TypeVar("T")
class SSEInMemoryBus(Generic[T]):
"""Buffer SSE progress events in memory, keyed by an event attribute.
Attributes:
_events (List[T]): Events published since the last clear.
_key_attr (str): Event attribute used to correlate a stream.
"""
def __init__(self, key_attr: str = "id"):
"""Initialize an empty event buffer.
Args:
key_attr (str): Attribute name on each event holding the stream
correlation key (for example ``"job_id"``).
"""
self._events: List[T] = []
self._key_attr = key_attr
async def publish(self, event: T) -> None:
"""Append an event to the buffer.
Args:
event (T): Event to append.
"""
self._events.append(event)
def get_events(self, key: str) -> List[T]:
"""Return buffered events for a correlation key.
Args:
key (str): Value matched against the configured ``key_attr``.
Returns:
List[T]: Events whose ``key_attr`` equals ``key``.
"""
return [e for e in self._events if getattr(e, self._key_attr, None) == key]
def clear(self) -> None:
"""Remove all buffered events."""
self._events.clear()

View File

@@ -0,0 +1,229 @@
"""# Summary
Domain-agnostic HTTP response cache.
---
A thin Redis-backed response cache that wraps route handlers as a decorator.
Example:
```python
from infra.http_cache import cache_response
@cache_response(ttl=60)
async def list_items(request, skip: int = 0, limit: int = 100):
...
```
Notes:
The cache operates directly on the endpoint and is independent of any
repository-level cache. Cached responses expire via TTL. When no
``http_cache_store`` is wired on ``request.app.state.db_resources`` the
decorator is a no-op pass-through, so applying/removing it is safe.
"""
import hashlib
import json
from functools import wraps
from typing import Any, Callable, Optional
from fastapi import Request, Response
from fastapi.encoders import jsonable_encoder
CACHE_PREFIX = "apicache:"
NS_PREFIX = CACHE_PREFIX + "ns:"
DEFAULT_TTL = 60
class HttpCacheStore:
"""Store string responses in Redis with TTL under the ``apicache:`` key space.
Attributes:
_redis (Redis): Asynchronous Redis client bound to the configured URL.
"""
def __init__(self, url: str = "redis://localhost:6379/0", decode_responses: bool = True):
"""Initialize the store with a Redis connection.
Args:
url (str): Redis connection URL.
decode_responses (bool): Whether Redis returns decoded strings.
Raises:
ImportError: When the optional ``redis`` package is not installed.
"""
from redis.asyncio import Redis
self._redis = Redis.from_url(url, decode_responses=decode_responses)
async def get(self, key: str) -> Optional[str]:
"""Read the value stored at ``key``.
Args:
key (str): Cache key to read.
Returns:
Optional[str]: Cached value, or ``None`` when absent.
"""
return await self._redis.get(CACHE_PREFIX + key)
async def set(self, key: str, value: str, ttl: int) -> None:
"""Store ``value`` at ``key`` with a TTL.
Args:
key (str): Cache key to write.
value (str): Serialized value to persist.
ttl (int): Lifetime in seconds.
"""
await self._redis.set(CACHE_PREFIX + key, value, ex=ttl)
async def delete(self, key: str) -> None:
"""Delete the value stored at ``key``.
Args:
key (str): Cache key to remove.
"""
await self._redis.delete(CACHE_PREFIX + key)
async def generation(self, resource: str) -> str:
"""Return the current generation counter for a resource namespace.
Args:
resource (str): Resource namespace to read.
Returns:
str: Generation counter, or ``"0"`` when unset.
"""
value = await self._redis.get(NS_PREFIX + resource)
return value if value is not None else "0"
async def invalidate(self, *resources: str) -> None:
"""Invalidate all cached responses for the given resource namespaces.
Args:
*resources (str): Resource namespaces to invalidate.
Notes:
Bumps each namespace's generation counter so existing entries become
unreachable immediately; Redis reclaims them at TTL expiry.
"""
if not resources:
return
pipe = self._redis.pipeline(transaction=False)
for resource in resources:
pipe.incr(NS_PREFIX + resource)
await pipe.execute()
async def close(self) -> None:
"""Close the underlying Redis connection."""
await self._redis.aclose()
def _user_scope(request: Request) -> str:
"""Build a stable per-user cache scope.
Args:
request (Request): Incoming HTTP request.
Returns:
str: Scoped identifier, or ``""`` for auth-free endpoints.
"""
user = getattr(request.state, "user", None)
if isinstance(user, dict):
for field in ("username", "id", "email"):
if user.get(field):
return str(user[field])
token = getattr(request.state, "token", None)
if token:
return hashlib.sha256(str(token).encode()).hexdigest()[:16]
return ""
def _resource_name(request: Request) -> str:
"""Return the first path segment identifying the resource.
Args:
request (Request): Incoming HTTP request.
Returns:
str: First path segment (for example ``"items"``), or ``"root"`` when
the path is empty.
"""
return request.url.path.strip("/").split("/", 1)[0] or "root"
def _cache_key(request: Request, generation: str = "0") -> str:
"""Compute a stable cache key for a request.
Args:
request (Request): Incoming HTTP request.
generation (str): Resource generation counter guarding stale entries.
Returns:
str: SHA-256 digest of the request method, path, query and path
parameters, user scope, and generation.
"""
parts = [
request.method,
request.url.path,
"&".join(f"{k}={v}" for k, v in sorted(request.query_params.items())),
"&".join(f"{k}={v}" for k, v in sorted(request.path_params.items())),
_user_scope(request),
generation,
]
return hashlib.sha256("|".join(parts).encode()).hexdigest()
def cache_response(ttl: Optional[int] = None) -> Callable:
"""Decorate a GET-style route handler to cache its JSON response.
Args:
ttl (Optional[int]): Cache lifetime in seconds; defaults to
``DEFAULT_TTL``.
Returns:
Callable: Decorator that wraps the route handler.
Notes:
The wrapped handler must accept a ``request: Request`` parameter (as
every openapi_first handler does). Responses that are already a
``fastapi.Response`` (for example 204) are returned untouched and
never cached. When no ``http_cache_store`` is wired on
``request.app.state.db_resources`` the decorator passes through to the
handler without caching.
"""
ttl = ttl if ttl is not None else DEFAULT_TTL
def decorator(func: Callable) -> Callable:
@wraps(func)
async def wrapper(*args: Any, **kwargs: Any):
request = kwargs.get("request")
if request is None:
request = next((a for a in args if isinstance(a, Request)), None)
if request is None:
return await func(*args, **kwargs)
db_resources = getattr(request.app.state, "db_resources", None) or {}
store: Optional[HttpCacheStore] = db_resources.get("http_cache_store")
if store is None:
return await func(*args, **kwargs)
key = _cache_key(request, await store.generation(_resource_name(request)))
cached = await store.get(key)
if cached is not None:
return Response(content=cached, media_type="application/json")
result = await func(*args, **kwargs)
if isinstance(result, Response):
return result
payload = json.dumps(
jsonable_encoder(result),
separators=(",", ":"),
ensure_ascii=False,
)
await store.set(key, payload, ttl)
return Response(content=payload, media_type="application/json")
return wrapper
return decorator

View File

@@ -11,6 +11,7 @@ dependencies = [
"uvicorn[standard]",
"pydantic>=2",
"motor",
"redis",
"python-dotenv",
"pyyaml",
"openapi-first",

View File

@@ -0,0 +1,44 @@
import importlib
import yaml
import interfaces.openapi as routes_module
from openapi_first.app import OpenAPIFirstApp
_HTTP_METHODS = {"get", "post", "put", "patch", "delete", "head", "options"}
def _operation_ids(spec: dict) -> list[str]:
"""Collect every declared operationId from an OpenAPI spec.
Args:
spec (dict): Parsed OpenAPI document.
Returns:
list[str]: Operation ids in declaration order.
"""
ids: list[str] = []
for path_item in (spec.get("paths") or {}).values():
for method, operation in path_item.items():
if method.lower() in _HTTP_METHODS and isinstance(operation, dict):
if operation.get("operationId"):
ids.append(operation["operationId"])
return ids
def test_openapi_app_builds():
"""Every operationId in the spec must resolve to a handler, and the app must build."""
spec = yaml.safe_load(open("openapi.yaml", encoding="utf-8"))
module = importlib.import_module(routes_module.__name__)
missing = [oid for oid in _operation_ids(spec) if not hasattr(module, oid)]
assert not missing, f"handlers missing for operationIds: {missing}"
app = OpenAPIFirstApp(
openapi_path="openapi.yaml",
routes_module=module,
title="App API",
version="0.1.0",
)
assert app is not None

View File

@@ -7,7 +7,7 @@ copying Tier B template components.
> **Doc model:** this page is the human entry point. The deterministic,
> machine-facing checklist lives in the reference page
> [Build Recipe](../lib/01_build_recipe/), served as `doc://modules/build_recipe`.
> [Build Recipe](../lib/01_build_recipe/), served as `docs://modules/build_recipe`.
---
@@ -20,7 +20,9 @@ Gather these before starting — the `start_app` MCP prompt asks for them:
`openapi.yaml` if you already have it.
3. **Axis 2 — pipelines.** Only if an input needs a multi-step transformation:
the families and their input formats (or "none").
4. **Auth needed now?** Default no; auth is deferred (`py-jwt` only).
4. **Auth needed now?** Default **no**. Auth is deferred (`py-jwt` primitives
only), and an authless app is just a spec with no `security` keys — see
[Spec-First Routing → Authless apps](../lib/04_spec_first_routing/#authless-apps).
5. **Integrations.** Mongo, ingestion formats, email, LLM provider.
If you can state axes 1 and 2, the rest is mechanical.
@@ -31,13 +33,18 @@ If you can state axes 1 and 2, the rest is mechanical.
From an MCP client connected to the `standards` server:
1. Invoke the **`start_app`** prompt with no arguments to get the intake
questions.
2. Answer them, then invoke
`start_app(app_name=..., resources=..., pipelines=..., auth=...)`.
3. The prompt hands off to `doc://modules/build_recipe`; the agent pulls exact
files with `get_app_component(kind, resource=..., family=...)` and authors
only the app-specific parts.
1. Invoke the **`intake`** prompt (or `start_app` with no arguments) to get the
intake questions.
2. Answer them, then invoke **`build`**
(`build(app_name=..., resources=..., pipelines=..., auth=...)`), or use
**`start_app`** to intake-and-build in one call.
3. The prompt hands off to `docs://modules/build_recipe`; the agent scaffolds
with `get_app_template()` and pulls exact files with
`get_app_component(kind, resource=..., family=...)`, authoring only the
app-specific parts.
4. To validate the standard without touching a real app, invoke **`trial`**
(`trial(app_name=..., build_path=...)`) to build into a scratch directory,
prove it builds, and report findings.
Manual alternative — copy the skeleton and follow the reference recipe:

View File

@@ -55,8 +55,8 @@ Registry](../lib/13_library_registry/).
## 🔗 Related Resources
- **MCP bundle** — `standards` server on `:8009`: read `doc://index`,
`doc://nav`, `doc://modules/{module}`; call `find_library(capability)`
- **MCP bundle** — `standards` server on `:8009`: read `docs://index`,
`docs://nav`, `docs://modules/{module}`; call `find_library(capability)`
- **Library registry** — the catalog of internal libraries behind every app
(`libraries.json`, served as `registry://libraries`)
- **Template** — the runnable skeleton at `standards/src/templates/app/`

View File

@@ -538,7 +538,7 @@ copying Tier B template components.</p>
<blockquote>
<p><strong>Doc model:</strong> this page is the human entry point. The deterministic,
machine-facing checklist lives in the reference page
<a href="../lib/01_build_recipe/">Build Recipe</a>, served as <code>doc://modules/build_recipe</code>.</p>
<a href="../lib/01_build_recipe/">Build Recipe</a>, served as <code>docs://modules/build_recipe</code>.</p>
</blockquote>
<hr />
<h2 id="what-the-agent-needs-from-you">🧾 What the agent needs from you</h2>
@@ -549,7 +549,9 @@ machine-facing checklist lives in the reference page
<code>openapi.yaml</code> if you already have it.</li>
<li><strong>Axis 2 — pipelines.</strong> Only if an input needs a multi-step transformation:
the families and their input formats (or "none").</li>
<li><strong>Auth needed now?</strong> Default no; auth is deferred (<code>py-jwt</code> only).</li>
<li><strong>Auth needed now?</strong> Default <strong>no</strong>. Auth is deferred (<code>py-jwt</code> primitives
only), and an authless app is just a spec with no <code>security</code> keys — see
<a href="../lib/04_spec_first_routing/#authless-apps">Spec-First Routing → Authless apps</a>.</li>
<li><strong>Integrations.</strong> Mongo, ingestion formats, email, LLM provider.</li>
</ol>
<p>If you can state axes 1 and 2, the rest is mechanical.</p>
@@ -557,13 +559,18 @@ machine-facing checklist lives in the reference page
<h2 id="how-to-start">▶️ How to start</h2>
<p>From an MCP client connected to the <code>standards</code> server:</p>
<ol>
<li>Invoke the <strong><code>start_app</code></strong> prompt with no arguments to get the intake
questions.</li>
<li>Answer them, then invoke
<code>start_app(app_name=..., resources=..., pipelines=..., auth=...)</code>.</li>
<li>The prompt hands off to <code>doc://modules/build_recipe</code>; the agent pulls exact
files with <code>get_app_component(kind, resource=..., family=...)</code> and authors
only the app-specific parts.</li>
<li>Invoke the <strong><code>intake</code></strong> prompt (or <code>start_app</code> with no arguments) to get the
intake questions.</li>
<li>Answer them, then invoke <strong><code>build</code></strong>
(<code>build(app_name=..., resources=..., pipelines=..., auth=...)</code>), or use
<strong><code>start_app</code></strong> to intake-and-build in one call.</li>
<li>The prompt hands off to <code>docs://modules/build_recipe</code>; the agent scaffolds
with <code>get_app_template()</code> and pulls exact files with
<code>get_app_component(kind, resource=..., family=...)</code>, authoring only the
app-specific parts.</li>
<li>To validate the standard without touching a real app, invoke <strong><code>trial</code></strong>
(<code>trial(app_name=..., build_path=...)</code>) to build into a scratch directory,
prove it builds, and report findings.</li>
</ol>
<p>Manual alternative — copy the skeleton and follow the reference recipe:</p>
<div class="language-bash 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></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>cp<span class="w"> </span>-r<span class="w"> </span>&lt;docs&gt;/standards/src/templates/app<span class="w"> </span>apps/&lt;your-app&gt;

View File

@@ -588,8 +588,8 @@ Registry</a>.</p>
<hr />
<h2 id="related-resources">🔗 Related Resources</h2>
<ul>
<li><strong>MCP bundle</strong> — <code>standards</code> server on <code>:8009</code>: read <code>doc://index</code>,
<code>doc://nav</code>, <code>doc://modules/{module}</code>; call <code>find_library(capability)</code></li>
<li><strong>MCP bundle</strong> — <code>standards</code> server on <code>:8009</code>: read <code>docs://index</code>,
<code>docs://nav</code>, <code>docs://modules/{module}</code>; call <code>find_library(capability)</code></li>
<li><strong>Library registry</strong> — the catalog of internal libraries behind every app
(<code>libraries.json</code>, served as <code>registry://libraries</code>)</li>
<li><strong>Template</strong> — the runnable skeleton at <code>standards/src/templates/app/</code></li>

File diff suppressed because one or more lines are too long