Files
docs/standards/wiki/search/search_index.json
Vishesh 'ironeagle' Bangotra 5a1204fbcb
All checks were successful
continuous-integration/drone/push Build is passing
Add npm backfill pipeline, shared htpasswd registry auth docs
- drone npm template gains backfill-npm-from-tags (mirrors PyPI)
- document the shared /auth/htpasswd credential model (npm + PyPI +
  docker) and Verdaccio's bcrypt requirement
- point doc-weaver source/path at ../react/doc-weaver
2026-10-21 00:57:08 +05:30

1 line
55 KiB
JSON

{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"\ud83e\udde9 Standards \u2014 the Aetos App Blueprint","text":"<p>One reusable architecture for every backend application in the Aetos ecosystem: a spec-first HTTP edge, a framework-free domain core, a pluggable infrastructure layer, and a single composition root.</p> <p>Fill in two axes only and a new app is done: your resource model, and (if needed) your pipeline family. The blueprint itself never names a business domain \u2014 expenses, health metrics, inventory, and analytics are all the same shape underneath.</p> <p>Doc model: this wiki is written for humans \u2014 how-to guides, the build recipe, and quality gates. The enforceable rules (layering, composition root, CRUD template, pipeline scaffold) and the library registry live in the reference pages, mirrored by the machine-readable <code>standards</code> MCP bundle. Query <code>find_library</code> before writing any infrastructure.</p>"},{"location":"#key-features","title":"\ud83d\ude80 Key Features","text":"<ul> <li>\ud83e\udded Spec-first routing \u2014 the OpenAPI document, not decorator routers, is the source of truth (operationId lookup)</li> <li>\ud83c\udfd7\ufe0f One-way layering \u2014 <code>interfaces \u2192 domain \u2192 infra</code>, enforced</li> <li>\ud83d\udd0c Single composition root \u2014 every dependency is wired in exactly one place</li> <li>\ud83e\udde9 Generic, not app-specific \u2014 reusable rules versus two authoring axes</li> <li>\ud83d\udcda Library-backed \u2014 DB, DI, pipelines, ingestion, auth, and docs come from internal libraries, never hand-written</li> <li>\ud83d\udea6 Explicit state machines \u2014 durable job statuses, progress, and human-in-the-loop pauses</li> </ul>"},{"location":"#documentation-structure","title":"\ud83d\udcc1 Documentation Structure","text":"Section Description Overview Philosophy, the one-way dependency rule, architecture App Components The component model and how the pieces wire together Start an App The entry point: gather the two axes, then build Reference App The runnable vet sample \u2014 server, pipelines, client Testing &amp; Quality Test layout and acceptance gates <p>Reference (separate site, mirrors the MCP bundle): the enforceable anatomy \u2014 start at the Build Recipe, then the pillars: domains (Layering), the OpenAPI contract (Composition Root, Spec-First Routing, CRUD Template, Auth Integration), interfaces &amp; infra (Infra Adapters), UI (Auto-Rendered Admin), the optional pipelines layer (Pipeline Scaffold, Worker &amp; Progress, Human-in-the-Loop), and delivery/meta (Genericity Matrix, Library Registry).</p>"},{"location":"#related-resources","title":"\ud83d\udd17 Related Resources","text":"<ul> <li>MCP bundle \u2014 <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>Library registry \u2014 the catalog of internal libraries behind every app (<code>libraries.json</code>, served as <code>registry://libraries</code>)</li> <li>Template \u2014 the runnable sample at <code>standards/src/templates/app/</code> (server) and <code>standards/src/templates/client/</code> (React admin); walkthrough in the Reference App</li> <li>Start a build \u2014 invoke the <code>start_app</code> prompt on the <code>standards</code> server, or read the Start an App guide</li> </ul> <p>\u00a9 Aetoskia Internal \u2014 <code>standards</code></p>"},{"location":"01_overview/","title":"Overview","text":"<p>The blueprint is a hexagonal (ports &amp; adapters) application with a spec-first web edge and a single composition root. It favors explicit wiring over implicit magic and existing internal libraries over hand-written infrastructure. Start here to build the mental model; the enforceable details live in the reference pages.</p>"},{"location":"01_overview/#architecture","title":"\ud83c\udfd7\ufe0f Architecture","text":"<pre><code>flowchart LR\n subgraph EDGE[\"interfaces/ \u2014 HTTP edge\"]\n OAS[\"openapi.yaml&lt;br/&gt;operationId contract\"]\n RT[\"&lt;resource&gt;/routes.py&lt;br/&gt;thin handlers\"]\n end\n\n subgraph CORE[\"domain/ \u2014 business core\"]\n MD[\"models\"]\n PT[\"ports (adapter binding seam)\"]\n SV[\"services\"]\n end\n\n subgraph INFRA[\"infra/ \u2014 adapters\"]\n RP[\"mongo repos (mongo-ops)\"]\n AD[\"cache \u00b7 stores \u00b7 llm \u00b7 mail\"]\n DB[(\"MongoDB\")]\n end\n\n CR[\"init.py&lt;br/&gt;composition root\"]\n\n OAS --&gt; RT\n RT --&gt; SV\n SV --&gt; PT\n SV --&gt; RP\n PT -. depends on .- RP\n CR --- RT\n CR --- SV\n CR --- RP\n RP --&gt; DB\n AD --- CR</code></pre> <p>The rule holds at every edge: code in an outer ring may depend on a ring closer to the core, never the reverse. <code>domain/</code> never imports a web framework; only its <code>ports/</code> seam reaches into <code>infra/</code> to bind adapters.</p> <p>Legend \u2014 solid arrows are compile-time calls; dotted arrows are the port/adapter binding, performed by the composition root at startup.</p>"},{"location":"01_overview/#the-one-way-dependency-rule","title":"\ud83d\udd04 The one-way dependency rule","text":"Ring May import Never imports <code>interfaces/</code> <code>domain</code>, framework glue (glue only) business logic, <code>infra</code> directly <code>domain/</code> its own models + its <code>ports/</code> seam (which binds <code>infra</code>/libraries) web frameworks, <code>interfaces/</code> <code>infra/</code> <code>domain</code> ports, drivers, libraries \u2014 <code>init.py</code> everything (the wiring point) \u2014"},{"location":"01_overview/#core-principles","title":"\ud83e\udded Core principles","text":"<ol> <li>One-way dependencies. <code>interfaces -&gt; domain -&gt; infra</code> is allowed; the reverse is forbidden.</li> <li>Ports in the domain, adapters in infra. A service depends on a port; the port's factory binds the concrete adapter, and the composition root calls those factories.</li> <li>One composition root. Nothing constructs its own dependencies. All wiring lives in one module (<code>init.py</code>) exposed as a flat resource registry.</li> <li>Spec-first. The HTTP contract is declared in <code>openapi.yaml</code>; route handlers are looked up by <code>operationId</code>. There are no decorator routers.</li> <li>Reuse the libraries. Orchestration, DI, Mongo repositories, ingestion, auth, and docs come from internal libraries \u2014 see the Library Registry.</li> <li>Explicit state machines. Long-running work moves through explicit, durable statuses and can pause for human input.</li> </ol>"},{"location":"01_overview/#runtime-processes","title":"\u2699\ufe0f Runtime processes","text":"<p>A standard app may run as up to three processes that share the same composition root:</p> Process Entry point Responsibility API <code>main.py</code> serve HTTP; start/stop resources via lifespan Worker <code>worker.py</code> poll durable jobs, run pipelines, publish progress Scripts <code>python -m scripts.&lt;name&gt;</code> one-shot operator tasks"},{"location":"01_overview/#read-next","title":"\u27a1\ufe0f Read Next","text":"<ul> <li>02 \u2013 App Components \u00b7 03 \u2013 Start an App</li> </ul>"},{"location":"02_components/","title":"App Components","text":"<p>What ships in every standard app, and how the pieces relate. This is the blueprint's component model \u2014 <code>templates/app/</code> is a runnable instance of it.</p>"},{"location":"02_components/#component-model","title":"\ud83d\uddc2\ufe0f Component model","text":"<p>The blueprint has two orthogonal views: runtime flow (who calls whom) and composition-root wiring (who constructs whom). Keeping them separate makes the layering visible instead of hiding it behind a hub.</p>"},{"location":"02_components/#runtime-flow","title":"\ud83d\udd00 Runtime flow","text":"<p>Who calls whom at request and job time:</p> <pre><code>%%{init: {\"flowchart\": {\"nodeSpacing\": 50, \"rankSpacing\": 80}}}%%\nflowchart TB\n subgraph REQ[\"Request path \u2014 API process\"]\n direction LR\n CL([\"Client&lt;br/&gt;operationId\"]) --&gt; RT[\"interfaces/&amp;lt;resource&amp;gt;/routes.py\"]\n RT --&gt; SVC[\"&amp;lt;resource&amp;gt;_svc\"]\n SVC --&gt; REPO[\"&amp;lt;resource&amp;gt;_repo\"]\n end\n\n subgraph WRK[\"Background path \u2014 worker process\"]\n direction LR\n W[\"worker.py\"] --&gt; JSVC[\"job_svc\"]\n W --&gt; PIPE[\"pipelines/&amp;lt;family&amp;gt;\"]\n PIPE --&gt; ISVC[\"item_svc\"]\n JSVC --&gt; JREPO[\"job_repo\"]\n ISVC --&gt; IREPO[\"item_repo\"]\n end\n\n subgraph INFRA[\"infra/ \u2014 adapters\"]\n MONGO[(\"MongoDB / mongo_ops\")]\n end\n\n REPO --&gt; MONGO\n JREPO --&gt; MONGO\n IREPO --&gt; MONGO</code></pre> <p>Legend \u2014 solid arrows are runtime calls. Nothing is self-constructed.</p>"},{"location":"02_components/#composition-root-wiring","title":"\ud83e\uddf5 Composition-root wiring","text":"<p>Who constructs what at startup:</p> <pre><code>%%{init: {\"flowchart\": {\"nodeSpacing\": 50, \"rankSpacing\": 80}}}%%\nflowchart TB\n INIT[\"init.py \u2014 composition root\"]\n SETUP[\"infra/db/setup.py\"]\n FACT[\"domain/*/ports/*&lt;br/&gt;factory seams\"]\n REPOS[\"repos&lt;br/&gt;infra/db/repos/mongo.py\"]\n SVCES[\"services&lt;br/&gt;CrudsServiceAdaptor\"]\n STATE[\"app.state.db_resources\"]\n HEXA[\"hexa.build\"]\n RT[\"routes.py\"]\n WK[\"worker.py\"]\n\n INIT --&gt; SETUP\n INIT --&gt; FACT\n FACT --&gt; REPOS\n FACT --&gt; SVCES\n REPOS --&gt; SVCES\n HEXA -.-&gt; SVCES\n INIT --&gt; STATE\n STATE --&gt; RT\n INIT --&gt; WK</code></pre> <p>Legend \u2014 <code>init.py</code> is the only place that imports concrete adapters. The <code>ports/</code> factory seams bind <code>infra</code>; the API reads resources from <code>app.state.db_resources</code>, the worker calls <code>init()</code> directly.</p>"},{"location":"02_components/#component-roles","title":"\ud83d\udce6 Component roles","text":""},{"location":"02_components/#1-interfaces-the-edge","title":"1. <code>interfaces/</code> \u2014 the edge","text":"<p>Thin handlers bound to <code>openapi.yaml</code> <code>operationId</code>s. They parse the request, call a service, and shape the response. No business rules, no I/O.</p> <ul> <li><code>interfaces/openapi.py</code> re-exports every route module so the OpenAPI-first app can resolve handlers.</li> <li>Routes may mount the <code>cache</code> decorator on read endpoints.</li> </ul>"},{"location":"02_components/#2-domain-the-core","title":"2. <code>domain/</code> \u2014 the core","text":"<p>Models, ports, and services (use cases). No framework imports. The <code>ports/</code> package is the one seam allowed to reach into <code>infra/</code>: each port module declares the document shape and a factory that binds the adapter.</p> <ul> <li><code>&lt;aggregate&gt;/models.py</code> \u2014 dataclasses, enums, document shapes.</li> <li><code>&lt;aggregate&gt;/ports/db.py</code> \u2014 document model + <code>create_&lt;aggregate&gt;_repo()</code>.</li> <li><code>&lt;aggregate&gt;/ports/service.py</code> \u2014 service + <code>get_&lt;aggregate&gt;_service(repo)</code>.</li> <li>Services compose ports and enforce the business rules; the port factories bind the implementations, and the composition root calls them.</li> </ul>"},{"location":"02_components/#3-infra-the-adapters","title":"3. <code>infra/</code> \u2014 the adapters","text":"<p>Concrete implementations of the domain ports plus cross-cutting infrastructure:</p> <ul> <li><code>db/</code> \u2014 mongo-ops repositories and the connection lifecycle.</li> <li><code>adapters/</code> \u2014 cache store, progress bus, storages, fetchers (library-backed).</li> <li>Anything else the domain needs, implemented against its ports.</li> </ul>"},{"location":"02_components/#4-pipelines-data-driven-families-optional","title":"4. <code>pipelines/</code> \u2014 data-driven families (optional)","text":"<p>Extraction and enrichment families, each a deterministic step chain (<code>abc.py</code> / <code>impl.py</code> / <code>pipeline.yaml</code> / <code>resources/</code>). Roots are registered in the pattern registry and driven either by a worker job or a CLI.</p>"},{"location":"02_components/#5-workerpy-the-job-worker-optional","title":"5. <code>worker.py</code> \u2014 the job worker (optional)","text":"<p>A separate process that polls durable job collections, runs pipeline roots, and publishes progress to the progress bus. It shares the same composition root as the API.</p>"},{"location":"02_components/#6-initpy-the-composition-root","title":"6. <code>init.py</code> \u2014 the composition root","text":"<p>The only place anything is constructed: leaf repositories \u2192 dependent repositories \u2192 services \u2192 adapters. Exposes <code>get_svc</code> overloads and starts / stops resources on the app lifespan.</p>"},{"location":"02_components/#how-the-request-flows","title":"\ud83d\udd17 How the request flows","text":"<pre><code>sequenceDiagram\n participant C as Client\n participant R as route (interfaces)\n participant S as service (port factory / adapter)\n participant P as repo (infra)\n participant DB as MongoDB\n\n C-&gt;&gt;R: POST /items (operationId)\n R-&gt;&gt;S: svc.create(payload)\n S-&gt;&gt;P: repo.insert(doc)\n P-&gt;&gt;DB: insert\n DB--&gt;&gt;P: ack\n P--&gt;&gt;S: doc\n S--&gt;&gt;R: result\n R--&gt;&gt;C: 200 JSON</code></pre>"},{"location":"02_components/#related","title":"Related","text":"<ul> <li>01 \u2013 Overview \u00b7 03 \u2013 Start an App \u00b7 04 \u2013 Testing &amp; Quality</li> </ul>"},{"location":"03_start_an_app/","title":"\ud83d\ude80 Start an App","text":"<p>This is the entry point for building a new application from the blueprint. The goal is one deterministic pass: you supply only the two axes, and the agent does the rest by reading the <code>standards</code> MCP, importing Tier A libraries, and copying Tier B template components.</p> <p>Doc model: this page is the human entry point. The deterministic, machine-facing checklist lives in the reference page Build Recipe, served as <code>docs://modules/build_recipe</code>.</p>"},{"location":"03_start_an_app/#what-the-agent-needs-from-you","title":"\ud83e\uddfe What the agent needs from you","text":"<p>Gather these before starting \u2014 the <code>start_app</code> MCP prompt asks for them:</p> <ol> <li>App name + one-line purpose.</li> <li>Axis 1 \u2014 resources. Each resource, its fields, and relationships. Paste <code>openapi.yaml</code> if you already have it.</li> <li>Axis 2 \u2014 pipelines. Only if an input needs a multi-step transformation: the families and their input formats (or \"none\").</li> <li>Auth needed now? Default no. Auth is deferred (<code>py-jwt</code> primitives only), and an authless app is just a spec with no <code>security</code> keys \u2014 see Spec-First Routing \u2192 Authless apps.</li> <li>Integrations. Mongo, ingestion formats, email, LLM provider.</li> </ol> <p>If you can state axes 1 and 2, the rest is mechanical.</p>"},{"location":"03_start_an_app/#how-to-start","title":"\u25b6\ufe0f How to start","text":"<p>From an MCP client connected to the <code>standards</code> server:</p> <ol> <li>Invoke the <code>intake</code> prompt (or <code>start_app</code> with no arguments) to get the intake questions.</li> <li>Answer them, then invoke <code>build</code> (<code>build(app_name=..., resources=..., pipelines=..., auth=...)</code>), or use <code>start_app</code> 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)</code>, authoring only the app-specific parts.</li> <li>To validate the standard without touching a real app, invoke <code>trial</code> (<code>trial(app_name=..., build_path=...)</code>) to build into a scratch directory, prove it builds, and report findings.</li> </ol> <p>Manual alternative \u2014 copy the skeleton and follow the reference recipe:</p> <pre><code>cp -r &lt;docs&gt;/standards/src/templates/app apps/&lt;your-app&gt;\n</code></pre>"},{"location":"03_start_an_app/#reference","title":"\ud83d\udcda Reference","text":"<ul> <li>Build Recipe \u2014 the deterministic checklist</li> <li>Genericity Matrix \u2014 Tier A / B / C</li> <li>Library Registry \u2014 the Tier A catalog</li> <li>App Components \u2014 the component model</li> <li>Reference App \u2014 the runnable vet sample to copy</li> </ul>"},{"location":"03_start_an_app/#related","title":"Related","text":"<ul> <li>01 \u2013 Overview \u00b7 02 \u2013 App Components \u00b7 04 \u2013 Testing &amp; Quality</li> </ul>"},{"location":"05_mcp_setup/","title":"\ud83d\udd0c MCP Setup","text":"<p>Every repository ships its documentation as a Model Context Protocol (MCP) server, and the <code>standards</code> registry server ties them together. The <code>docs</code> portal runs each one as a FastMCP streamable-http app bound to <code>0.0.0.0:&lt;port&gt;</code>, exposing the endpoint at <code>/mcp</code> (see <code>docs/main.py</code> and <code>docs/config.yml</code>).</p> <p>Connect your agent to the <code>standards</code> server first: it answers <code>find_library</code>, <code>list_repos</code>, and the <code>start_app</code> prompt, so the agent reaches for an internal library instead of reimplementing one (see the Library Registry).</p>"},{"location":"05_mcp_setup/#run-the-servers-locally","title":"\u25b6\ufe0f Run the servers locally","text":"<p>From the <code>docs</code> checkout:</p> <pre><code>.venv\\Scripts\\python.exe main.py # every server\n.venv\\Scripts\\python.exe main.py --servers standards # just the registry\n</code></pre> <p><code>:8000</code> is the health endpoint (<code>GET /healthz</code>), not an MCP server.</p>"},{"location":"05_mcp_setup/#servers","title":"\ud83e\udded Servers","text":"Slug MCP server Port localhost mcp.aetoskia.com <code>standards</code> <code>standards</code> 8009 <code>http://localhost:8009/mcp</code> <code>https://mcp.aetoskia.com/standards/mcp</code> <code>doc-forge</code> <code>docforge</code> 8001 <code>http://localhost:8001/mcp</code> <code>https://mcp.aetoskia.com/doc-forge/mcp</code> <code>mail-intake</code> <code>mail_intake</code> 8002 <code>http://localhost:8002/mcp</code> <code>https://mcp.aetoskia.com/mail-intake/mcp</code> <code>omniread</code> <code>omniread</code> 8003 <code>http://localhost:8003/mcp</code> <code>https://mcp.aetoskia.com/omniread/mcp</code> <code>openapi-first</code> <code>openapi_first</code> 8004 <code>http://localhost:8004/mcp</code> <code>https://mcp.aetoskia.com/openapi-first/mcp</code> <code>py-jwt</code> <code>jwtlib</code> 8005 <code>http://localhost:8005/mcp</code> <code>https://mcp.aetoskia.com/py-jwt/mcp</code> <code>dagpipe</code> <code>dagpipe</code> 8006 <code>http://localhost:8006/mcp</code> <code>https://mcp.aetoskia.com/dagpipe/mcp</code> <code>mongo-ops</code> <code>mongo_ops</code> 8007 <code>http://localhost:8007/mcp</code> <code>https://mcp.aetoskia.com/mongo-ops/mcp</code> <code>hexa</code> <code>hexa</code> 8008 <code>http://localhost:8008/mcp</code> <code>https://mcp.aetoskia.com/hexa/mcp</code> <code>doc-weaver</code> <code>doc-weaver</code> 8010 <code>http://localhost:8010/mcp</code> <code>https://mcp.aetoskia.com/doc-weaver/mcp</code> <code>react-auth</code> <code>react-auth</code> 8011 <code>http://localhost:8011/mcp</code> <code>https://mcp.aetoskia.com/react-auth/mcp</code> <code>react-openapi</code> <code>react-openapi</code> 8012 <code>http://localhost:8012/mcp</code> <code>https://mcp.aetoskia.com/react-openapi/mcp</code> <p>The hosted endpoints are served over HTTPS at <code>https://mcp.aetoskia.com/&lt;slug&gt;/mcp</code>. The tabs below are linked (<code>content.tabs.link</code>): switching one switches all of them. Every snippet registers all twelve servers, so a single paste is enough.</p>"},{"location":"05_mcp_setup/#claude-code","title":"\ud83e\udd16 Claude Code","text":"<p>Project-scoped <code>.mcp.json</code> at the repo root. The <code>type</code> field is required \u2014 an entry with a <code>url</code> but no <code>type</code> is read as a stdio server and skipped.</p> localhostmcp.aetoskia.com <pre><code>{\n \"mcpServers\": {\n \"standards\": { \"type\": \"http\", \"url\": \"http://localhost:8009/mcp\" },\n \"doc-forge\": { \"type\": \"http\", \"url\": \"http://localhost:8001/mcp\" },\n \"mail-intake\": { \"type\": \"http\", \"url\": \"http://localhost:8002/mcp\" },\n \"omniread\": { \"type\": \"http\", \"url\": \"http://localhost:8003/mcp\" },\n \"openapi-first\": { \"type\": \"http\", \"url\": \"http://localhost:8004/mcp\" },\n \"py-jwt\": { \"type\": \"http\", \"url\": \"http://localhost:8005/mcp\" },\n \"dagpipe\": { \"type\": \"http\", \"url\": \"http://localhost:8006/mcp\" },\n \"mongo-ops\": { \"type\": \"http\", \"url\": \"http://localhost:8007/mcp\" },\n \"hexa\": { \"type\": \"http\", \"url\": \"http://localhost:8008/mcp\" },\n \"doc-weaver\": { \"type\": \"http\", \"url\": \"http://localhost:8010/mcp\" },\n \"react-auth\": { \"type\": \"http\", \"url\": \"http://localhost:8011/mcp\" },\n \"react-openapi\": { \"type\": \"http\", \"url\": \"http://localhost:8012/mcp\" }\n }\n}\n</code></pre> <pre><code>{\n \"mcpServers\": {\n \"standards\": { \"type\": \"http\", \"url\": \"https://mcp.aetoskia.com/standards/mcp\" },\n \"doc-forge\": { \"type\": \"http\", \"url\": \"https://mcp.aetoskia.com/doc-forge/mcp\" },\n \"mail-intake\": { \"type\": \"http\", \"url\": \"https://mcp.aetoskia.com/mail-intake/mcp\" },\n \"omniread\": { \"type\": \"http\", \"url\": \"https://mcp.aetoskia.com/omniread/mcp\" },\n \"openapi-first\": { \"type\": \"http\", \"url\": \"https://mcp.aetoskia.com/openapi-first/mcp\" },\n \"py-jwt\": { \"type\": \"http\", \"url\": \"https://mcp.aetoskia.com/py-jwt/mcp\" },\n \"dagpipe\": { \"type\": \"http\", \"url\": \"https://mcp.aetoskia.com/dagpipe/mcp\" },\n \"mongo-ops\": { \"type\": \"http\", \"url\": \"https://mcp.aetoskia.com/mongo-ops/mcp\" },\n \"hexa\": { \"type\": \"http\", \"url\": \"https://mcp.aetoskia.com/hexa/mcp\" },\n \"doc-weaver\": { \"type\": \"http\", \"url\": \"https://mcp.aetoskia.com/doc-weaver/mcp\" },\n \"react-auth\": { \"type\": \"http\", \"url\": \"https://mcp.aetoskia.com/react-auth/mcp\" },\n \"react-openapi\": { \"type\": \"http\", \"url\": \"https://mcp.aetoskia.com/react-openapi/mcp\" }\n }\n}\n</code></pre> <p>Or add them with the CLI: <code>claude mcp add --transport http standards http://localhost:8009/mcp</code>.</p>"},{"location":"05_mcp_setup/#codex","title":"\ud83e\udd16 Codex","text":"<p><code>~/.codex/config.toml</code> (or a project-scoped <code>.codex/config.toml</code>).</p> localhostmcp.aetoskia.com <pre><code>[mcp_servers.standards]\nurl = \"http://localhost:8009/mcp\"\n\n[mcp_servers.doc-forge]\nurl = \"http://localhost:8001/mcp\"\n\n[mcp_servers.mail-intake]\nurl = \"http://localhost:8002/mcp\"\n\n[mcp_servers.omniread]\nurl = \"http://localhost:8003/mcp\"\n\n[mcp_servers.openapi-first]\nurl = \"http://localhost:8004/mcp\"\n\n[mcp_servers.py-jwt]\nurl = \"http://localhost:8005/mcp\"\n\n[mcp_servers.dagpipe]\nurl = \"http://localhost:8006/mcp\"\n\n[mcp_servers.mongo-ops]\nurl = \"http://localhost:8007/mcp\"\n\n[mcp_servers.hexa]\nurl = \"http://localhost:8008/mcp\"\n\n[mcp_servers.doc-weaver]\nurl = \"http://localhost:8010/mcp\"\n\n[mcp_servers.react-auth]\nurl = \"http://localhost:8011/mcp\"\n\n[mcp_servers.react-openapi]\nurl = \"http://localhost:8012/mcp\"\n</code></pre> <pre><code>[mcp_servers.standards]\nurl = \"https://mcp.aetoskia.com/standards/mcp\"\n\n[mcp_servers.doc-forge]\nurl = \"https://mcp.aetoskia.com/doc-forge/mcp\"\n\n[mcp_servers.mail-intake]\nurl = \"https://mcp.aetoskia.com/mail-intake/mcp\"\n\n[mcp_servers.omniread]\nurl = \"https://mcp.aetoskia.com/omniread/mcp\"\n\n[mcp_servers.openapi-first]\nurl = \"https://mcp.aetoskia.com/openapi-first/mcp\"\n\n[mcp_servers.py-jwt]\nurl = \"https://mcp.aetoskia.com/py-jwt/mcp\"\n\n[mcp_servers.dagpipe]\nurl = \"https://mcp.aetoskia.com/dagpipe/mcp\"\n\n[mcp_servers.mongo-ops]\nurl = \"https://mcp.aetoskia.com/mongo-ops/mcp\"\n\n[mcp_servers.hexa]\nurl = \"https://mcp.aetoskia.com/hexa/mcp\"\n\n[mcp_servers.doc-weaver]\nurl = \"https://mcp.aetoskia.com/doc-weaver/mcp\"\n\n[mcp_servers.react-auth]\nurl = \"https://mcp.aetoskia.com/react-auth/mcp\"\n\n[mcp_servers.react-openapi]\nurl = \"https://mcp.aetoskia.com/react-openapi/mcp\"\n</code></pre> <p>Or add them with the CLI: <code>codex mcp add standards --url http://localhost:8009/mcp</code>.</p>"},{"location":"05_mcp_setup/#antigravity","title":"\ud83e\udd16 Antigravity","text":"<p><code>~/.gemini/antigravity/mcp_config.json</code> (IDE) or <code>~/.gemini/config/mcp_config.json</code> (CLI). Antigravity uses <code>serverUrl</code>, not <code>url</code>, for HTTP endpoints.</p> localhostmcp.aetoskia.com <pre><code>{\n \"mcpServers\": {\n \"standards\": { \"serverUrl\": \"http://localhost:8009/mcp\" },\n \"doc-forge\": { \"serverUrl\": \"http://localhost:8001/mcp\" },\n \"mail-intake\": { \"serverUrl\": \"http://localhost:8002/mcp\" },\n \"omniread\": { \"serverUrl\": \"http://localhost:8003/mcp\" },\n \"openapi-first\": { \"serverUrl\": \"http://localhost:8004/mcp\" },\n \"py-jwt\": { \"serverUrl\": \"http://localhost:8005/mcp\" },\n \"dagpipe\": { \"serverUrl\": \"http://localhost:8006/mcp\" },\n \"mongo-ops\": { \"serverUrl\": \"http://localhost:8007/mcp\" },\n \"hexa\": { \"serverUrl\": \"http://localhost:8008/mcp\" },\n \"doc-weaver\": { \"serverUrl\": \"http://localhost:8010/mcp\" },\n \"react-auth\": { \"serverUrl\": \"http://localhost:8011/mcp\" },\n \"react-openapi\": { \"serverUrl\": \"http://localhost:8012/mcp\" }\n }\n}\n</code></pre> <pre><code>{\n \"mcpServers\": {\n \"standards\": { \"serverUrl\": \"https://mcp.aetoskia.com/standards/mcp\" },\n \"doc-forge\": { \"serverUrl\": \"https://mcp.aetoskia.com/doc-forge/mcp\" },\n \"mail-intake\": { \"serverUrl\": \"https://mcp.aetoskia.com/mail-intake/mcp\" },\n \"omniread\": { \"serverUrl\": \"https://mcp.aetoskia.com/omniread/mcp\" },\n \"openapi-first\": { \"serverUrl\": \"https://mcp.aetoskia.com/openapi-first/mcp\" },\n \"py-jwt\": { \"serverUrl\": \"https://mcp.aetoskia.com/py-jwt/mcp\" },\n \"dagpipe\": { \"serverUrl\": \"https://mcp.aetoskia.com/dagpipe/mcp\" },\n \"mongo-ops\": { \"serverUrl\": \"https://mcp.aetoskia.com/mongo-ops/mcp\" },\n \"hexa\": { \"serverUrl\": \"https://mcp.aetoskia.com/hexa/mcp\" },\n \"doc-weaver\": { \"serverUrl\": \"https://mcp.aetoskia.com/doc-weaver/mcp\" },\n \"react-auth\": { \"serverUrl\": \"https://mcp.aetoskia.com/react-auth/mcp\" },\n \"react-openapi\": { \"serverUrl\": \"https://mcp.aetoskia.com/react-openapi/mcp\" }\n }\n}\n</code></pre>"},{"location":"05_mcp_setup/#opencode","title":"\ud83e\udd16 OpenCode","text":"<p><code>opencode.json</code> (project) or <code>~/.config/opencode/opencode.json</code> (global).</p> localhostmcp.aetoskia.com <pre><code>{\n \"$schema\": \"https://opencode.ai/config.json\",\n \"mcp\": {\n \"standards\": { \"type\": \"remote\", \"url\": \"http://localhost:8009/mcp\", \"enabled\": true },\n \"doc-forge\": { \"type\": \"remote\", \"url\": \"http://localhost:8001/mcp\", \"enabled\": true },\n \"mail-intake\": { \"type\": \"remote\", \"url\": \"http://localhost:8002/mcp\", \"enabled\": true },\n \"omniread\": { \"type\": \"remote\", \"url\": \"http://localhost:8003/mcp\", \"enabled\": true },\n \"openapi-first\": { \"type\": \"remote\", \"url\": \"http://localhost:8004/mcp\", \"enabled\": true },\n \"py-jwt\": { \"type\": \"remote\", \"url\": \"http://localhost:8005/mcp\", \"enabled\": true },\n \"dagpipe\": { \"type\": \"remote\", \"url\": \"http://localhost:8006/mcp\", \"enabled\": true },\n \"mongo-ops\": { \"type\": \"remote\", \"url\": \"http://localhost:8007/mcp\", \"enabled\": true },\n \"hexa\": { \"type\": \"remote\", \"url\": \"http://localhost:8008/mcp\", \"enabled\": true },\n \"doc-weaver\": { \"type\": \"remote\", \"url\": \"http://localhost:8010/mcp\", \"enabled\": true },\n \"react-auth\": { \"type\": \"remote\", \"url\": \"http://localhost:8011/mcp\", \"enabled\": true },\n \"react-openapi\": { \"type\": \"remote\", \"url\": \"http://localhost:8012/mcp\", \"enabled\": true }\n }\n}\n</code></pre> <pre><code>{\n \"$schema\": \"https://opencode.ai/config.json\",\n \"mcp\": {\n \"standards\": { \"type\": \"remote\", \"url\": \"https://mcp.aetoskia.com/standards/mcp\", \"enabled\": true },\n \"doc-forge\": { \"type\": \"remote\", \"url\": \"https://mcp.aetoskia.com/doc-forge/mcp\", \"enabled\": true },\n \"mail-intake\": { \"type\": \"remote\", \"url\": \"https://mcp.aetoskia.com/mail-intake/mcp\", \"enabled\": true },\n \"omniread\": { \"type\": \"remote\", \"url\": \"https://mcp.aetoskia.com/omniread/mcp\", \"enabled\": true },\n \"openapi-first\": { \"type\": \"remote\", \"url\": \"https://mcp.aetoskia.com/openapi-first/mcp\", \"enabled\": true },\n \"py-jwt\": { \"type\": \"remote\", \"url\": \"https://mcp.aetoskia.com/py-jwt/mcp\", \"enabled\": true },\n \"dagpipe\": { \"type\": \"remote\", \"url\": \"https://mcp.aetoskia.com/dagpipe/mcp\", \"enabled\": true },\n \"mongo-ops\": { \"type\": \"remote\", \"url\": \"https://mcp.aetoskia.com/mongo-ops/mcp\", \"enabled\": true },\n \"hexa\": { \"type\": \"remote\", \"url\": \"https://mcp.aetoskia.com/hexa/mcp\", \"enabled\": true },\n \"doc-weaver\": { \"type\": \"remote\", \"url\": \"https://mcp.aetoskia.com/doc-weaver/mcp\", \"enabled\": true },\n \"react-auth\": { \"type\": \"remote\", \"url\": \"https://mcp.aetoskia.com/react-auth/mcp\", \"enabled\": true },\n \"react-openapi\": { \"type\": \"remote\", \"url\": \"https://mcp.aetoskia.com/react-openapi/mcp\", \"enabled\": true }\n }\n}\n</code></pre> <p>Or add them with the CLI: <code>opencode mcp add standards --url http://localhost:8009/mcp --global</code>.</p>"},{"location":"05_mcp_setup/#pycharm-junie-ai-assistant","title":"\ud83e\udd16 PyCharm (Junie / AI Assistant)","text":"<p><code>~/.junie/mcp/mcp.json</code> (user scope) or <code>.junie/mcp/mcp.json</code> (project scope). The same config is reachable from Settings | Tools | MCP Server \u2192 Manual Client Configuration \u2192 Copy HTTP Stream Config.</p> localhostmcp.aetoskia.com <pre><code>{\n \"mcpServers\": {\n \"standards\": { \"url\": \"http://localhost:8009/mcp\" },\n \"doc-forge\": { \"url\": \"http://localhost:8001/mcp\" },\n \"mail-intake\": { \"url\": \"http://localhost:8002/mcp\" },\n \"omniread\": { \"url\": \"http://localhost:8003/mcp\" },\n \"openapi-first\": { \"url\": \"http://localhost:8004/mcp\" },\n \"py-jwt\": { \"url\": \"http://localhost:8005/mcp\" },\n \"dagpipe\": { \"url\": \"http://localhost:8006/mcp\" },\n \"mongo-ops\": { \"url\": \"http://localhost:8007/mcp\" },\n \"hexa\": { \"url\": \"http://localhost:8008/mcp\" },\n \"doc-weaver\": { \"url\": \"http://localhost:8010/mcp\" },\n \"react-auth\": { \"url\": \"http://localhost:8011/mcp\" },\n \"react-openapi\": { \"url\": \"http://localhost:8012/mcp\" }\n }\n}\n</code></pre> <pre><code>{\n \"mcpServers\": {\n \"standards\": { \"url\": \"https://mcp.aetoskia.com/standards/mcp\" },\n \"doc-forge\": { \"url\": \"https://mcp.aetoskia.com/doc-forge/mcp\" },\n \"mail-intake\": { \"url\": \"https://mcp.aetoskia.com/mail-intake/mcp\" },\n \"omniread\": { \"url\": \"https://mcp.aetoskia.com/omniread/mcp\" },\n \"openapi-first\": { \"url\": \"https://mcp.aetoskia.com/openapi-first/mcp\" },\n \"py-jwt\": { \"url\": \"https://mcp.aetoskia.com/py-jwt/mcp\" },\n \"dagpipe\": { \"url\": \"https://mcp.aetoskia.com/dagpipe/mcp\" },\n \"mongo-ops\": { \"url\": \"https://mcp.aetoskia.com/mongo-ops/mcp\" },\n \"hexa\": { \"url\": \"https://mcp.aetoskia.com/hexa/mcp\" },\n \"doc-weaver\": { \"url\": \"https://mcp.aetoskia.com/doc-weaver/mcp\" },\n \"react-auth\": { \"url\": \"https://mcp.aetoskia.com/react-auth/mcp\" },\n \"react-openapi\": { \"url\": \"https://mcp.aetoskia.com/react-openapi/mcp\" }\n }\n}\n</code></pre>"},{"location":"05_mcp_setup/#verify","title":"\u2705 Verify","text":"<ul> <li>Health check: <code>curl http://localhost:8000/healthz</code> \u2192 <code>ok</code>.</li> <li>Claude Code: <code>claude mcp list</code> \u2192 each server \u2026 <code>\u2713 Connected</code>.</li> <li>Codex: <code>codex mcp list</code> \u2192 each server \u2026 <code>http</code>.</li> <li>OpenCode: <code>opencode mcp list</code> \u2192 each server \u2026 <code>connected</code>.</li> <li>In-session: run <code>/mcp</code> to list the connected servers and their tools.</li> </ul>"},{"location":"05_mcp_setup/#reference","title":"\ud83d\udcda Reference","text":"<ul> <li>Library Registry \u2014 the catalog each server mirrors, and where <code>find_library</code> resolves</li> <li>Start an App \u2014 connect <code>standards</code>, then invoke <code>start_app</code></li> <li>App Components \u2014 the component model</li> </ul>"},{"location":"05_mcp_setup/#related","title":"Related","text":"<ul> <li>Start an App \u00b7 Testing &amp; Quality \u00b7 Library Registry</li> </ul>"},{"location":"04_development/01_testing/","title":"Testing &amp; Quality Gates","text":"<p>Two suites run under pytest with asyncio auto mode: integration (the HTTP edge against fakes) and unit (pure domain and pipeline logic). Finish a blueprint app only when every gate below passes.</p>"},{"location":"04_development/01_testing/#layout","title":"\ud83d\uddc2\ufe0f Layout","text":"<pre><code>tests/\n integration/ # TestClient(app) + mocked resources\n unit/ # models, converters, pipeline stages/roots\n conftest.py # fixtures\n</code></pre>"},{"location":"04_development/01_testing/#integration-tests","title":"\ud83d\ude80 Integration tests","text":"<ul> <li>Build the app with <code>TestClient</code>.</li> <li>Replace <code>app.state.db_resources</code> with fake repositories/services so no database is required.</li> <li>Patch the auth introspection call (<code>httpx.AsyncClient.post</code>) to return a canned identity.</li> <li>Assert status codes and bodies for list/get/create/update/delete, plus <code>400</code>, <code>404</code>, <code>204</code>, and the streaming endpoint's fallback.</li> </ul> <pre><code>client = TestClient(app)\napp.state.db_resources = {\"item_svc\": FakeItemService(), ...}\n\ndef test_create_item():\n r = client.post(\"/items\", json={\"name\": \"x\"})\n assert r.status_code == 200\n</code></pre>"},{"location":"04_development/01_testing/#unit-tests","title":"\ud83e\uddea Unit tests","text":"<ul> <li>Domain models and converters: pure, no I/O.</li> <li>Pipeline stages: feed a fixture state, assert the transformed state.</li> <li>Assembled pipeline roots: fixture parity \u2014 replay a stored fixture input and compare against a golden output file.</li> </ul>"},{"location":"04_development/01_testing/#config-consistency-tests","title":"\ud83d\udd10 Config-consistency tests","text":"<p>For rule-driven pipelines, add a test that cross-references rules against the declared pattern registry: every flag emitted must be defined; every defined flag must be reachable. This catches drift without running the pipeline.</p>"},{"location":"04_development/01_testing/#quality-gates","title":"\u2705 Quality gates","text":"<p>A definition of done for any standard app:</p> <ul> <li><code>python -c \"import main\"</code> succeeds.</li> <li><code>pytest</code> passes.</li> <li>The app serves the spec's operations; the worker drains its queue.</li> <li>No domain module imports a web framework.</li> <li>No infrastructure was reimplemented that a library already provides.</li> </ul>"},{"location":"04_development/01_testing/#conventions","title":"\ud83d\udca1 Conventions","text":"<ul> <li><code>asyncio_mode = \"auto\"</code>; no manual event loops.</li> <li>Fixtures are checked into the repo (inputs + golden outputs).</li> <li>Tests skip (not fail) when a required fixture is absent.</li> <li>Do not mock the domain; mock only the infrastructure edge.</li> </ul>"},{"location":"04_development/01_testing/#related","title":"Related","text":"<ul> <li>02 \u2013 App Components \u00b7 03 \u2013 Start an App \u00b7 Overview</li> </ul>"},{"location":"04_development/02_releasing/","title":"Releasing &amp; Versioning","text":"<p>How to cut a release of any Aetos repository. The rule lives in the reference page Versioning &amp; Changelog; this is the runbook you follow.</p> <p>Every repository is one of two Drone shapes \u2014 a PyPI package or a Docker deployment (Drone Pipelines). Both release the same way: the version bump and changelog promotion go in the merge request, and the tag is created on the merged commit.</p>"},{"location":"04_development/02_releasing/#before-you-start","title":"\ud83e\uddfe Before you start","text":"<ul> <li>You have an open merge request holding the change, and <code>main</code> is green (the Drone quality gate passed on the last run).</li> <li>You know whether this is a breaking, feature, or fix release.</li> <li>The version file exists:</li> <li>package \u2192 <code>version</code> in <code>pyproject.toml</code></li> <li>service \u2192 <code>VERSION</code> at the repo root</li> </ul>"},{"location":"04_development/02_releasing/#pick-the-next-version","title":"\ud83d\udea6 Pick the next version","text":"<p>The version file and the tag must be identical. For <code>0.y.z</code>, <code>0.y</code> is the major line (see the reference page); once at <code>1.0.0</code>, normal SemVer applies.</p> Change <code>0.y.z</code> \u2192 <code>1.0.0+</code> \u2192 Breaking <code>0.(y+1).0</code> <code>(x+1).0.0</code> Feature <code>0.y.(z+1)</code> <code>x.(y+1).0</code> Fix <code>0.y.(z+1)</code> <code>x.y.(z+1)</code>"},{"location":"04_development/02_releasing/#release-in-the-mr","title":"\ud83c\udf3f Release in the MR","text":"<p>The version is known while the change is still a merge request, so the bump rides along with it. Do all edits in the MR:</p> <ul> <li> <p>Reconcile the changelog first. <code>[Unreleased]</code> is a draft, not the source of truth: diff the change against the last tag (<code>git log &lt;last-tag&gt;..HEAD</code>, <code>git diff --stat &lt;last-tag&gt;..HEAD</code>) and add an entry for every user-facing change that is missing. Always show the proposed additions to the user and wait for approval before promoting \u2014 ask rather than guess when an entry is ambiguous.</p> </li> <li> <p>Promote the changelog in <code>CHANGELOG.md</code>: move the (now reconciled) <code>[Unreleased]</code> content under a new <code>## [x.y.z] - YYYY-MM-DD</code> heading, leave a fresh empty <code>[Unreleased]</code> on top, and add the compare links \u2014 naming the version/tag, never a commit id:</p> </li> </ul> <pre><code>[Unreleased]: https://git.aetoskia.com/aetos/&lt;repo&gt;/compare/x.y.z...HEAD\n[x.y.z]: https://git.aetoskia.com/aetos/&lt;repo&gt;/compare/x.y.(z-1)...x.y.z\n</code></pre> <ul> <li>Bump the version file to the intended release version: <code>version = \"x.y.z\"</code> in <code>pyproject.toml</code> (packages) or the <code>VERSION</code> file (services).</li> </ul>"},{"location":"04_development/02_releasing/#tag-the-merged-commit","title":"\ud83c\udff7\ufe0f Tag the merged commit","text":"<ol> <li>Merge or squash the MR into <code>main</code>.</li> <li>Create the tag on the resulting commit: <code>git tag x.y.z</code> (lightweight).</li> <li>Push the tag (<code>git push origin x.y.z</code>) \u2014 Drone builds the release.</li> </ol>"},{"location":"04_development/02_releasing/#agent-assisted-tagging","title":"\ud83e\udd16 Agent-assisted tagging","text":"<p>The agent does not author or merge the MR and never tags before the merge. After you confirm the MR is merged/squashed and tell it to proceed, the agent:</p> <ol> <li>Waits for that explicit go-ahead.</li> <li>Finds the commit that introduced the bump:</li> <li>package: <code>git log -S 'version = \"x.y.z\"' -n 1 --format='%H %s' -- pyproject.toml</code></li> <li>service: <code>git log -S 'x.y.z' -n 1 --format='%H %s' -- VERSION</code></li> <li>Verifies that commit's version file equals <code>x.y.z</code>, then tags only that commit: <code>git tag x.y.z &lt;sha&gt;</code>.</li> <li>Does not push \u2014 it asks you to run <code>git push origin x.y.z</code>.</li> </ol>"},{"location":"04_development/02_releasing/#what-drone-does","title":"\ud83e\udd16 What Drone does","text":"<ul> <li>Type A (package): asserts <code>tag == pyproject version</code>, then skips if that version already exists on the index, runs the quality gate, builds, and uploads the wheel + sdist.</li> <li>Type B (service): asserts <code>tag == VERSION</code>, resolves the tag, skips if the image already exists, runs the quality gate, builds, pushes, and restarts the container.</li> </ul> <p>If the guard fails, nothing is published \u2014 fix the version file, amend the commit, and move the tag only if it was never pushed.</p>"},{"location":"04_development/02_releasing/#backfill-recovery","title":"\ud83d\udedf Backfill / recovery","text":"<p>If an older tag never published (a past version-file mismatch, an index outage):</p> <ul> <li>Package: run the matching <code>backfill-*-from-tags</code> pipeline (custom event) \u2014 <code>backfill-pypi-from-tags</code> for Python, <code>backfill-npm-from-tags</code> for npm. It checks out every tag, verifies <code>tag ==</code> the version file at that commit, and uploads the ones missing from the index. Already-fixed history is never rewritten.</li> <li>Service: rebuild the image for the missing tag with a fresh commit and a new patch version; do not move a published tag.</li> </ul>"},{"location":"04_development/02_releasing/#if-a-release-is-wrong","title":"\ud83d\udd01 If a release is wrong","text":"<ul> <li>Never reuse or force-move a pushed tag.</li> <li>Fix forward: bump to the next patch, promote the changelog, tag again.</li> </ul>"},{"location":"04_development/02_releasing/#related","title":"Related","text":"<ul> <li>Testing &amp; Quality Gates \u00b7 Versioning &amp; Changelog \u00b7 Drone Pipelines</li> </ul>"},{"location":"06_reference_app/01_overview/","title":"Reference App \u2014 Overview","text":"<p>The standards ship a runnable sample of the whole blueprint: the vet veterinary-clinic application. It exists to be copied \u2014 it is the reference for how a real domain sits on the architecture, and the code an agent pulls through the MCP tools (<code>get_app_template</code>, <code>get_app_component</code>).</p> <p>One sample, two templates</p> <p>The server sample lives at <code>standards/src/templates/app/</code> (FastAPI + openapi-first + mongo-ops + hexa). The client sample lives at <code>standards/src/templates/client/</code> (Vite + MUI + <code>@aetoskia/react-openapi</code> + <code>@aetoskia/react-auth</code>). Together they are the full-stack reference.</p>"},{"location":"06_reference_app/01_overview/#the-domain-at-a-glance","title":"The domain at a glance","text":"Aggregate What it demonstrates parents The simplest CRUD slice (no foreign keys) \u2014 copy this first pets Embedded list-of-refs (<code>parents[]</code>), file upload, enum fields treatments Plain resource with pricing fields vets Plain resource; target of prefetch FKs appointments Single FKs (<code>pet</code>, <code>vet</code>, <code>treatment</code>) expanded on read, filtered list jobs The worker's queue \u2014 durable status machine <p>The pipeline axis ships one worked family, <code>appointment_reminder</code>, plus the reusable <code>pipelines/shared/</code> scaffold and a <code>worker.py</code> that dispatches jobs by <code>job.kind</code>.</p>"},{"location":"06_reference_app/01_overview/#run-it","title":"Run it","text":"<pre><code>cp -r &lt;docs&gt;/standards/src/templates/app apps/vet-sample\ncd apps/vet-sample\npip install -e \".[dev]\"\n\nuvicorn main:app --reload # API on :8020 (memory backend, zero services)\npython worker.py # background worker (polls jobs)\npython -m scripts.seed # optional: seed demo data (mongo backend)\n</code></pre> <p>The default <code>REPO_BACKEND=memory</code> means the API boots with no external services \u2014 repositories are process-local dictionaries. Switch to <code>REPO_BACKEND=mongo</code> for persistence (see <code>.env.example</code>).</p> <p>The client pairs with it:</p> <pre><code>cp -r &lt;docs&gt;/standards/src/templates/client apps/vet-sample-client\ncd apps/vet-sample-client\necho \"VITE_API_BASE_URL=http://localhost:8020\" &gt; .env.local\necho \"VITE_AUTH_BASE_URL=http://localhost:8020\" &gt;&gt; .env.local\nnpm install\nnpm run dev # Vite dev server\n</code></pre> <p>Auth</p> <p>The server ships with the <code>bearerAuth</code> security scheme declared (<code>{AUTH_SERVER}</code> introspection). Without a reachable auth-server the API answers <code>401</code> on resource routes; <code>/health</code> stays public. For a local no-auth demo, remove the <code>security</code> block from <code>openapi.yaml</code> and the global security entry \u2014 see Auth Integration.</p>"},{"location":"06_reference_app/01_overview/#next","title":"Next","text":"<ul> <li>Domain &amp; API \u2014 walking the resource axis</li> <li>Pipelines &amp; Worker \u2014 the pipeline axis</li> <li>Client Admin \u2014 the spec-driven frontend</li> </ul>"},{"location":"06_reference_app/02_domain_and_api/","title":"Domain &amp; API \u2014 the resource axis","text":"<p>Every aggregate follows the same four-file slice (CRUD Template); walk <code>parents</code> first, then <code>appointments</code> when you need foreign keys.</p>"},{"location":"06_reference_app/02_domain_and_api/#the-slice-end-to-end-parents","title":"The slice, end to end (parents)","text":"<pre><code>domain/parents/\n models.py # framework-free Pydantic model (id optional)\n ports/\n db.py # ParentDocument(Parent, BaseDocument) + create_parent_repo()\n service.py # ParentCrudsService(CrudsServiceAdaptor[...]) + get_parent_service()\ninterfaces/parents/\n routes.py # list/create/get/update/delete \u2014 names == operationIds\n</code></pre> <p>Wiring happens in exactly one place \u2014 the composition root <code>init.py</code>:</p> <pre><code>parent_repo = create_parent_repo()\n_resources[\"parent_svc\"] = get_parent_service(parent_repo)\n</code></pre> <p>Routes resolve the service from app state through the shared helper (<code>interfaces/_common.py</code>):</p> <pre><code>async def list_parents(request: Request, limit: int = 20, offset: int = 0) -&gt; dict:\n docs = await svc(request, \"parent\").search({})\n return paginate([dump(d) for d in docs], limit, offset)\n</code></pre>"},{"location":"06_reference_app/02_domain_and_api/#the-spec-is-the-contract","title":"The spec is the contract","text":"<p><code>openapi.yaml</code> is OpenAPI 3.1 and the single source of truth: <code>main.py</code> builds an <code>OpenAPIFirstApp</code> that binds each <code>operationId</code> to a function of the same name in <code>interfaces/openapi.py</code> (a flat re-export of every routes module). There are no decorator routers.</p> <p>The x- extension fields on schemas and properties are what make the admin client render itself \u2014 see Auto-Rendered Admin and the worked <code>Job</code> schema (enum selects, hidden fields, filterable columns).</p>"},{"location":"06_reference_app/02_domain_and_api/#foreign-keys-appointments","title":"Foreign keys (appointments)","text":"<p><code>Appointment</code> declares <code>pet</code>, <code>vet</code>, <code>treatment</code> as <code>dict | int | None</code>. The write path normalizes bare ids through <code>expand_ref</code>; the read path returns fully expanded objects:</p> <pre><code>pet = await expand_ref(svc(request, \"pet\"), body.get(\"pet\"), \"pet\")\n</code></pre> <p>The list endpoint additionally filters by date prefix and embedded fk ids \u2014 client-side over the repository result, the pattern for small reference data.</p>"},{"location":"06_reference_app/02_domain_and_api/#ids-on-the-wire","title":"Ids on the wire","text":"<p>The domain models declare <code>id: int | None = None</code> and documents use the integer-id repository variants (<code>infra/db/repos/mongo_int.py</code>, <code>create_repo()</code> in <code>domain/*/ports/db.py</code>, <code>REPO_BACKEND=memory|mongo</code>). This keeps the OpenAPI <code>id: integer</code> contract, the in-memory demo store, and the Mongo persistence layer consistent.</p>"},{"location":"06_reference_app/02_domain_and_api/#tests","title":"Tests","text":"<ul> <li><code>tests/integration/test_openapi_app_builds.py</code> \u2014 generic gate: every <code>operationId</code> resolves and the app builds. Copy it verbatim.</li> <li><code>tests/unit/test_jobs_routes.py</code> \u2014 the pattern for route unit tests with a service double (no HTTP client needed).</li> </ul>"},{"location":"06_reference_app/03_pipelines_and_worker/","title":"Pipelines &amp; Worker \u2014 the pipeline axis","text":"<p>The sample proves the full loop: API creates a job document \u2192 worker polls \u2192 pipeline family runs \u2192 job reaches a terminal status.</p>"},{"location":"06_reference_app/03_pipelines_and_worker/#the-jobs-slice","title":"The jobs slice","text":"<p><code>domain/jobs/</code> is an ordinary CRUD slice; its model is the durable state machine the worker drives:</p> <pre><code>class Job(BaseModel):\n name: str\n kind: str # dispatch key\n status: Literal[\"pending\", \"processing\", \"done\", \"failed\"] = \"pending\"\n payload: dict | None = None\n retry_count: int = 0\n error_message: str | None = None\n</code></pre> <p><code>POST /jobs</code> with <code>{\"name\": \"...\", \"kind\": \"appointment_reminder\", \"payload\": {\"appointment_id\": 1}}</code> enqueues work; <code>GET /jobs?status=...</code> inspects the queue.</p>"},{"location":"06_reference_app/03_pipelines_and_worker/#the-appointment_reminder-family","title":"The appointment_reminder family","text":"<pre><code>pipelines/appointment_reminder/\n abc.py # LoadPort / ComposePort / DeliverPort + root interface\n impl.py # concrete stages, wired root, register(\"appointment_reminder\", ...)\n pipeline.yaml # graph spec (nodes, edges, roots)\n resources/\n pipeline.yaml # runtime spec consumed by the loader\n patterns/reminder.yaml # message template fragments\npipelines/shared/\n ports.py loader.py registry.py # reused by every family\n</code></pre> <p>Each stage is a small, independently testable unit. The loader (<code>pipelines/shared/loader.py</code>) wires the root through <code>hexa.build</code> and merges <code>resources/patterns/*.yaml</code> into the stage-injected registry:</p> <pre><code>pipeline = get_appointment_reminder_pipeline(appointment_svc)\nreminder = await pipeline.run(appointment_id)\n</code></pre> <p>To add a family: copy <code>appointment_reminder/</code>, replace the stages, call <code>register(kind, variant, root_cls)</code> \u2014 see Pipeline Scaffold.</p>"},{"location":"06_reference_app/03_pipelines_and_worker/#the-worker","title":"The worker","text":"<p><code>worker.py</code> is the process that turns documents into execution:</p> <ol> <li>poll <code>job_svc.search({\"status\": \"pending\"})</code> on <code>WORKER_POLL_SECONDS</code></li> <li>claim a job: <code>status \u2192 processing</code></li> <li>dispatch through the <code>HANDLERS</code> table \u2014 <code>HANDLERS[job.kind](job, resources)</code></li> <li>on success <code>status \u2192 done</code>; on exception <code>status \u2192 failed</code>, <code>retry_count += 1</code>, <code>error_message</code> captured; jobs at <code>WORKER_MAX_RETRIES</code> are skipped</li> </ol> <pre><code>HANDLERS: dict[str, Callable] = {\n \"appointment_reminder\": run_appointment_reminder,\n # add yours here\n}\n</code></pre> <p>The worker shares the composition root: <code>from init import init</code> gives it the same wired services the API uses \u2014 one place to add a dependency.</p> <p>Same process model in production</p> <p>API and worker are separate processes (see <code>docker-compose.yaml</code>: <code>api</code>, <code>worker</code>, <code>mongo</code>). Any in-memory progress bus is per-process; to surface live progress through the API, swap the bus adaptor for a broker-backed one \u2014 see Worker &amp; Progress.</p>"},{"location":"06_reference_app/03_pipelines_and_worker/#tests","title":"Tests","text":"<ul> <li><code>tests/unit/test_appointment_reminder_pipeline.py</code> \u2014 stage units + the wired root against a fake service.</li> <li><code>tests/unit/test_worker.py</code> \u2014 dispatch, done/failed transitions, retry exhaustion, using an in-memory job service double and a cancelled poll task.</li> </ul>"},{"location":"06_reference_app/04_client_admin/","title":"Client Admin \u2014 the spec-driven frontend","text":"<p>The client sample (<code>standards/src/templates/client/</code>) renders a full admin \u2014 auth pages, resource lists, forms, detail views \u2014 from the server's <code>openapi.yaml</code>, driven entirely by the x- extension fields. There are no hand-written screens per resource.</p>"},{"location":"06_reference_app/04_client_admin/#what-ships","title":"What ships","text":"<p>A Vite + React + MUI app wiring the two Aetos packages:</p> <ul> <li><code>@aetoskia/react-openapi</code> \u2014 fetches <code>${VITE_API_BASE_URL}/openapi.json</code>, builds the router and the resource admin from the spec's schemas and x- fields (list columns, autocomplete, fk pickers, enum selects, hidden fields\u2026)</li> <li><code>@aetoskia/react-auth</code> \u2014 login/register/logout against the auth-server paths declared in the spec's <code>securitySchemes.bearerAuth</code>, and attaches the JWT to every API call</li> <li>a shared MUI theme (<code>src/shared-theme/</code>) and the shell (<code>Header</code> / <code>Footer</code> / <code>Home</code>)</li> </ul> <p><code>src/openapi-config.ts</code> is the only configuration:</p> <pre><code>export const specConfiguration: SpecConfiguration = {\n specUrl: `${apiBase}/openapi.json`,\n baseApiUrl: apiBase,\n title: \"Vet Admin\",\n getToken: () =&gt; tokenStore.get(),\n};\n</code></pre>"},{"location":"06_reference_app/04_client_admin/#pair-it-with-the-sample-server","title":"Pair it with the sample server","text":"<pre><code>cd apps/vet-sample-client\nprintf 'VITE_API_BASE_URL=http://localhost:8020\\nVITE_AUTH_BASE_URL=http://localhost:8020\\n' &gt; .env.local\nnpm install\nnpm run dev\n</code></pre> <p>Open the dev-server URL; the admin lists Parents, Pets, Treatments, Vets, Appointments and Jobs straight from the vet spec. Edit <code>openapi.yaml</code> on the server and the client follows on reload \u2014 the spec is the contract on both sides.</p>"},{"location":"06_reference_app/04_client_admin/#where-the-x-extensions-come-from","title":"Where the x- extensions come from","text":"<p>The canonical catalog lives in the react-openapi package (<code>spec-extensions.ts</code>, served to agents as the <code>specExtensions</code> module of the react-openapi MCP bundle). The worked examples on the server side are the <code>Job</code> schema (enum selects via <code>x-edit-as: select</code> + <code>x-ui-type: enum</code>, <code>x-hidden</code> field masks, <code>x-list-columns</code>) and the FK fields on <code>Appointment</code> (<code>x-fk: {resource: pets, prefetch: true}</code>).</p>"},{"location":"06_reference_app/04_client_admin/#ci","title":"CI","text":"<p>The client ships a <code>drone-npm</code> shape (Type B in Drone Pipelines) and follows the Versioning standard (<code>package.json</code> + <code>CHANGELOG.md</code>).</p>"}]}