diff --git a/standards/lib/02_layering_rules/index.html b/standards/lib/02_layering_rules/index.html index b720776..fac1c8d 100644 --- a/standards/lib/02_layering_rules/index.html +++ b/standards/lib/02_layering_rules/index.html @@ -662,12 +662,14 @@ 3 4 5 -6
interfaces/* -> domain/* (services, ports, models)
interfaces/* -> infra/adapters/* (only read-only value objects)
domain/* -> domain/* (same layer)
domain/* -> pipelines/* (call a pipeline via a port)
-infra/adapters/* -> domain/ports/* (implement the interface)
-main.py/init.py -> everything (composition root only)
+domain/*/ports/* -> infra/* (bind adapters + libraries at the seam)
+infra/adapters/* -> domain/ports/* (implement the interface)
+main.py/init.py -> everything (composition root only)
1
@@ -680,27 +682,32 @@
|
A port is an abstract class or protocol declared in domain/<x>/ports/.
-An adapter implements it in infra/adapters/. The composition root binds
-the two.
The ports/ package is the seam where the domain meets infrastructure.
+Each domain/<resource>/ports/ module declares the resource's models/document
+shape and exposes factory functions that bind a concrete adapter — so a port
+module is allowed to import from infra/ and from the persistence libraries.
+Everything outside ports/ in domain/ stays framework- and driver-free.
|
The composition root calls the port factories; nothing else constructs an +adapter.
fastapi, the layering is broken.domain/ must never import the web framework (fastapi / starlette) or
+ interfaces/. Only the ports/ seam, infra/, and the composition root
+ touch drivers.import fastapi inside domain/ or infra/.progress_bus — progress/event bus.<vendor>_<kind> — fetchers, clients, stores.The API stores the dict on app.state.resources during lifespan startup.
+
The API stores the dict on app.state.db_resources during lifespan startup.
Handlers read from it; nothing imports concrete adapters.
1
@@ -747,7 +747,7 @@ typing without importing concrete classes:
|
operationId across the whole app:
async def stream_job_events(request, id):
- bus = request.app.state.resources.get("progress_bus")
+ bus = request.app.state.db_resources.get("progress_bus")
if bus is None:
return {"events": []}
diff --git a/standards/lib/08_human_in_loop/index.html b/standards/lib/08_human_in_loop/index.html
index eb9602b..9279a51 100644
--- a/standards/lib/08_human_in_loop/index.html
+++ b/standards/lib/08_human_in_loop/index.html
@@ -734,7 +734,7 @@ injects it into the pipeline so that stage can skip re-raising:
12
13
14async def resolve_question(request, id, body):
- svc = request.app.state.resources["question_svc"]
+ svc = request.app.state.db_resources["question_svc"]
q = await svc.read(id)
await svc.update(id, {"chosen": body["chosen"], "status": "resolved",
"resolved_at": now()})
@@ -742,7 +742,7 @@ injects it into the pipeline so that stage can skip re-raising:
pending = await svc.search(filter={"job_id": q.job_id, "status": "pending"},
limit=1)
if not pending:
- jobs = request.app.state.resources["job_svc"]
+ jobs = request.app.state.db_resources["job_svc"]
job = await jobs.read(q.job_id)
if job and job.status == "paused":
await jobs.update_status(q.job_id, "pending")
diff --git a/standards/lib/search/search_index.json b/standards/lib/search/search_index.json
index 3e37c33..7fd0e5e 100644
--- a/standards/lib/search/search_index.json
+++ b/standards/lib/search/search_index.json
@@ -1 +1 @@
-{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"Standards Reference","text":"The reference half of the Standards description: the enforceable rules, patterns, and the internal library catalog that agents consult while coding.
- Layering Rules \u2014 the strict request \u2192 domain \u2192 repository layering and the rules that enforce it.
- Composition Root \u2014 a single composition point wiring the whole app.
- Spec-First Routing \u2014 the OpenAPI contract as the routing source of truth.
- CRUD Template \u2014 the standardized generic resource endpoint scaffold.
- Pipeline Scaffold \u2014 the deterministic pipeline skeleton for multi-stage jobs.
- Worker & Progress \u2014 queued work, progress reporting, and visibility.
- Human-in-the-Loop \u2014 guardrails and approvals inside automations.
- Infra Adapters \u2014 how infrastructure connects without leaking into domain.
- Genericity Matrix \u2014 what is generic vs app-specific.
- Library Registry \u2014 the catalog of internal libraries behind all of it.
Read the Wiki for the philosophy and rationale; these pages are the reference that mirrors the standards MCP bundle.
"},{"location":"02_layering_rules/","title":"Layering Rules","text":""},{"location":"02_layering_rules/#summary","title":"Summary","text":"The dependency direction is fixed. Violating it is a design defect.
"},{"location":"02_layering_rules/#allowed-imports","title":"Allowed imports","text":"interfaces/* -> domain/* (services, ports, models)\ninterfaces/* -> infra/adapters/* (only read-only value objects)\ndomain/* -> domain/* (same layer)\ndomain/* -> pipelines/* (call a pipeline via a port)\ninfra/adapters/* -> domain/ports/* (implement the interface)\nmain.py/init.py -> everything (composition root only)\n
"},{"location":"02_layering_rules/#forbidden-imports","title":"Forbidden imports","text":"domain/* -> interfaces/* (never)\ndomain/* -> fastapi/starlette (never)\ninfra/adapters/* -> interfaces/* (never)\ninterfaces/* -> infra/db/* (state lives in resources, not imports)\n
"},{"location":"02_layering_rules/#why","title":"Why","text":" - A pure domain is testable without a server, a database, or an event loop.
- Adapters can be swapped (in-memory fake, real database) without touching business logic.
- The HTTP edge stays thin and replaceable.
"},{"location":"02_layering_rules/#the-portadapter-contract","title":"The port/adapter contract","text":"A port is an abstract class or protocol declared in domain/<x>/ports/. An adapter implements it in infra/adapters/. The composition root binds the two.
domain/<resource>/ports/db.py -> repository port + document model\ndomain/<resource>/ports/service.py -> service + factory\ninfra/db/repos/<vendor>.py -> repository adapter factory\ninfra/adapters/service.py -> generic CRUD service base\n
"},{"location":"02_layering_rules/#enforcement","title":"Enforcement","text":" - Keep every domain module importable with only the standard library and Pydantic. If a domain import pulls in
fastapi, the layering is broken. - Reviewers (and CI) reject
import fastapi inside domain/ or infra/.
"},{"location":"03_composition_root/","title":"Composition Root","text":""},{"location":"03_composition_root/#summary","title":"Summary","text":"All wiring happens in exactly one module. It builds a flat resource registry and exposes it to the HTTP layer and to scripts.
"},{"location":"03_composition_root/#the-resource-registry","title":"The resource registry","text":"init.py returns a plain dict[str, Any]. Keys follow two conventions:
<resource>_repo \u2014 a repository adapter for a resource. <resource>_svc \u2014 a service for a resource.
Plus named infrastructure adaptors:
http_cache_store \u2014 response cache backend. progress_bus \u2014 progress/event bus. <vendor>_<kind> \u2014 fetchers, clients, stores.
The API stores the dict on app.state.resources during lifespan startup. Handlers read from it; nothing imports concrete adapters.
"},{"location":"03_composition_root/#shape","title":"Shape","text":"_resources: dict = {}\n_initialized = False\n\nasync def init(settings=None) -> dict:\n global _initialized\n if _initialized:\n return _resources\n\n await setup_database(uri=settings.mongo_uri, db_name=settings.mongo_db)\n\n # 1. leaf repositories\n user_repo = create_user_repo()\n\n # 2. dependent repositories (dependencies passed by keyword)\n wallet_repo = create_wallet_repo(user_repo=user_repo)\n\n # 3. services (build via the DI library, injecting the repo)\n _resources[\"user_repo\"] = user_repo\n _resources[\"wallet_repo\"] = wallet_repo\n _resources[\"user_svc\"] = get_user_service(user_repo)\n _resources[\"wallet_svc\"] = get_wallet_service(wallet_repo)\n\n # 4. infrastructure adaptors (optional)\n _resources[\"http_cache_store\"] = HttpCacheStore(...)\n _resources[\"progress_bus\"] = ProgressEventBus()\n\n _initialized = True\n return _resources\n\nasync def shutdown() -> None:\n global _initialized\n await close_stores(_resources)\n await shutdown_database()\n _resources.clear()\n _initialized = False\n
"},{"location":"03_composition_root/#typed-accessors","title":"Typed accessors","text":"Expose a get_svc(name) helper with one @overload per key so scripts get typing without importing concrete classes:
@overload\ndef get_svc(name: Literal[\"user_svc\"]) -> UserService: ...\n@overload\ndef get_svc(name: Literal[\"wallet_svc\"]) -> WalletService: ...\n\ndef get_svc(name: str):\n return _resources[name]\n
Also expose get_db() (raw database handle) and pure helpers such as compute_fingerprint(payload) (canonical hash used for deduplication).
"},{"location":"03_composition_root/#rules","title":"Rules","text":" init() is idempotent. - The composition root is the only place that imports concrete adapters.
- Handlers never call
init(); they read request.app.state.resources. - Scripts call
init() / shutdown() around their work.
"},{"location":"04_spec_first_routing/","title":"Spec-First Routing","text":""},{"location":"04_spec_first_routing/#summary","title":"Summary","text":"The OpenAPI document is the single source of truth for the HTTP contract. Handlers are bound to it by name; there is no decorator router.
"},{"location":"04_spec_first_routing/#the-contract","title":"The contract","text":" openapi.yaml (or .json) declares paths, schemas, parameters, responses, and security. - A custom
operationId names the handler function. - The framework (
openapi_first) validates requests/responses against the spec and dispatches to the handler whose name equals operationId.
"},{"location":"04_spec_first_routing/#app-bootstrap","title":"App bootstrap","text":"# 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
lifespan calls the composition root:
@asynccontextmanager\nasync def lifespan(app):\n app.state.resources = await init()\n yield\n await shutdown()\n
"},{"location":"04_spec_first_routing/#handler-registry","title":"Handler registry","text":"interfaces/openapi.py is a flat re-export of every handler, so the framework can resolve operationId across the whole app:
from interfaces.item.routes import *\nfrom interfaces.wallet.routes import *\nfrom interfaces.progress.sse import *\n
"},{"location":"04_spec_first_routing/#handler-shape","title":"Handler shape","text":"Handlers are thin and synchronous with respect to business logic:
async def create_item(request: Request, body: dict):\n svc = request.app.state.resources[\"item_svc\"]\n return await svc.create(Item.model_validate(body))\n
"},{"location":"04_spec_first_routing/#conventions","title":"Conventions","text":" operationId naming: list_<resource>, create_<resource>, get_<resource>, update_<resource>, delete_<resource>. - Read-only endpoints may use a cache decorator keyed on path + query + user.
- Validation errors ->
400; not found -> 404; delete -> 204. - Security is declared once in the spec and enforced by the framework; handlers contain no auth code.
"},{"location":"04_spec_first_routing/#codegen","title":"Codegen","text":"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.
"},{"location":"05_crud_template/","title":"CRUD Template","text":""},{"location":"05_crud_template/#summary","title":"Summary","text":"Every resource follows the same four-file slice. Repetition is intentional: it makes the codebase predictable and generatable.
"},{"location":"05_crud_template/#the-slice","title":"The slice","text":"domain/<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
"},{"location":"05_crud_template/#modelspy","title":"models.py","text":"A framework-free Pydantic model describing the business shape.
class Item(BaseModel):\n name: str\n quantity: int = 0\n
"},{"location":"05_crud_template/#portsdbpy","title":"ports/db.py","text":"The persistence model extends the business model and adds database concerns. The repository factory selects the right repository flavour.
class 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
"},{"location":"05_crud_template/#portsservicepy","title":"ports/service.py","text":"The service is built through the DI library, injecting the repository. The generic CRUD base provides create/read/update/patch/delete/search/....
class 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
"},{"location":"05_crud_template/#naming-conventions","title":"Naming conventions","text":"Concept Convention collection plural snake case (items) document <Resource>Document repository factory create_<resource>_repo service <Resource>Service service factory get_<resource>_service resource key <resource>_svc operationId list_items, create_item, ..."},{"location":"05_crud_template/#optional-files","title":"Optional files","text":" converters.py \u2014 pure functions mapping between domain types. ports/enrichment.py \u2014 adapts a pipeline into an async callable the service can invoke (used when a resource is populated by a pipeline).
"},{"location":"06_pipeline_scaffold/","title":"Pipeline Scaffold","text":""},{"location":"06_pipeline_scaffold/#summary","title":"Summary","text":"Data-driven processing is expressed as a composition graph. The graph is built by a DI library; execution is driven by a DAG engine. Stages are pure and individually testable.
"},{"location":"06_pipeline_scaffold/#when-to-use-a-pipeline","title":"When to use a pipeline","text":"Use a pipeline when a resource is produced by a multi-step transformation of an input (a document, a file, a message) rather than by a simple CRUD write.
"},{"location":"06_pipeline_scaffold/#structure-of-a-family","title":"Structure of a family","text":"A family is one input shape (e.g. a file format or a message kind). Each family has the same skeleton:
pipelines/<family>/\n abc.py # port contracts (stage interfaces + root interface)\n impl.py # concrete stages + root implementation\n pipeline.yaml # graph spec: nodes, edges, roots, schema\n resources/\n pipeline.yaml # runtime spec consumed by the loader\n patterns/*.yaml # rule fragments (pattern registry)\n banks/<variant>.py # optional variants that re-pin a few slots\n
"},{"location":"06_pipeline_scaffold/#shared-scaffolding","title":"Shared scaffolding","text":"pipelines/shared/\n ports.py # RegistryPort, FinalizePort, etc.\n loader.py # build(root_cls) -> wires the graph + engine\n registry.py # resolve_root(...) -> root class for a (kind, variant)\n
"},{"location":"06_pipeline_scaffold/#the-loader","title":"The loader","text":"def build(root_cls, *, instances=None, config=None):\n # merge resources/patterns/*.yaml into a pattern registry\n # bind registry/config into each graph node by id\n # build the schema (state) class and the engine\n root = root_cls()\n root._engine = Engine(graph, state_cls, payload_cls)\n return root\n
"},{"location":"06_pipeline_scaffold/#the-registry","title":"The registry","text":"A single lookup replaces hand-rolled if/else selection:
_FAMILIES = {\n \"csv\": {\"acme\": {\"heuristic\": AcmeCsvPipeline}},\n \"json\": {\"globex\": {\"heuristic\": GlobexJsonPipeline}},\n}\n\ndef resolve_root(kind, variant, mode=\"heuristic\"):\n return _FAMILIES[kind][variant][mode]\n
Adding a new variant means adding one class and one registry entry.
"},{"location":"06_pipeline_scaffold/#driving-a-pipeline","title":"Driving a pipeline","text":"Stages are driven explicitly so intermediate state can be persisted:
pipeline = build(resolve_root(kind, variant))\ncontent = pipeline.ingest.materialize(raw_bytes)\nrecords = pipeline.reader.read(content)\nitems = pipeline.normalize.build(records)\n
"},{"location":"06_pipeline_scaffold/#testing","title":"Testing","text":"Because stages are pure, test each stage in isolation and test the assembled root against fixture inputs whose expected outputs are stored as golden files. A parity test replays fixtures headlessly.
"},{"location":"07_worker_and_progress/","title":"Worker & Progress","text":""},{"location":"07_worker_and_progress/#summary","title":"Summary","text":"Long-running work runs in a background worker. Jobs are durable documents that move through an explicit status machine. Progress is streamed to clients.
"},{"location":"07_worker_and_progress/#the-status-machine","title":"The status machine","text":"pending -> processing -> completed\n \\-> failed (retried up to N times)\n \\-> paused (waiting for human input)\n
pending \u2014 queued, eligible for pickup. processing \u2014 claimed by a worker. paused \u2014 blocked on a human-in-the-loop question. failed \u2014 error; retry_count increments until a max, then skipped. completed \u2014 terminal success.
"},{"location":"07_worker_and_progress/#the-poll-loop","title":"The poll loop","text":"async def poll(resources):\n backoff = MIN_POLL\n while True:\n jobs = await job_svc.search(filter={\"status\": \"pending\"}, limit=100)\n backoff = MIN_POLL if jobs else min(backoff * 2, MAX_POLL)\n for job in jobs:\n if (job.retry_count or 0) >= MAX_RETRIES:\n continue\n await job_svc.update_status(job.id, \"processing\")\n try:\n await process(job, resources)\n except PauseForResolution:\n await job_svc.update_status(job.id, \"paused\")\n except Exception as e:\n await job_svc.update_status(\n job.id, \"failed\", error=str(e),\n retry_count=(job.retry_count or 0) + 1,\n )\n await asyncio.sleep(backoff)\n
Tunables live in environment variables: WORKER_POLL_MIN, WORKER_POLL_MAX, WORKER_MAX_RETRIES.
"},{"location":"07_worker_and_progress/#progress-events","title":"Progress events","text":"A worker publishes structured progress events to a bus (a port). The HTTP edge exposes a streaming endpoint that replays events for a given job id.
class ProgressEvent(BaseModel):\n job_id: str\n step: str\n status: str # started | progress | completed | skipped | paused\n message: dict | None = None\n
The bus is an in-memory adapter by default; because it is a port, it can be swapped for a durable broker. Emit from the orchestrator at each stage:
await self.bus.publish(ProgressEvent(job_id=jid, step=\"normalize\",\n status=\"completed\", message={\"count\": n}))\n
"},{"location":"07_worker_and_progress/#streaming-endpoint","title":"Streaming endpoint","text":"async def stream_job_events(request, id):\n bus = request.app.state.resources.get(\"progress_bus\")\n if bus is None:\n return {\"events\": []}\n\n async def gen():\n sent = 0\n while True:\n events = bus.get_events(id)\n for e in events[sent:]:\n yield f\"data: {e.model_dump_json(exclude_none=True)}\\n\\n\"\n sent += 1\n await asyncio.sleep(0.5)\n\n return StreamingResponse(gen(), media_type=\"text/event-stream\")\n
Known limitation. The default in-memory bus is per-process. If the worker and the API run as separate processes, events published by the worker are not visible to the API. For production, back the bus with a shared broker. The port boundary makes this a one-adapter change.
"},{"location":"08_human_in_loop/","title":"Human-in-the-Loop","text":""},{"location":"08_human_in_loop/#summary","title":"Summary","text":"Some inputs are ambiguous and must be resolved by a human before processing can continue. The blueprint models this as a durable pause: an exception is raised, a question is recorded, the job pauses, and later resumes.
"},{"location":"08_human_in_loop/#the-mechanism","title":"The mechanism","text":" - During processing, a stage raises a domain exception carrying the context needed to ask the question:
raise PauseForResolution(\n job_id=jid,\n reference=\"<stable key for this decision>\",\n options=[...],\n context={...},\n)\n
- The orchestrator catches it, records a question document, and re-raises so the worker marks the job
paused. - A client lists pending questions and answers one:
GET /jobs/{id}/questions -> list pending questions\nPOST /questions/{qid}/resolve {choice} -> record the choice\n
- When the last pending question is resolved, the resolve endpoint flips the parent job back to
pending. - The worker picks it up again, rebuilds a resolution map from previously answered questions, and seeds it into the pipeline so the same decision is not asked twice.
"},{"location":"08_human_in_loop/#the-question-document","title":"The question document","text":"class Question(BaseDocument):\n job_id: str\n reference: str # stable key: stage|field|value\n status: str # pending | resolved\n options: list[dict]\n context: dict\n chosen: dict | None = None\n resolved_at: str | None = None\n
"},{"location":"08_human_in_loop/#resume-map","title":"Resume map","text":"The worker turns answered questions into a lookup keyed by reference, then injects it into the pipeline so that stage can skip re-raising:
resolved = {q.reference: q.chosen for q in await svc.search(\n filter={\"job_id\": jid, \"status\": \"resolved\"})}\n\npipeline = build(root, instances={\"pause\": handler}, config={\"resolved\": resolved})\n
"},{"location":"08_human_in_loop/#resolve-endpoint","title":"Resolve endpoint","text":"async def resolve_question(request, id, body):\n svc = request.app.state.resources[\"question_svc\"]\n q = await svc.read(id)\n await svc.update(id, {\"chosen\": body[\"chosen\"], \"status\": \"resolved\",\n \"resolved_at\": now()})\n\n pending = await svc.search(filter={\"job_id\": q.job_id, \"status\": \"pending\"},\n limit=1)\n if not pending:\n jobs = request.app.state.resources[\"job_svc\"]\n job = await jobs.read(q.job_id)\n if job and job.status == \"paused\":\n await jobs.update_status(q.job_id, \"pending\")\n return q\n
"},{"location":"08_human_in_loop/#cli-parity","title":"CLI parity","text":"The same flow is available through scripts/CLI: list, get, create, questions, resolve, retry.
"},{"location":"09_infra_adaptors/","title":"Infra Adapters","text":""},{"location":"09_infra_adaptors/#summary","title":"Summary","text":"All external systems are reached through adapters in infra/. Prefer an existing internal library over writing a new adapter.
"},{"location":"09_infra_adaptors/#database-mongodb","title":"Database (MongoDB)","text":" - A single connection manager owns the client lifecycle.
setup_database(uri, db_name) / shutdown_database(). - A repository factory selects the repository flavour:
repo_factory(collection, document, engine=None,\n populate_rules=None, cache=None)\n
- base repository \u2014 plain CRUD;
- populating repository \u2014 resolves referenced documents (foreign keys);
-
cached repository \u2014 an LRU/TTL cache in front of reads.
-
Referenced documents are described declaratively:
PopulateRule(field=\"owner\", collection=\"users\",\n nested_rules=[], max_depth=2, projection=None)\n
- Transactions use a session context manager provided by the library.
Library: the Mongo operations library (see registry). Do not write raw Motor calls in the domain.
"},{"location":"09_infra_adaptors/#http-response-cache","title":"HTTP response cache","text":"A generic GET cache sits behind a decorator.
@cache_response(ttl=60)\nasync def list_items(request): ...\n
- Keyed on path + query + user.
- No-op when no store is wired.
invalidate(*resources) bumps a generation counter for write-triggered invalidation.
"},{"location":"09_infra_adaptors/#event-progress-bus","title":"Event / progress bus","text":"A port with an in-memory default adapter (publish, get_events). Swap for a durable broker in production (see Worker & Progress).
"},{"location":"09_infra_adaptors/#external-clients-ingestion","title":"External clients (ingestion)","text":"External fetchers (files, mail, HTTP) are adapters that wrap ingestion libraries. Construct them in the composition root and inject into orchestrators.
Libraries: the ingestion library (formats) and the mail library (email).
"},{"location":"09_infra_adaptors/#llm-clients","title":"LLM clients","text":"When a pipeline calls a model provider, route through a pool adaptor that handles provider selection, retries, and client lifecycle:
pool = setup_async_pool(provider=settings.llm_provider)\nresult = await pool.complete(model=\"...\", messages=[...])\n
Providers are selected by LLM_PROVIDER and configured by environment.
"},{"location":"09_infra_adaptors/#key-value-store","title":"Key-value store","text":"For report/snapshot storage, provide a pluggable store port with a Redis adapter and an in-memory fallback.
"},{"location":"09_infra_adaptors/#rule","title":"Rule","text":"If a capability already exists as a library (DB, DI, ingestion, mail, auth, docs), use it. Writing a private copy of a library is a defect.
"},{"location":"12_genericity_matrix/","title":"Genericity Matrix","text":""},{"location":"12_genericity_matrix/#summary","title":"Summary","text":"This page states precisely what is reusable as-is, what is a template to adapt, and what is app-specific and must be authored. Use it to scope a new build.
"},{"location":"12_genericity_matrix/#reusable-as-is-do-not-rewrite","title":"Reusable as-is (do not rewrite)","text":"Area Artifact Layering & dependency rule the package layout and import rules Composition root the init.py shape, idempotency, get_svc overloading Web edge OpenAPIFirstApp bootstrap, handler-name = operationId, thin handlers CRUD slice the four-file pattern (model / ports-db / ports-service / converters) Generic CRUD service the base service adaptor Repository factory base / populating / cached selection Worker the poll loop, backoff, retry, status machine Progress bus the bus port + in-memory adapter + streaming endpoint Human-in-the-loop the durable pause + question document + resolve endpoint + resume map Infra adaptors HTTP cache decorator, KV store port, LLM pool scaffold Testing integration harness with fakes, fixture-parity pattern Docs GSDFC + generated stubs + MCP bundle pipeline"},{"location":"12_genericity_matrix/#template-to-adapt-copy-then-change-names","title":"Template to adapt (copy, then change names)","text":"Artifact Change pyproject.toml project name, dependencies Dockerfile / docker-compose.yaml service name, ports, services .env.example connection strings, provider keys openapi.yaml every path/schema/operationId scripts/ seed and operator tasks"},{"location":"12_genericity_matrix/#app-specific-must-be-authored","title":"App-specific (must be authored)","text":"Artifact Why Resource models the business shape Repository populate/cache rules the relationships Service business logic domain behaviour Pipeline families (if any) the transformation graph Pattern/rule resources data-driven policy Fixtures & golden outputs test data"},{"location":"12_genericity_matrix/#the-two-axes","title":"The two axes","text":"Everything app-specific reduces to two axes:
- Resource model \u2014 the set of resources, their fields, relationships, and the OpenAPI contract.
- Pipeline family \u2014 only if inputs require multi-step transformation.
If you can specify those two axes, the rest of the app follows mechanically.
"},{"location":"13_library_registry/","title":"Library Registry","text":""},{"location":"13_library_registry/#summary","title":"Summary","text":"The Aetos ecosystem provides internal libraries for the shared concerns of every application. Use them. Writing a private copy of any capability below 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}.
"},{"location":"13_library_registry/#catalog","title":"Catalog","text":"Library Import Purpose Use instead of MCP DAG Pipe dagpipe Deterministic state-transformation DAG engine hand-rolled orchestration/topological ordering :8006 Hexa hexa Declarative DI / composition-root tooling manual wiring, hand-written factories (see note) Mongo Ops mongo_ops Async MongoDB repositories, cache, populate, transactions raw Motor CRUD and connection lifecycle :8007 Omniread omniread Format-agnostic fetch + parse (HTML, PDF, CSV, XLSX) custom scrapers/parsers :8003 OpenAPI First openapi_first Spec-first FastAPI bootstrap, client, codegen decorator routers and hand-written clients :8004 Mail Intake mail_intake Provider-agnostic read-only email ingestion Gmail OAuth + MIME parsing :8002 Py JWT jwtlib JWT auth primitives (register/login/introspect) password hashing + token code :8005 Doc Forge docforge Docstring compiler \u2192 MkDocs + MCP docs custom doc generators :8001 Auth Server (service) Central identity service (register/login/me/introspect) building auth endpoints yourself \u2014"},{"location":"13_library_registry/#how-to-choose","title":"How to choose","text":"You need to\u2026 Use persist and query documents Mongo Ops compose dependencies Hexa run a multi-step transformation DAG Pipe read/parse an arbitrary input format Omniread expose a contract-first HTTP API OpenAPI First ingest email Mail Intake authenticate users / verify tokens Py JWT (+ Auth Server) generate documentation Doc Forge"},{"location":"13_library_registry/#discovery","title":"Discovery","text":"A single standards MCP exposes find_library(capability), which returns the matching library, its import name, its \"use instead of\" guidance, and its MCP endpoint. Query it before writing any infrastructure.
"},{"location":"13_library_registry/#notes","title":"Notes","text":" - Hexa ships an MCP bundle but is not yet assigned a dedicated port in the hub config; it is reachable via the standards registry entry and will be wired to a port.
- Library MCP servers expose documentation resources (not executable tools): they tell an agent the available API surface so it does not invent code.
"}]}
\ No newline at end of file
+{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"Standards Reference","text":"The reference half of the Standards description: the enforceable rules, patterns, and the internal library catalog that agents consult while coding.
- Layering Rules \u2014 the strict request \u2192 domain \u2192 repository layering and the rules that enforce it.
- Composition Root \u2014 a single composition point wiring the whole app.
- Spec-First Routing \u2014 the OpenAPI contract as the routing source of truth.
- CRUD Template \u2014 the standardized generic resource endpoint scaffold.
- Pipeline Scaffold \u2014 the deterministic pipeline skeleton for multi-stage jobs.
- Worker & Progress \u2014 queued work, progress reporting, and visibility.
- Human-in-the-Loop \u2014 guardrails and approvals inside automations.
- Infra Adapters \u2014 how infrastructure connects without leaking into domain.
- Genericity Matrix \u2014 what is generic vs app-specific.
- Library Registry \u2014 the catalog of internal libraries behind all of it.
Read the Wiki for the philosophy and rationale; these pages are the reference that mirrors the standards MCP bundle.
"},{"location":"02_layering_rules/","title":"Layering Rules","text":""},{"location":"02_layering_rules/#summary","title":"Summary","text":"The dependency direction is fixed. Violating it is a design defect.
"},{"location":"02_layering_rules/#allowed-imports","title":"Allowed imports","text":"interfaces/* -> domain/* (services, ports, models)\ninterfaces/* -> infra/adapters/* (only read-only value objects)\ndomain/* -> domain/* (same layer)\ndomain/* -> pipelines/* (call a pipeline via a port)\ndomain/*/ports/* -> infra/* (bind adapters + libraries at the seam)\ninfra/adapters/* -> domain/ports/* (implement the interface)\nmain.py/init.py -> everything (composition root only)\n
"},{"location":"02_layering_rules/#forbidden-imports","title":"Forbidden imports","text":"domain/* -> interfaces/* (never)\ndomain/* -> fastapi/starlette (never)\ninfra/adapters/* -> interfaces/* (never)\ninterfaces/* -> infra/db/* (state lives in resources, not imports)\n
"},{"location":"02_layering_rules/#why","title":"Why","text":" - The business logic is testable without a server or an event loop.
- Adapters can be swapped (in-memory fake, real database) without touching business logic.
- The HTTP edge stays thin and replaceable.
"},{"location":"02_layering_rules/#the-portadapter-contract","title":"The port/adapter contract","text":"The ports/ package is the seam where the domain meets infrastructure. Each domain/<resource>/ports/ module declares the resource's models/document shape and exposes factory functions that bind a concrete adapter \u2014 so a port module is allowed to import from infra/ and from the persistence libraries. Everything outside ports/ in domain/ stays framework- and driver-free.
domain/<resource>/ports/db.py -> document model + create_<resource>_repo()\ndomain/<resource>/ports/service.py -> service + get_<resource>_service()\ninfra/db/repos/<vendor>.py -> repository adapter factory\ninfra/adapters/service.py -> generic CRUD service base\n
The composition root calls the port factories; nothing else constructs an adapter.
"},{"location":"02_layering_rules/#enforcement","title":"Enforcement","text":" domain/ must never import the web framework (fastapi / starlette) or interfaces/. Only the ports/ seam, infra/, and the composition root touch drivers. - Reviewers (and CI) reject
import fastapi inside domain/ or infra/.
"},{"location":"03_composition_root/","title":"Composition Root","text":""},{"location":"03_composition_root/#summary","title":"Summary","text":"All wiring happens in exactly one module. It builds a flat resource registry and exposes it to the HTTP layer and to scripts.
"},{"location":"03_composition_root/#the-resource-registry","title":"The resource registry","text":"init.py returns a plain dict[str, Any]. Keys follow two conventions:
<resource>_repo \u2014 a repository adapter for a resource. <resource>_svc \u2014 a service for a resource.
Plus named infrastructure adaptors:
http_cache_store \u2014 response cache backend. progress_bus \u2014 progress/event bus. <vendor>_<kind> \u2014 fetchers, clients, stores.
The API stores the dict on app.state.db_resources during lifespan startup. Handlers read from it; nothing imports concrete adapters.
"},{"location":"03_composition_root/#shape","title":"Shape","text":"_resources: dict = {}\n_initialized = False\n\nasync def init(settings=None) -> dict:\n global _initialized\n if _initialized:\n return _resources\n\n await setup_database(uri=settings.mongo_uri, db_name=settings.mongo_db)\n\n # 1. leaf repositories\n user_repo = create_user_repo()\n\n # 2. dependent repositories (dependencies passed by keyword)\n wallet_repo = create_wallet_repo(user_repo=user_repo)\n\n # 3. services (build via the DI library, injecting the repo)\n _resources[\"user_repo\"] = user_repo\n _resources[\"wallet_repo\"] = wallet_repo\n _resources[\"user_svc\"] = get_user_service(user_repo)\n _resources[\"wallet_svc\"] = get_wallet_service(wallet_repo)\n\n # 4. infrastructure adaptors (optional)\n _resources[\"http_cache_store\"] = HttpCacheStore(...)\n _resources[\"progress_bus\"] = ProgressEventBus()\n\n _initialized = True\n return _resources\n\nasync def shutdown() -> None:\n global _initialized\n await close_stores(_resources)\n await shutdown_database()\n _resources.clear()\n _initialized = False\n
"},{"location":"03_composition_root/#typed-accessors","title":"Typed accessors","text":"Expose a get_svc(name) helper with one @overload per key so scripts get typing without importing concrete classes:
@overload\ndef get_svc(name: Literal[\"user_svc\"]) -> UserService: ...\n@overload\ndef get_svc(name: Literal[\"wallet_svc\"]) -> WalletService: ...\n\ndef get_svc(name: str):\n return _resources[name]\n
Also expose get_db() (raw database handle) and pure helpers such as compute_fingerprint(payload) (canonical hash used for deduplication).
"},{"location":"03_composition_root/#rules","title":"Rules","text":" init() is idempotent. - The composition root is the only place that imports concrete adapters.
- Handlers never call
init(); they read request.app.state.db_resources. - Scripts call
init() / shutdown() around their work.
"},{"location":"04_spec_first_routing/","title":"Spec-First Routing","text":""},{"location":"04_spec_first_routing/#summary","title":"Summary","text":"The OpenAPI document is the single source of truth for the HTTP contract. Handlers are bound to it by name; there is no decorator router.
"},{"location":"04_spec_first_routing/#the-contract","title":"The contract","text":" openapi.yaml (or .json) declares paths, schemas, parameters, responses, and security. - A custom
operationId names the handler function. - The framework (
openapi_first) validates requests/responses against the spec and dispatches to the handler whose name equals operationId.
"},{"location":"04_spec_first_routing/#app-bootstrap","title":"App bootstrap","text":"# 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
lifespan calls the composition root:
@asynccontextmanager\nasync def lifespan(app):\n app.state.db_resources = await init()\n yield\n await shutdown()\n
"},{"location":"04_spec_first_routing/#handler-registry","title":"Handler registry","text":"interfaces/openapi.py is a flat re-export of every handler, so the framework can resolve operationId across the whole app:
from interfaces.item.routes import *\nfrom interfaces.wallet.routes import *\nfrom interfaces.progress.sse import *\n
"},{"location":"04_spec_first_routing/#handler-shape","title":"Handler shape","text":"Handlers are thin and synchronous with respect to business logic:
async 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
"},{"location":"04_spec_first_routing/#conventions","title":"Conventions","text":" operationId naming: list_<resource>, create_<resource>, get_<resource>, update_<resource>, delete_<resource>. - Read-only endpoints may use a cache decorator keyed on path + query + user.
- Validation errors ->
400; not found -> 404; delete -> 204. - Security is declared once in the spec and enforced by the framework; handlers contain no auth code.
"},{"location":"04_spec_first_routing/#codegen","title":"Codegen","text":"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.
"},{"location":"05_crud_template/","title":"CRUD Template","text":""},{"location":"05_crud_template/#summary","title":"Summary","text":"Every resource follows the same four-file slice. Repetition is intentional: it makes the codebase predictable and generatable.
"},{"location":"05_crud_template/#the-slice","title":"The slice","text":"domain/<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
"},{"location":"05_crud_template/#modelspy","title":"models.py","text":"A framework-free Pydantic model describing the business shape.
class Item(BaseModel):\n name: str\n quantity: int = 0\n
"},{"location":"05_crud_template/#portsdbpy","title":"ports/db.py","text":"The persistence model extends the business model and adds database concerns. The repository factory selects the right repository flavour.
class 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
"},{"location":"05_crud_template/#portsservicepy","title":"ports/service.py","text":"The service is built through the DI library, injecting the repository. The generic CRUD base provides create/read/update/patch/delete/search/....
class 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
"},{"location":"05_crud_template/#naming-conventions","title":"Naming conventions","text":"Concept Convention collection plural snake case (items) document <Resource>Document repository factory create_<resource>_repo service <Resource>Service service factory get_<resource>_service resource key <resource>_svc operationId list_items, create_item, ..."},{"location":"05_crud_template/#optional-files","title":"Optional files","text":" converters.py \u2014 pure functions mapping between domain types. ports/enrichment.py \u2014 adapts a pipeline into an async callable the service can invoke (used when a resource is populated by a pipeline).
"},{"location":"06_pipeline_scaffold/","title":"Pipeline Scaffold","text":""},{"location":"06_pipeline_scaffold/#summary","title":"Summary","text":"Data-driven processing is expressed as a composition graph. The graph is built by a DI library; execution is driven by a DAG engine. Stages are pure and individually testable.
"},{"location":"06_pipeline_scaffold/#when-to-use-a-pipeline","title":"When to use a pipeline","text":"Use a pipeline when a resource is produced by a multi-step transformation of an input (a document, a file, a message) rather than by a simple CRUD write.
"},{"location":"06_pipeline_scaffold/#structure-of-a-family","title":"Structure of a family","text":"A family is one input shape (e.g. a file format or a message kind). Each family has the same skeleton:
pipelines/<family>/\n abc.py # port contracts (stage interfaces + root interface)\n impl.py # concrete stages + root implementation\n pipeline.yaml # graph spec: nodes, edges, roots, schema\n resources/\n pipeline.yaml # runtime spec consumed by the loader\n patterns/*.yaml # rule fragments (pattern registry)\n banks/<variant>.py # optional variants that re-pin a few slots\n
"},{"location":"06_pipeline_scaffold/#shared-scaffolding","title":"Shared scaffolding","text":"pipelines/shared/\n ports.py # RegistryPort, FinalizePort, etc.\n loader.py # build(root_cls) -> wires the graph + engine\n registry.py # resolve_root(...) -> root class for a (kind, variant)\n
"},{"location":"06_pipeline_scaffold/#the-loader","title":"The loader","text":"def build(root_cls, *, instances=None, config=None):\n # merge resources/patterns/*.yaml into a pattern registry\n # bind registry/config into each graph node by id\n # build the schema (state) class and the engine\n root = root_cls()\n root._engine = Engine(graph, state_cls, payload_cls)\n return root\n
"},{"location":"06_pipeline_scaffold/#the-registry","title":"The registry","text":"A single lookup replaces hand-rolled if/else selection:
_FAMILIES = {\n \"csv\": {\"acme\": {\"heuristic\": AcmeCsvPipeline}},\n \"json\": {\"globex\": {\"heuristic\": GlobexJsonPipeline}},\n}\n\ndef resolve_root(kind, variant, mode=\"heuristic\"):\n return _FAMILIES[kind][variant][mode]\n
Adding a new variant means adding one class and one registry entry.
"},{"location":"06_pipeline_scaffold/#driving-a-pipeline","title":"Driving a pipeline","text":"Stages are driven explicitly so intermediate state can be persisted:
pipeline = build(resolve_root(kind, variant))\ncontent = pipeline.ingest.materialize(raw_bytes)\nrecords = pipeline.reader.read(content)\nitems = pipeline.normalize.build(records)\n
"},{"location":"06_pipeline_scaffold/#testing","title":"Testing","text":"Because stages are pure, test each stage in isolation and test the assembled root against fixture inputs whose expected outputs are stored as golden files. A parity test replays fixtures headlessly.
"},{"location":"07_worker_and_progress/","title":"Worker & Progress","text":""},{"location":"07_worker_and_progress/#summary","title":"Summary","text":"Long-running work runs in a background worker. Jobs are durable documents that move through an explicit status machine. Progress is streamed to clients.
"},{"location":"07_worker_and_progress/#the-status-machine","title":"The status machine","text":"pending -> processing -> completed\n \\-> failed (retried up to N times)\n \\-> paused (waiting for human input)\n
pending \u2014 queued, eligible for pickup. processing \u2014 claimed by a worker. paused \u2014 blocked on a human-in-the-loop question. failed \u2014 error; retry_count increments until a max, then skipped. completed \u2014 terminal success.
"},{"location":"07_worker_and_progress/#the-poll-loop","title":"The poll loop","text":"async def poll(resources):\n backoff = MIN_POLL\n while True:\n jobs = await job_svc.search(filter={\"status\": \"pending\"}, limit=100)\n backoff = MIN_POLL if jobs else min(backoff * 2, MAX_POLL)\n for job in jobs:\n if (job.retry_count or 0) >= MAX_RETRIES:\n continue\n await job_svc.update_status(job.id, \"processing\")\n try:\n await process(job, resources)\n except PauseForResolution:\n await job_svc.update_status(job.id, \"paused\")\n except Exception as e:\n await job_svc.update_status(\n job.id, \"failed\", error=str(e),\n retry_count=(job.retry_count or 0) + 1,\n )\n await asyncio.sleep(backoff)\n
Tunables live in environment variables: WORKER_POLL_MIN, WORKER_POLL_MAX, WORKER_MAX_RETRIES.
"},{"location":"07_worker_and_progress/#progress-events","title":"Progress events","text":"A worker publishes structured progress events to a bus (a port). The HTTP edge exposes a streaming endpoint that replays events for a given job id.
class ProgressEvent(BaseModel):\n job_id: str\n step: str\n status: str # started | progress | completed | skipped | paused\n message: dict | None = None\n
The bus is an in-memory adapter by default; because it is a port, it can be swapped for a durable broker. Emit from the orchestrator at each stage:
await self.bus.publish(ProgressEvent(job_id=jid, step=\"normalize\",\n status=\"completed\", message={\"count\": n}))\n
"},{"location":"07_worker_and_progress/#streaming-endpoint","title":"Streaming endpoint","text":"async def stream_job_events(request, id):\n bus = request.app.state.db_resources.get(\"progress_bus\")\n if bus is None:\n return {\"events\": []}\n\n async def gen():\n sent = 0\n while True:\n events = bus.get_events(id)\n for e in events[sent:]:\n yield f\"data: {e.model_dump_json(exclude_none=True)}\\n\\n\"\n sent += 1\n await asyncio.sleep(0.5)\n\n return StreamingResponse(gen(), media_type=\"text/event-stream\")\n
Known limitation. The default in-memory bus is per-process. If the worker and the API run as separate processes, events published by the worker are not visible to the API. For production, back the bus with a shared broker. The port boundary makes this a one-adapter change.
"},{"location":"08_human_in_loop/","title":"Human-in-the-Loop","text":""},{"location":"08_human_in_loop/#summary","title":"Summary","text":"Some inputs are ambiguous and must be resolved by a human before processing can continue. The blueprint models this as a durable pause: an exception is raised, a question is recorded, the job pauses, and later resumes.
"},{"location":"08_human_in_loop/#the-mechanism","title":"The mechanism","text":" - During processing, a stage raises a domain exception carrying the context needed to ask the question:
raise PauseForResolution(\n job_id=jid,\n reference=\"<stable key for this decision>\",\n options=[...],\n context={...},\n)\n
- The orchestrator catches it, records a question document, and re-raises so the worker marks the job
paused. - A client lists pending questions and answers one:
GET /jobs/{id}/questions -> list pending questions\nPOST /questions/{qid}/resolve {choice} -> record the choice\n
- When the last pending question is resolved, the resolve endpoint flips the parent job back to
pending. - The worker picks it up again, rebuilds a resolution map from previously answered questions, and seeds it into the pipeline so the same decision is not asked twice.
"},{"location":"08_human_in_loop/#the-question-document","title":"The question document","text":"class Question(BaseDocument):\n job_id: str\n reference: str # stable key: stage|field|value\n status: str # pending | resolved\n options: list[dict]\n context: dict\n chosen: dict | None = None\n resolved_at: str | None = None\n
"},{"location":"08_human_in_loop/#resume-map","title":"Resume map","text":"The worker turns answered questions into a lookup keyed by reference, then injects it into the pipeline so that stage can skip re-raising:
resolved = {q.reference: q.chosen for q in await svc.search(\n filter={\"job_id\": jid, \"status\": \"resolved\"})}\n\npipeline = build(root, instances={\"pause\": handler}, config={\"resolved\": resolved})\n
"},{"location":"08_human_in_loop/#resolve-endpoint","title":"Resolve endpoint","text":"async def resolve_question(request, id, body):\n svc = request.app.state.db_resources[\"question_svc\"]\n q = await svc.read(id)\n await svc.update(id, {\"chosen\": body[\"chosen\"], \"status\": \"resolved\",\n \"resolved_at\": now()})\n\n pending = await svc.search(filter={\"job_id\": q.job_id, \"status\": \"pending\"},\n limit=1)\n if not pending:\n jobs = request.app.state.db_resources[\"job_svc\"]\n job = await jobs.read(q.job_id)\n if job and job.status == \"paused\":\n await jobs.update_status(q.job_id, \"pending\")\n return q\n
"},{"location":"08_human_in_loop/#cli-parity","title":"CLI parity","text":"The same flow is available through scripts/CLI: list, get, create, questions, resolve, retry.
"},{"location":"09_infra_adaptors/","title":"Infra Adapters","text":""},{"location":"09_infra_adaptors/#summary","title":"Summary","text":"All external systems are reached through adapters in infra/. Prefer an existing internal library over writing a new adapter.
"},{"location":"09_infra_adaptors/#database-mongodb","title":"Database (MongoDB)","text":" - A single connection manager owns the client lifecycle.
setup_database(uri, db_name) / shutdown_database(). - A repository factory selects the repository flavour:
repo_factory(collection, document, engine=None,\n populate_rules=None, cache=None)\n
- base repository \u2014 plain CRUD;
- populating repository \u2014 resolves referenced documents (foreign keys);
-
cached repository \u2014 an LRU/TTL cache in front of reads.
-
Referenced documents are described declaratively:
PopulateRule(field=\"owner\", collection=\"users\",\n nested_rules=[], max_depth=2, projection=None)\n
- Transactions use a session context manager provided by the library.
Library: the Mongo operations library (see registry). Do not write raw Motor calls in the domain.
"},{"location":"09_infra_adaptors/#http-response-cache","title":"HTTP response cache","text":"A generic GET cache sits behind a decorator.
@cache_response(ttl=60)\nasync def list_items(request): ...\n
- Keyed on path + query + user.
- No-op when no store is wired.
invalidate(*resources) bumps a generation counter for write-triggered invalidation.
"},{"location":"09_infra_adaptors/#event-progress-bus","title":"Event / progress bus","text":"A port with an in-memory default adapter (publish, get_events). Swap for a durable broker in production (see Worker & Progress).
"},{"location":"09_infra_adaptors/#external-clients-ingestion","title":"External clients (ingestion)","text":"External fetchers (files, mail, HTTP) are adapters that wrap ingestion libraries. Construct them in the composition root and inject into orchestrators.
Libraries: the ingestion library (formats) and the mail library (email).
"},{"location":"09_infra_adaptors/#llm-clients","title":"LLM clients","text":"When a pipeline calls a model provider, route through a pool adaptor that handles provider selection, retries, and client lifecycle:
pool = setup_async_pool(provider=settings.llm_provider)\nresult = await pool.complete(model=\"...\", messages=[...])\n
Providers are selected by LLM_PROVIDER and configured by environment.
"},{"location":"09_infra_adaptors/#key-value-store","title":"Key-value store","text":"For report/snapshot storage, provide a pluggable store port with a Redis adapter and an in-memory fallback.
"},{"location":"09_infra_adaptors/#rule","title":"Rule","text":"If a capability already exists as a library (DB, DI, ingestion, mail, auth, docs), use it. Writing a private copy of a library is a defect.
"},{"location":"12_genericity_matrix/","title":"Genericity Matrix","text":""},{"location":"12_genericity_matrix/#summary","title":"Summary","text":"This page states precisely what is reusable as-is, what is a template to adapt, and what is app-specific and must be authored. Use it to scope a new build.
"},{"location":"12_genericity_matrix/#reusable-as-is-do-not-rewrite","title":"Reusable as-is (do not rewrite)","text":"Area Artifact Layering & dependency rule the package layout and import rules Composition root the init.py shape, idempotency, get_svc overloading Web edge OpenAPIFirstApp bootstrap, handler-name = operationId, thin handlers CRUD slice the four-file pattern (model / ports-db / ports-service / converters) Generic CRUD service the base service adaptor Repository factory base / populating / cached selection Worker the poll loop, backoff, retry, status machine Progress bus the bus port + in-memory adapter + streaming endpoint Human-in-the-loop the durable pause + question document + resolve endpoint + resume map Infra adaptors HTTP cache decorator, KV store port, LLM pool scaffold Testing integration harness with fakes, fixture-parity pattern Docs GSDFC + generated stubs + MCP bundle pipeline"},{"location":"12_genericity_matrix/#template-to-adapt-copy-then-change-names","title":"Template to adapt (copy, then change names)","text":"Artifact Change pyproject.toml project name, dependencies Dockerfile / docker-compose.yaml service name, ports, services .env.example connection strings, provider keys openapi.yaml every path/schema/operationId scripts/ seed and operator tasks"},{"location":"12_genericity_matrix/#app-specific-must-be-authored","title":"App-specific (must be authored)","text":"Artifact Why Resource models the business shape Repository populate/cache rules the relationships Service business logic domain behaviour Pipeline families (if any) the transformation graph Pattern/rule resources data-driven policy Fixtures & golden outputs test data"},{"location":"12_genericity_matrix/#the-two-axes","title":"The two axes","text":"Everything app-specific reduces to two axes:
- Resource model \u2014 the set of resources, their fields, relationships, and the OpenAPI contract.
- Pipeline family \u2014 only if inputs require multi-step transformation.
If you can specify those two axes, the rest of the app follows mechanically.
"},{"location":"13_library_registry/","title":"Library Registry","text":""},{"location":"13_library_registry/#summary","title":"Summary","text":"The Aetos ecosystem provides internal libraries for the shared concerns of every application. Use them. Writing a private copy of any capability below 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}.
"},{"location":"13_library_registry/#catalog","title":"Catalog","text":"Library Import Purpose Use instead of MCP DAG Pipe dagpipe Deterministic state-transformation DAG engine hand-rolled orchestration/topological ordering :8006 Hexa hexa Declarative DI / composition-root tooling manual wiring, hand-written factories (see note) Mongo Ops mongo_ops Async MongoDB repositories, cache, populate, transactions raw Motor CRUD and connection lifecycle :8007 Omniread omniread Format-agnostic fetch + parse (HTML, PDF, CSV, XLSX) custom scrapers/parsers :8003 OpenAPI First openapi_first Spec-first FastAPI bootstrap, client, codegen decorator routers and hand-written clients :8004 Mail Intake mail_intake Provider-agnostic read-only email ingestion Gmail OAuth + MIME parsing :8002 Py JWT jwtlib JWT auth primitives (register/login/introspect) password hashing + token code :8005 Doc Forge docforge Docstring compiler \u2192 MkDocs + MCP docs custom doc generators :8001 Auth Server (service) Central identity service (register/login/me/introspect) building auth endpoints yourself \u2014"},{"location":"13_library_registry/#how-to-choose","title":"How to choose","text":"You need to\u2026 Use persist and query documents Mongo Ops compose dependencies Hexa run a multi-step transformation DAG Pipe read/parse an arbitrary input format Omniread expose a contract-first HTTP API OpenAPI First ingest email Mail Intake authenticate users / verify tokens Py JWT (+ Auth Server) generate documentation Doc Forge"},{"location":"13_library_registry/#discovery","title":"Discovery","text":"A single standards MCP exposes find_library(capability), which returns the matching library, its import name, its \"use instead of\" guidance, and its MCP endpoint. Query it before writing any infrastructure.
"},{"location":"13_library_registry/#notes","title":"Notes","text":" - Hexa ships an MCP bundle but is not yet assigned a dedicated port in the hub config; it is reachable via the standards registry entry and will be wired to a port.
- Library MCP servers expose documentation resources (not executable tools): they tell an agent the available API surface so it does not invent code.
"}]}
\ No newline at end of file
diff --git a/standards/mcp/modules/composition_root.json b/standards/mcp/modules/composition_root.json
index 0011498..44156b1 100644
--- a/standards/mcp/modules/composition_root.json
+++ b/standards/mcp/modules/composition_root.json
@@ -2,7 +2,7 @@
"module": "composition_root",
"content": {
"path": "composition_root",
- "docstring": "# Composition Root\n\n## Summary\n\nAll wiring happens in exactly one module. It builds a **flat resource\nregistry** and exposes it to the HTTP layer and to scripts.\n\n---\n\n## The resource registry\n\n`init.py` returns a plain `dict[str, Any]`. Keys follow two conventions:\n\n- `_repo` — a repository adapter for a resource.\n- `_svc` — a service for a resource.\n\nPlus named infrastructure adaptors:\n\n- `http_cache_store` — response cache backend.\n- `progress_bus` — progress/event bus.\n- `_` — fetchers, clients, stores.\n\nThe API stores the dict on `app.state.resources` during lifespan startup.\nHandlers read from it; nothing imports concrete adapters.\n\n## Shape\n\n```python\n_resources: dict = {}\n_initialized = False\n\nasync def init(settings=None) -> dict:\n global _initialized\n if _initialized:\n return _resources\n\n await setup_database(uri=settings.mongo_uri, db_name=settings.mongo_db)\n\n # 1. leaf repositories\n user_repo = create_user_repo()\n\n # 2. dependent repositories (dependencies passed by keyword)\n wallet_repo = create_wallet_repo(user_repo=user_repo)\n\n # 3. services (build via the DI library, injecting the repo)\n _resources[\"user_repo\"] = user_repo\n _resources[\"wallet_repo\"] = wallet_repo\n _resources[\"user_svc\"] = get_user_service(user_repo)\n _resources[\"wallet_svc\"] = get_wallet_service(wallet_repo)\n\n # 4. infrastructure adaptors (optional)\n _resources[\"http_cache_store\"] = HttpCacheStore(...)\n _resources[\"progress_bus\"] = ProgressEventBus()\n\n _initialized = True\n return _resources\n\nasync def shutdown() -> None:\n global _initialized\n await close_stores(_resources)\n await shutdown_database()\n _resources.clear()\n _initialized = False\n```\n\n## Typed accessors\n\nExpose a `get_svc(name)` helper with one `@overload` per key so scripts get\ntyping without importing concrete classes:\n\n```python\n@overload\ndef get_svc(name: Literal[\"user_svc\"]) -> UserService: ...\n@overload\ndef get_svc(name: Literal[\"wallet_svc\"]) -> WalletService: ...\n\ndef get_svc(name: str):\n return _resources[name]\n```\n\nAlso expose `get_db()` (raw database handle) and pure helpers such as\n`compute_fingerprint(payload)` (canonical hash used for deduplication).\n\n## Rules\n\n- `init()` is **idempotent**.\n- The composition root is the only place that imports concrete adapters.\n- Handlers never call `init()`; they read `request.app.state.resources`.\n- Scripts call `init()` / `shutdown()` around their work.\n",
+ "docstring": "# Composition Root\n\n## Summary\n\nAll wiring happens in exactly one module. It builds a **flat resource\nregistry** and exposes it to the HTTP layer and to scripts.\n\n---\n\n## The resource registry\n\n`init.py` returns a plain `dict[str, Any]`. Keys follow two conventions:\n\n- `_repo` — a repository adapter for a resource.\n- `_svc` — a service for a resource.\n\nPlus named infrastructure adaptors:\n\n- `http_cache_store` — response cache backend.\n- `progress_bus` — progress/event bus.\n- `_` — fetchers, clients, stores.\n\nThe API stores the dict on `app.state.db_resources` during lifespan startup.\nHandlers read from it; nothing imports concrete adapters.\n\n## Shape\n\n```python\n_resources: dict = {}\n_initialized = False\n\nasync def init(settings=None) -> dict:\n global _initialized\n if _initialized:\n return _resources\n\n await setup_database(uri=settings.mongo_uri, db_name=settings.mongo_db)\n\n # 1. leaf repositories\n user_repo = create_user_repo()\n\n # 2. dependent repositories (dependencies passed by keyword)\n wallet_repo = create_wallet_repo(user_repo=user_repo)\n\n # 3. services (build via the DI library, injecting the repo)\n _resources[\"user_repo\"] = user_repo\n _resources[\"wallet_repo\"] = wallet_repo\n _resources[\"user_svc\"] = get_user_service(user_repo)\n _resources[\"wallet_svc\"] = get_wallet_service(wallet_repo)\n\n # 4. infrastructure adaptors (optional)\n _resources[\"http_cache_store\"] = HttpCacheStore(...)\n _resources[\"progress_bus\"] = ProgressEventBus()\n\n _initialized = True\n return _resources\n\nasync def shutdown() -> None:\n global _initialized\n await close_stores(_resources)\n await shutdown_database()\n _resources.clear()\n _initialized = False\n```\n\n## Typed accessors\n\nExpose a `get_svc(name)` helper with one `@overload` per key so scripts get\ntyping without importing concrete classes:\n\n```python\n@overload\ndef get_svc(name: Literal[\"user_svc\"]) -> UserService: ...\n@overload\ndef get_svc(name: Literal[\"wallet_svc\"]) -> WalletService: ...\n\ndef get_svc(name: str):\n return _resources[name]\n```\n\nAlso expose `get_db()` (raw database handle) and pure helpers such as\n`compute_fingerprint(payload)` (canonical hash used for deduplication).\n\n## Rules\n\n- `init()` is **idempotent**.\n- The composition root is the only place that imports concrete adapters.\n- Handlers never call `init()`; they read `request.app.state.db_resources`.\n- Scripts call `init()` / `shutdown()` around their work.\n",
"objects": {}
}
}
\ No newline at end of file
diff --git a/standards/mcp/modules/human_in_loop.json b/standards/mcp/modules/human_in_loop.json
index ea11e76..0a6fcaf 100644
--- a/standards/mcp/modules/human_in_loop.json
+++ b/standards/mcp/modules/human_in_loop.json
@@ -2,7 +2,7 @@
"module": "human_in_loop",
"content": {
"path": "human_in_loop",
- "docstring": "# Human-in-the-Loop\n\n## Summary\n\nSome inputs are ambiguous and must be resolved by a human before processing can\ncontinue. The blueprint models this as a **durable pause**: an exception is\nraised, a question is recorded, the job pauses, and later resumes.\n\n---\n\n## The mechanism\n\n1. During processing, a stage raises a domain exception carrying the context\n needed to ask the question:\n\n ```python\n raise PauseForResolution(\n job_id=jid,\n reference=\"\",\n options=[...],\n context={...},\n )\n ```\n\n2. The orchestrator catches it, records a **question document**, and re-raises\n so the worker marks the job `paused`.\n3. A client lists pending questions and answers one:\n\n ```\n GET /jobs/{id}/questions -> list pending questions\n POST /questions/{qid}/resolve {choice} -> record the choice\n ```\n\n4. When the **last** pending question is resolved, the resolve endpoint flips\n the parent job back to `pending`.\n5. The worker picks it up again, rebuilds a **resolution map** from previously\n answered questions, and seeds it into the pipeline so the same decision is\n not asked twice.\n\n## The question document\n\n```python\nclass Question(BaseDocument):\n job_id: str\n reference: str # stable key: stage|field|value\n status: str # pending | resolved\n options: list[dict]\n context: dict\n chosen: dict | None = None\n resolved_at: str | None = None\n```\n\n## Resume map\n\nThe worker turns answered questions into a lookup keyed by `reference`, then\ninjects it into the pipeline so that stage can skip re-raising:\n\n```python\nresolved = {q.reference: q.chosen for q in await svc.search(\n filter={\"job_id\": jid, \"status\": \"resolved\"})}\n\npipeline = build(root, instances={\"pause\": handler}, config={\"resolved\": resolved})\n```\n\n## Resolve endpoint\n\n```python\nasync def resolve_question(request, id, body):\n svc = request.app.state.resources[\"question_svc\"]\n q = await svc.read(id)\n await svc.update(id, {\"chosen\": body[\"chosen\"], \"status\": \"resolved\",\n \"resolved_at\": now()})\n\n pending = await svc.search(filter={\"job_id\": q.job_id, \"status\": \"pending\"},\n limit=1)\n if not pending:\n jobs = request.app.state.resources[\"job_svc\"]\n job = await jobs.read(q.job_id)\n if job and job.status == \"paused\":\n await jobs.update_status(q.job_id, \"pending\")\n return q\n```\n\n## CLI parity\n\nThe same flow is available through scripts/CLI: `list`, `get`, `create`,\n`questions`, `resolve`, `retry`.\n",
+ "docstring": "# Human-in-the-Loop\n\n## Summary\n\nSome inputs are ambiguous and must be resolved by a human before processing can\ncontinue. The blueprint models this as a **durable pause**: an exception is\nraised, a question is recorded, the job pauses, and later resumes.\n\n---\n\n## The mechanism\n\n1. During processing, a stage raises a domain exception carrying the context\n needed to ask the question:\n\n ```python\n raise PauseForResolution(\n job_id=jid,\n reference=\"\",\n options=[...],\n context={...},\n )\n ```\n\n2. The orchestrator catches it, records a **question document**, and re-raises\n so the worker marks the job `paused`.\n3. A client lists pending questions and answers one:\n\n ```\n GET /jobs/{id}/questions -> list pending questions\n POST /questions/{qid}/resolve {choice} -> record the choice\n ```\n\n4. When the **last** pending question is resolved, the resolve endpoint flips\n the parent job back to `pending`.\n5. The worker picks it up again, rebuilds a **resolution map** from previously\n answered questions, and seeds it into the pipeline so the same decision is\n not asked twice.\n\n## The question document\n\n```python\nclass Question(BaseDocument):\n job_id: str\n reference: str # stable key: stage|field|value\n status: str # pending | resolved\n options: list[dict]\n context: dict\n chosen: dict | None = None\n resolved_at: str | None = None\n```\n\n## Resume map\n\nThe worker turns answered questions into a lookup keyed by `reference`, then\ninjects it into the pipeline so that stage can skip re-raising:\n\n```python\nresolved = {q.reference: q.chosen for q in await svc.search(\n filter={\"job_id\": jid, \"status\": \"resolved\"})}\n\npipeline = build(root, instances={\"pause\": handler}, config={\"resolved\": resolved})\n```\n\n## Resolve endpoint\n\n```python\nasync def resolve_question(request, id, body):\n svc = request.app.state.db_resources[\"question_svc\"]\n q = await svc.read(id)\n await svc.update(id, {\"chosen\": body[\"chosen\"], \"status\": \"resolved\",\n \"resolved_at\": now()})\n\n pending = await svc.search(filter={\"job_id\": q.job_id, \"status\": \"pending\"},\n limit=1)\n if not pending:\n jobs = request.app.state.db_resources[\"job_svc\"]\n job = await jobs.read(q.job_id)\n if job and job.status == \"paused\":\n await jobs.update_status(q.job_id, \"pending\")\n return q\n```\n\n## CLI parity\n\nThe same flow is available through scripts/CLI: `list`, `get`, `create`,\n`questions`, `resolve`, `retry`.\n",
"objects": {}
}
}
\ No newline at end of file
diff --git a/standards/mcp/modules/layering_rules.json b/standards/mcp/modules/layering_rules.json
index 594b7e1..f4f94e7 100644
--- a/standards/mcp/modules/layering_rules.json
+++ b/standards/mcp/modules/layering_rules.json
@@ -2,7 +2,7 @@
"module": "layering_rules",
"content": {
"path": "layering_rules",
- "docstring": "# Layering Rules\n\n## Summary\n\nThe dependency direction is fixed. Violating it is a design defect.\n\n---\n\n## Allowed imports\n\n```\ninterfaces/* -> domain/* (services, ports, models)\ninterfaces/* -> infra/adapters/* (only read-only value objects)\ndomain/* -> domain/* (same layer)\ndomain/* -> pipelines/* (call a pipeline via a port)\ninfra/adapters/* -> domain/ports/* (implement the interface)\nmain.py/init.py -> everything (composition root only)\n```\n\n## Forbidden imports\n\n```\ndomain/* -> interfaces/* (never)\ndomain/* -> fastapi/starlette (never)\ninfra/adapters/* -> interfaces/* (never)\ninterfaces/* -> infra/db/* (state lives in resources, not imports)\n```\n\n## Why\n\n- A pure domain is testable without a server, a database, or an event loop.\n- Adapters can be swapped (in-memory fake, real database) without touching\n business logic.\n- The HTTP edge stays thin and replaceable.\n\n## The port/adapter contract\n\nA **port** is an abstract class or protocol declared in `domain//ports/`.\nAn **adapter** implements it in `infra/adapters/`. The composition root binds\nthe two.\n\n```\ndomain//ports/db.py -> repository port + document model\ndomain//ports/service.py -> service + factory\ninfra/db/repos/.py -> repository adapter factory\ninfra/adapters/service.py -> generic CRUD service base\n```\n\n## Enforcement\n\n- Keep every domain module importable with only the standard library and\n Pydantic. If a domain import pulls in `fastapi`, the layering is broken.\n- Reviewers (and CI) reject `import fastapi` inside `domain/` or `infra/`.\n",
+ "docstring": "# Layering Rules\n\n## Summary\n\nThe dependency direction is fixed. Violating it is a design defect.\n\n---\n\n## Allowed imports\n\n```\ninterfaces/* -> domain/* (services, ports, models)\ninterfaces/* -> infra/adapters/* (only read-only value objects)\ndomain/* -> domain/* (same layer)\ndomain/* -> pipelines/* (call a pipeline via a port)\ndomain/*/ports/* -> infra/* (bind adapters + libraries at the seam)\ninfra/adapters/* -> domain/ports/* (implement the interface)\nmain.py/init.py -> everything (composition root only)\n```\n\n## Forbidden imports\n\n```\ndomain/* -> interfaces/* (never)\ndomain/* -> fastapi/starlette (never)\ninfra/adapters/* -> interfaces/* (never)\ninterfaces/* -> infra/db/* (state lives in resources, not imports)\n```\n\n## Why\n\n- The business logic is testable without a server or an event loop.\n- Adapters can be swapped (in-memory fake, real database) without touching\n business logic.\n- The HTTP edge stays thin and replaceable.\n\n## The port/adapter contract\n\nThe `ports/` package is the **seam** where the domain meets infrastructure.\nEach `domain//ports/` module declares the resource's models/document\nshape and exposes **factory functions** that bind a concrete adapter — so a port\nmodule *is* allowed to import from `infra/` and from the persistence libraries.\nEverything outside `ports/` in `domain/` stays framework- and driver-free.\n\n```\ndomain//ports/db.py -> document model + create__repo()\ndomain//ports/service.py -> service + get__service()\ninfra/db/repos/.py -> repository adapter factory\ninfra/adapters/service.py -> generic CRUD service base\n```\n\nThe composition root calls the port factories; nothing else constructs an\nadapter.\n\n## Enforcement\n\n- `domain/` must never import the web framework (`fastapi` / `starlette`) or\n `interfaces/`. Only the `ports/` seam, `infra/`, and the composition root\n touch drivers.\n- Reviewers (and CI) reject `import fastapi` inside `domain/` or `infra/`.\n",
"objects": {}
}
}
\ No newline at end of file
diff --git a/standards/mcp/modules/spec_first_routing.json b/standards/mcp/modules/spec_first_routing.json
index 9db3023..96a3371 100644
--- a/standards/mcp/modules/spec_first_routing.json
+++ b/standards/mcp/modules/spec_first_routing.json
@@ -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=\" 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.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.resources[\"item_svc\"]\n return await svc.create(Item.model_validate(body))\n```\n\n## Conventions\n\n- `operationId` naming: `list_`, `create_`,\n `get_`, `update_`, `delete_`.\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=\" 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_`, `create_`,\n `get_`, `update_`, `delete_`.\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",
"objects": {}
}
}
\ No newline at end of file
diff --git a/standards/mcp/modules/worker_and_progress.json b/standards/mcp/modules/worker_and_progress.json
index 09d79ef..37635a3 100644
--- a/standards/mcp/modules/worker_and_progress.json
+++ b/standards/mcp/modules/worker_and_progress.json
@@ -2,7 +2,7 @@
"module": "worker_and_progress",
"content": {
"path": "worker_and_progress",
- "docstring": "# Worker & Progress\n\n## Summary\n\nLong-running work runs in a background worker. Jobs are durable documents that\nmove through an explicit status machine. Progress is streamed to clients.\n\n---\n\n## The status machine\n\n```\npending -> processing -> completed\n \\-> failed (retried up to N times)\n \\-> paused (waiting for human input)\n```\n\n- `pending` — queued, eligible for pickup.\n- `processing` — claimed by a worker.\n- `paused` — blocked on a human-in-the-loop question.\n- `failed` — error; `retry_count` increments until a max, then skipped.\n- `completed` — terminal success.\n\n## The poll loop\n\n```python\nasync def poll(resources):\n backoff = MIN_POLL\n while True:\n jobs = await job_svc.search(filter={\"status\": \"pending\"}, limit=100)\n backoff = MIN_POLL if jobs else min(backoff * 2, MAX_POLL)\n for job in jobs:\n if (job.retry_count or 0) >= MAX_RETRIES:\n continue\n await job_svc.update_status(job.id, \"processing\")\n try:\n await process(job, resources)\n except PauseForResolution:\n await job_svc.update_status(job.id, \"paused\")\n except Exception as e:\n await job_svc.update_status(\n job.id, \"failed\", error=str(e),\n retry_count=(job.retry_count or 0) + 1,\n )\n await asyncio.sleep(backoff)\n```\n\nTunables live in environment variables: `WORKER_POLL_MIN`, `WORKER_POLL_MAX`,\n`WORKER_MAX_RETRIES`.\n\n## Progress events\n\nA worker publishes structured progress events to a **bus** (a port). The HTTP\nedge exposes a streaming endpoint that replays events for a given job id.\n\n```python\nclass ProgressEvent(BaseModel):\n job_id: str\n step: str\n status: str # started | progress | completed | skipped | paused\n message: dict | None = None\n```\n\nThe bus is an in-memory adapter by default; because it is a port, it can be\nswapped for a durable broker. Emit from the orchestrator at each stage:\n\n```python\nawait self.bus.publish(ProgressEvent(job_id=jid, step=\"normalize\",\n status=\"completed\", message={\"count\": n}))\n```\n\n## Streaming endpoint\n\n```python\nasync def stream_job_events(request, id):\n bus = request.app.state.resources.get(\"progress_bus\")\n if bus is None:\n return {\"events\": []}\n\n async def gen():\n sent = 0\n while True:\n events = bus.get_events(id)\n for e in events[sent:]:\n yield f\"data: {e.model_dump_json(exclude_none=True)}\\n\\n\"\n sent += 1\n await asyncio.sleep(0.5)\n\n return StreamingResponse(gen(), media_type=\"text/event-stream\")\n```\n\n> **Known limitation.** The default in-memory bus is per-process. If the worker\n> and the API run as separate processes, events published by the worker are not\n> visible to the API. For production, back the bus with a shared broker. The\n> port boundary makes this a one-adapter change.\n",
+ "docstring": "# Worker & Progress\n\n## Summary\n\nLong-running work runs in a background worker. Jobs are durable documents that\nmove through an explicit status machine. Progress is streamed to clients.\n\n---\n\n## The status machine\n\n```\npending -> processing -> completed\n \\-> failed (retried up to N times)\n \\-> paused (waiting for human input)\n```\n\n- `pending` — queued, eligible for pickup.\n- `processing` — claimed by a worker.\n- `paused` — blocked on a human-in-the-loop question.\n- `failed` — error; `retry_count` increments until a max, then skipped.\n- `completed` — terminal success.\n\n## The poll loop\n\n```python\nasync def poll(resources):\n backoff = MIN_POLL\n while True:\n jobs = await job_svc.search(filter={\"status\": \"pending\"}, limit=100)\n backoff = MIN_POLL if jobs else min(backoff * 2, MAX_POLL)\n for job in jobs:\n if (job.retry_count or 0) >= MAX_RETRIES:\n continue\n await job_svc.update_status(job.id, \"processing\")\n try:\n await process(job, resources)\n except PauseForResolution:\n await job_svc.update_status(job.id, \"paused\")\n except Exception as e:\n await job_svc.update_status(\n job.id, \"failed\", error=str(e),\n retry_count=(job.retry_count or 0) + 1,\n )\n await asyncio.sleep(backoff)\n```\n\nTunables live in environment variables: `WORKER_POLL_MIN`, `WORKER_POLL_MAX`,\n`WORKER_MAX_RETRIES`.\n\n## Progress events\n\nA worker publishes structured progress events to a **bus** (a port). The HTTP\nedge exposes a streaming endpoint that replays events for a given job id.\n\n```python\nclass ProgressEvent(BaseModel):\n job_id: str\n step: str\n status: str # started | progress | completed | skipped | paused\n message: dict | None = None\n```\n\nThe bus is an in-memory adapter by default; because it is a port, it can be\nswapped for a durable broker. Emit from the orchestrator at each stage:\n\n```python\nawait self.bus.publish(ProgressEvent(job_id=jid, step=\"normalize\",\n status=\"completed\", message={\"count\": n}))\n```\n\n## Streaming endpoint\n\n```python\nasync def stream_job_events(request, id):\n bus = request.app.state.db_resources.get(\"progress_bus\")\n if bus is None:\n return {\"events\": []}\n\n async def gen():\n sent = 0\n while True:\n events = bus.get_events(id)\n for e in events[sent:]:\n yield f\"data: {e.model_dump_json(exclude_none=True)}\\n\\n\"\n sent += 1\n await asyncio.sleep(0.5)\n\n return StreamingResponse(gen(), media_type=\"text/event-stream\")\n```\n\n> **Known limitation.** The default in-memory bus is per-process. If the worker\n> and the API run as separate processes, events published by the worker are not\n> visible to the API. For production, back the bus with a shared broker. The\n> port boundary makes this a one-adapter change.\n",
"objects": {}
}
}
\ No newline at end of file
diff --git a/standards/src/lib/02_layering_rules.md b/standards/src/lib/02_layering_rules.md
index 580805c..aceb185 100644
--- a/standards/src/lib/02_layering_rules.md
+++ b/standards/src/lib/02_layering_rules.md
@@ -13,6 +13,7 @@ interfaces/* -> domain/* (services, ports, models)
interfaces/* -> infra/adapters/* (only read-only value objects)
domain/* -> domain/* (same layer)
domain/* -> pipelines/* (call a pipeline via a port)
+domain/*/ports/* -> infra/* (bind adapters + libraries at the seam)
infra/adapters/* -> domain/ports/* (implement the interface)
main.py/init.py -> everything (composition root only)
```
@@ -28,26 +29,32 @@ interfaces/* -> infra/db/* (state lives in resources, not imports
## Why
-- A pure domain is testable without a server, a database, or an event loop.
+- The business logic is testable without a server or an event loop.
- Adapters can be swapped (in-memory fake, real database) without touching
business logic.
- The HTTP edge stays thin and replaceable.
## The port/adapter contract
-A **port** is an abstract class or protocol declared in `domain//ports/`.
-An **adapter** implements it in `infra/adapters/`. The composition root binds
-the two.
+The `ports/` package is the **seam** where the domain meets infrastructure.
+Each `domain//ports/` module declares the resource's models/document
+shape and exposes **factory functions** that bind a concrete adapter — so a port
+module *is* allowed to import from `infra/` and from the persistence libraries.
+Everything outside `ports/` in `domain/` stays framework- and driver-free.
```
-domain//ports/db.py -> repository port + document model
-domain//ports/service.py -> service + factory
+domain//ports/db.py -> document model + create__repo()
+domain//ports/service.py -> service + get__service()
infra/db/repos/.py -> repository adapter factory
infra/adapters/service.py -> generic CRUD service base
```
+The composition root calls the port factories; nothing else constructs an
+adapter.
+
## Enforcement
-- Keep every domain module importable with only the standard library and
- Pydantic. If a domain import pulls in `fastapi`, the layering is broken.
+- `domain/` must never import the web framework (`fastapi` / `starlette`) or
+ `interfaces/`. Only the `ports/` seam, `infra/`, and the composition root
+ touch drivers.
- Reviewers (and CI) reject `import fastapi` inside `domain/` or `infra/`.
diff --git a/standards/src/lib/03_composition_root.md b/standards/src/lib/03_composition_root.md
index 756aaac..81532fc 100644
--- a/standards/src/lib/03_composition_root.md
+++ b/standards/src/lib/03_composition_root.md
@@ -20,7 +20,7 @@ Plus named infrastructure adaptors:
- `progress_bus` — progress/event bus.
- `_` — fetchers, clients, stores.
-The API stores the dict on `app.state.resources` during lifespan startup.
+The API stores the dict on `app.state.db_resources` during lifespan startup.
Handlers read from it; nothing imports concrete adapters.
## Shape
@@ -85,5 +85,5 @@ Also expose `get_db()` (raw database handle) and pure helpers such as
- `init()` is **idempotent**.
- The composition root is the only place that imports concrete adapters.
-- Handlers never call `init()`; they read `request.app.state.resources`.
+- Handlers never call `init()`; they read `request.app.state.db_resources`.
- Scripts call `init()` / `shutdown()` around their work.
diff --git a/standards/src/lib/04_spec_first_routing.md b/standards/src/lib/04_spec_first_routing.md
index aa3e238..acb1a76 100644
--- a/standards/src/lib/04_spec_first_routing.md
+++ b/standards/src/lib/04_spec_first_routing.md
@@ -35,7 +35,7 @@ app = OpenAPIFirstApp(
```python
@asynccontextmanager
async def lifespan(app):
- app.state.resources = await init()
+ app.state.db_resources = await init()
yield
await shutdown()
```
@@ -57,7 +57,7 @@ Handlers are thin and synchronous with respect to business logic:
```python
async def create_item(request: Request, body: dict):
- svc = request.app.state.resources["item_svc"]
+ svc = request.app.state.db_resources["item_svc"]
return await svc.create(Item.model_validate(body))
```
diff --git a/standards/src/lib/07_worker_and_progress.md b/standards/src/lib/07_worker_and_progress.md
index 56c73fa..7742ec0 100644
--- a/standards/src/lib/07_worker_and_progress.md
+++ b/standards/src/lib/07_worker_and_progress.md
@@ -73,7 +73,7 @@ await self.bus.publish(ProgressEvent(job_id=jid, step="normalize",
```python
async def stream_job_events(request, id):
- bus = request.app.state.resources.get("progress_bus")
+ bus = request.app.state.db_resources.get("progress_bus")
if bus is None:
return {"events": []}
diff --git a/standards/src/lib/08_human_in_loop.md b/standards/src/lib/08_human_in_loop.md
index d46d5e5..3e3ee92 100644
--- a/standards/src/lib/08_human_in_loop.md
+++ b/standards/src/lib/08_human_in_loop.md
@@ -66,7 +66,7 @@ pipeline = build(root, instances={"pause": handler}, config={"resolved": resolve
```python
async def resolve_question(request, id, body):
- svc = request.app.state.resources["question_svc"]
+ svc = request.app.state.db_resources["question_svc"]
q = await svc.read(id)
await svc.update(id, {"chosen": body["chosen"], "status": "resolved",
"resolved_at": now()})
@@ -74,7 +74,7 @@ async def resolve_question(request, id, body):
pending = await svc.search(filter={"job_id": q.job_id, "status": "pending"},
limit=1)
if not pending:
- jobs = request.app.state.resources["job_svc"]
+ jobs = request.app.state.db_resources["job_svc"]
job = await jobs.read(q.job_id)
if job and job.status == "paused":
await jobs.update_status(q.job_id, "pending")
diff --git a/standards/src/wiki/01_overview.md b/standards/src/wiki/01_overview.md
index 0bb90d2..4c94019 100644
--- a/standards/src/wiki/01_overview.md
+++ b/standards/src/wiki/01_overview.md
@@ -17,9 +17,9 @@ flowchart LR
RT["/routes.py
thin handlers"]
end
- subgraph CORE["domain/ — pure business core"]
+ subgraph CORE["domain/ — business core"]
MD["models"]
- PT["ports (abstract)"]
+ PT["ports (adapter binding seam)"]
SV["services"]
end
@@ -44,11 +44,11 @@ flowchart LR
```
The rule holds at every edge: code in an outer ring may depend on a ring closer
-to the core, never the reverse. `domain/` never imports a web framework or a
-database driver.
+to the core, never the reverse. `domain/` never imports a web framework;
+only its `ports/` seam reaches into `infra/` to bind adapters.
> **Legend** — solid arrows are compile-time calls; dotted arrows are the
-> port/adapter contract, resolved by the composition root at startup.
+> port/adapter binding, performed by the composition root at startup.
---
@@ -57,7 +57,7 @@ database driver.
| Ring | May import | Never imports |
|-------------|------------------------------------|----------------------------------|
| `interfaces/` | `domain`, framework glue (glue only) | business logic, `infra` directly |
-| `domain/` | nothing but its own ports | web frameworks, drivers, `infra` |
+| `domain/` | its own models + its `ports/` seam (which binds `infra`/libraries) | web frameworks, `interfaces/` |
| `infra/` | `domain` ports, drivers, libraries | — |
| `init.py` | everything (the wiring point) | — |
@@ -67,8 +67,9 @@ database driver.
1. **One-way dependencies.** `interfaces -> domain -> infra` is allowed; the
reverse is forbidden.
-2. **Ports in the domain, adapters in infra.** A service depends on an abstract
- port; the composition root injects a concrete adapter.
+2. **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.
3. **One composition root.** Nothing constructs its own dependencies. All
wiring lives in one module (`init.py`) exposed as a flat resource registry.
4. **Spec-first.** The HTTP contract is declared in `openapi.yaml`; route
diff --git a/standards/src/wiki/02_components.md b/standards/src/wiki/02_components.md
index 622f1ba..5d0fe84 100644
--- a/standards/src/wiki/02_components.md
+++ b/standards/src/wiki/02_components.md
@@ -7,57 +7,75 @@ blueprint's component model — `templates/app/` is a runnable instance of it.
## 🗂️ Component model
+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.
+
+### 🔀 Runtime flow
+
+Who calls whom at request and job time:
+
```mermaid
-%%{init: {"flowchart": {"nodeSpacing": 55, "rankSpacing": 120}}}%%
+%%{init: {"flowchart": {"nodeSpacing": 50, "rankSpacing": 80}}}%%
flowchart TB
- subgraph EDGE["interfaces/ — HTTP edge"]
- OAS["openapi.yaml"]
- RU["resource routes"]
- JU["job routes"]
+ subgraph REQ["Request path — API process"]
+ direction LR
+ CL(["Client
operationId"]) --> RT["interfaces/<resource>/routes.py"]
+ RT --> SVC["<resource>_svc"]
+ SVC --> REPO["<resource>_repo"]
end
- subgraph CORE["domain/ — business core"]
- RI[" · models · ports · services"]
- JI[" · models · ports · services"]
- PI["pipelines/"]
+ subgraph WRK["Background path — worker process"]
+ direction LR
+ W["worker.py"] --> JSVC["job_svc"]
+ W --> PIPE["pipelines/<family>"]
+ PIPE --> ISVC["item_svc"]
+ JSVC --> JREPO["job_repo"]
+ ISVC --> IREPO["item_repo"]
end
subgraph INFRA["infra/ — adapters"]
- DB[("MongoDB repos")]
- CA["redis cache"]
- ST["stores · progress bus"]
- LL["llm clients"]
+ MONGO[("MongoDB / mongo_ops")]
end
- W["worker.py"]
- CR["init.py — composition root"]
-
- OAS --> RU
- OAS --> JU
- RU --> RI
- JU --> JI
- JI --> PI
- PI --> DB
- PI --> LL
- RI --> CA
- RI --> DB
- JI --> ST
- W --> JI
- W --> PI
-
- CR === RU
- CR === JU
- CR === RI
- CR === JI
- CR === PI
- CR === W
- CR === DB
- CR === CA
- CR === ST
+ REPO --> MONGO
+ JREPO --> MONGO
+ IREPO --> MONGO
```
-> **Legend** — solid arrows are direct calls; thick links are wires created by
-> the composition root. Every component is injected, never self-constructed.
+> **Legend** — solid arrows are runtime calls. Nothing is self-constructed.
+
+### 🧵 Composition-root wiring
+
+Who constructs what at startup:
+
+```mermaid
+%%{init: {"flowchart": {"nodeSpacing": 50, "rankSpacing": 80}}}%%
+flowchart TB
+ INIT["init.py — composition root"]
+ SETUP["infra/db/setup.py"]
+ FACT["domain/*/ports/*
factory seams"]
+ REPOS["repos
infra/db/repos/mongo.py"]
+ SVCES["services
CrudsServiceAdaptor"]
+ STATE["app.state.db_resources"]
+ HEXA["hexa.build"]
+ RT["routes.py"]
+ WK["worker.py"]
+
+ INIT --> SETUP
+ INIT --> FACT
+ FACT --> REPOS
+ FACT --> SVCES
+ REPOS --> SVCES
+ HEXA -.-> SVCES
+ INIT --> STATE
+ STATE --> RT
+ INIT --> WK
+```
+
+> **Legend** — `init.py` is the only place that imports concrete adapters. The
+> `ports/` factory seams bind `infra`; the API reads resources from
+> `app.state.db_resources`, the worker calls `init()` directly.
---
@@ -74,14 +92,15 @@ call a service, and shape the response. No business rules, no I/O.
### 2. `domain/` — the core
-Pure models, **ports** (abstract contracts), and services (use cases). No
-framework imports, no drivers.
+Models, ports, and services (use cases). No framework imports. The `ports/`
+package is the one seam allowed to reach into `infra/`: each port module declares
+the document shape and a factory that binds the adapter.
- `/models.py` — dataclasses, enums, document shapes.
-- `/ports/db.py` — repo contract; `/ports/service.py` —
- service contract.
-- Services compose ports and enforce the business rules; the composition root
- injects the implementations.
+- `/ports/db.py` — document model + `create__repo()`.
+- `/ports/service.py` — service + `get__service(repo)`.
+- Services compose ports and enforce the business rules; the port factories bind
+ the implementations, and the composition root calls them.
### 3. `infra/` — the adapters
@@ -117,7 +136,7 @@ stops resources on the app lifespan.
sequenceDiagram
participant C as Client
participant R as route (interfaces)
- participant S as service (domain)
+ participant S as service (port factory / adapter)
participant P as repo (infra)
participant DB as MongoDB
diff --git a/standards/src/wiki/04_development/01_testing.md b/standards/src/wiki/04_development/01_testing.md
index b6af74a..36fb371 100644
--- a/standards/src/wiki/04_development/01_testing.md
+++ b/standards/src/wiki/04_development/01_testing.md
@@ -20,7 +20,7 @@ tests/
## 🚀 Integration tests
- Build the app with `TestClient`.
-- Replace `app.state.resources` with fake repositories/services so no database
+- Replace `app.state.db_resources` with fake repositories/services so no database
is required.
- Patch the auth introspection call (`httpx.AsyncClient.post`) to return a
canned identity.
@@ -29,7 +29,7 @@ tests/
```python
client = TestClient(app)
-app.state.resources = {"item_svc": FakeItemService(), ...}
+app.state.db_resources = {"item_svc": FakeItemService(), ...}
def test_create_item():
r = client.post("/items", json={"name": "x"})
diff --git a/standards/src/wiki/index.md b/standards/src/wiki/index.md
index b01d3f9..afafd65 100644
--- a/standards/src/wiki/index.md
+++ b/standards/src/wiki/index.md
@@ -1,8 +1,8 @@
# 🧩 Standards — the Aetos App Blueprint
One reusable architecture for every backend application in the Aetos ecosystem:
-a spec-first HTTP edge, a pure domain core, a pluggable infrastructure layer,
-and a single composition root.
+a spec-first HTTP edge, a framework-free domain core, a pluggable infrastructure
+layer, and a single composition root.
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
diff --git a/standards/wiki/01_overview/index.html b/standards/wiki/01_overview/index.html
index 4061168..3428ef7 100644
--- a/standards/wiki/01_overview/index.html
+++ b/standards/wiki/01_overview/index.html
@@ -553,9 +553,9 @@ live in the reference pages.
RT["<resource>/routes.py<br/>thin handlers"]
end
- subgraph CORE["domain/ — pure business core"]
+ subgraph CORE["domain/ — business core"]
MD["models"]
- PT["ports (abstract)"]
+ PT["ports (adapter binding seam)"]
SV["services"]
end
@@ -578,11 +578,11 @@ live in the reference pages.
RP --> DB
AD --- CR
The rule holds at every edge: code in an outer ring may depend on a ring closer
-to the core, never the reverse. domain/ never imports a web framework or a
-database driver.
domain/ never imports a web framework;
+only its ports/ seam reaches into infra/ to bind adapters.
Legend — solid arrows are compile-time calls; dotted arrows are the -port/adapter contract, resolved by the composition root at startup.
+port/adapter binding, performed by the composition root at startup.
domain/infraports/ seam (which binds infra/libraries)interfaces/infra/interfaces -> domain -> infra is allowed; the
reverse is forbidden.init.py) exposed as a flat resource registry.openapi.yaml; route
diff --git a/standards/wiki/02_components/index.html b/standards/wiki/02_components/index.html
index a9886bd..783d787 100644
--- a/standards/wiki/02_components/index.html
+++ b/standards/wiki/02_components/index.html
@@ -433,6 +433,30 @@
+
+
templates/app/ is a runnable instance of it.
%%{init: {"flowchart": {"nodeSpacing": 55, "rankSpacing": 120}}}%%
+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.
+🔀 Runtime flow
+Who calls whom at request and job time:
+%%{init: {"flowchart": {"nodeSpacing": 50, "rankSpacing": 80}}}%%
flowchart TB
- subgraph EDGE["interfaces/ — HTTP edge"]
- OAS["openapi.yaml"]
- RU["resource routes"]
- JU["job routes"]
+ subgraph REQ["Request path — API process"]
+ direction LR
+ CL(["Client<br/>operationId"]) --> RT["interfaces/<resource>/routes.py"]
+ RT --> SVC["<resource>_svc"]
+ SVC --> REPO["<resource>_repo"]
end
- subgraph CORE["domain/ — business core"]
- RI["<resource> · models · ports · services"]
- JI["<job/worker> · models · ports · services"]
- PI["pipelines/<family>"]
+ subgraph WRK["Background path — worker process"]
+ direction LR
+ W["worker.py"] --> JSVC["job_svc"]
+ W --> PIPE["pipelines/<family>"]
+ PIPE --> ISVC["item_svc"]
+ JSVC --> JREPO["job_repo"]
+ ISVC --> IREPO["item_repo"]
end
subgraph INFRA["infra/ — adapters"]
- DB[("MongoDB repos")]
- CA["redis cache"]
- ST["stores · progress bus"]
- LL["llm clients"]
+ MONGO[("MongoDB / mongo_ops")]
end
- W["worker.py"]
- CR["init.py — composition root"]
-
- OAS --> RU
- OAS --> JU
- RU --> RI
- JU --> JI
- JI --> PI
- PI --> DB
- PI --> LL
- RI --> CA
- RI --> DB
- JI --> ST
- W --> JI
- W --> PI
-
- CR === RU
- CR === JU
- CR === RI
- CR === JI
- CR === PI
- CR === W
- CR === DB
- CR === CA
- CR === ST
+ REPO --> MONGO
+ JREPO --> MONGO
+ IREPO --> MONGO
-+Legend — solid arrows are direct calls; thick links are wires created by -the composition root. Every component is injected, never self-constructed.
+Legend — solid arrows are runtime calls. Nothing is self-constructed.
+
Who constructs what at startup:
+%%{init: {"flowchart": {"nodeSpacing": 50, "rankSpacing": 80}}}%%
+flowchart TB
+ INIT["init.py — composition root"]
+ SETUP["infra/db/setup.py"]
+ FACT["domain/*/ports/*<br/>factory seams"]
+ REPOS["repos<br/>infra/db/repos/mongo.py"]
+ SVCES["services<br/>CrudsServiceAdaptor"]
+ STATE["app.state.db_resources"]
+ HEXA["hexa.build"]
+ RT["routes.py"]
+ WK["worker.py"]
+
+ INIT --> SETUP
+ INIT --> FACT
+ FACT --> REPOS
+ FACT --> SVCES
+ REPOS --> SVCES
+ HEXA -.-> SVCES
+ INIT --> STATE
+ STATE --> RT
+ INIT --> WK
++Legend —
init.pyis the only place that imports concrete adapters. The +ports/factory seams bindinfra; the API reads resources from +app.state.db_resources, the worker callsinit()directly.
cache decorator on read endpoints.domain/ — the corePure models, ports (abstract contracts), and services (use cases). No -framework imports, no drivers.
+Models, ports, and services (use cases). No framework imports. The ports/
+package is the one seam allowed to reach into infra/: each port module declares
+the document shape and a factory that binds the adapter.
<aggregate>/models.py — dataclasses, enums, document shapes.<aggregate>/ports/db.py — repo contract; <aggregate>/ports/service.py —
- service contract.<aggregate>/ports/db.py — document model + create_<aggregate>_repo().<aggregate>/ports/service.py — service + get_<aggregate>_service(repo).infra/ — the adaptersConcrete implementations of the domain ports plus cross-cutting infrastructure:
@@ -689,7 +725,7 @@ stops resources on the app lifespan.sequenceDiagram
participant C as Client
participant R as route (interfaces)
- participant S as service (domain)
+ participant S as service (port factory / adapter)
participant P as repo (infra)
participant DB as MongoDB
diff --git a/standards/wiki/04_development/01_testing/index.html b/standards/wiki/04_development/01_testing/index.html
index 7d1dac1..54e6f45 100644
--- a/standards/wiki/04_development/01_testing/index.html
+++ b/standards/wiki/04_development/01_testing/index.html
@@ -573,7 +573,7 @@ blueprint app only when every gate below passes.
🚀 Integration tests
- Build the app with
TestClient.
-- Replace
app.state.resources with fake repositories/services so no database
+ - Replace
app.state.db_resources with fake repositories/services so no database
is required.
- Patch the auth introspection call (
httpx.AsyncClient.post) to return a
canned identity.
@@ -586,7 +586,7 @@ blueprint app only when every gate below passes.
4
5
6
client = TestClient(app)
-app.state.resources = {"item_svc": FakeItemService(), ...}
+app.state.db_resources = {"item_svc": FakeItemService(), ...}
def test_create_item():
r = client.post("/items", json={"name": "x"})
diff --git a/standards/wiki/index.html b/standards/wiki/index.html
index c4cf97b..e684740 100644
--- a/standards/wiki/index.html
+++ b/standards/wiki/index.html
@@ -521,8 +521,8 @@
🧩 Standards — the Aetos App Blueprint
One reusable architecture for every backend application in the Aetos ecosystem:
-a spec-first HTTP edge, a pure domain core, a pluggable infrastructure layer,
-and a single composition root.
+a spec-first HTTP edge, a framework-free domain core, a pluggable infrastructure
+layer, and a single composition root.
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 — expenses, health metrics, inventory, and analytics are all
diff --git a/standards/wiki/search/search_index.json b/standards/wiki/search/search_index.json
index 226a936..b8ab9db 100644
--- a/standards/wiki/search/search_index.json
+++ b/standards/wiki/search/search_index.json
@@ -1 +1 @@
-{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"\ud83e\udde9 Standards \u2014 the Aetos App Blueprint","text":"
One reusable architecture for every backend application in the Aetos ecosystem: a spec-first HTTP edge, a pure domain core, a pluggable infrastructure layer, and a single composition root.
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.
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 standards MCP bundle. Query find_library before writing any infrastructure.
"},{"location":"#key-features","title":"\ud83d\ude80 Key Features","text":" - \ud83e\udded Spec-first routing \u2014 the OpenAPI document, not decorator routers, is the source of truth (operationId lookup)
- \ud83c\udfd7\ufe0f One-way layering \u2014
interfaces \u2192 domain \u2192 infra, enforced - \ud83d\udd0c Single composition root \u2014 every dependency is wired in exactly one place
- \ud83e\udde9 Generic, not app-specific \u2014 reusable rules versus two authoring axes
- \ud83d\udcda Library-backed \u2014 DB, DI, pipelines, ingestion, auth, and docs come from internal libraries, never hand-written
- \ud83d\udea6 Explicit state machines \u2014 durable job statuses, progress, and human-in-the-loop pauses
"},{"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 Build Recipe The ordered, agentic checklist for a new app Testing & Quality Test layout and acceptance gates Reference (separate site, mirrors the MCP bundle): the enforceable rules \u2014 Layering, Composition Root, Spec-First Routing, CRUD Template, Pipeline Scaffold, Worker & Progress, Human-in-the-Loop, Infra Adapters, Genericity Matrix, Library Registry.
"},{"location":"#related-resources","title":"\ud83d\udd17 Related Resources","text":" - MCP bundle \u2014
standards server on :8009: read doc://index, doc://nav, doc://modules/{module}; call find_library(capability) - Library registry \u2014 the catalog of internal libraries behind every app (
libraries.json, served as registry://libraries) - Template \u2014 the runnable skeleton at
standards/src/templates/app/ - Copy the repo's
AGENTS.md-style build prompt from Build Recipe
\u00a9 Aetoskia Internal \u2014 standards
"},{"location":"01_overview/","title":"Overview","text":"The blueprint is a hexagonal (ports & 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.
"},{"location":"01_overview/#architecture","title":"\ud83c\udfd7\ufe0f Architecture","text":"flowchart LR\n subgraph EDGE[\"interfaces/ \u2014 HTTP edge\"]\n OAS[\"openapi.yaml<br/>operationId contract\"]\n RT[\"<resource>/routes.py<br/>thin handlers\"]\n end\n\n subgraph CORE[\"domain/ \u2014 pure business core\"]\n MD[\"models\"]\n PT[\"ports (abstract)\"]\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<br/>composition root\"]\n\n OAS --> RT\n RT --> SV\n SV --> PT\n SV --> RP\n PT -. depends on .- RP\n CR --- RT\n CR --- SV\n CR --- RP\n RP --> DB\n AD --- CR
The rule holds at every edge: code in an outer ring may depend on a ring closer to the core, never the reverse. domain/ never imports a web framework or a database driver.
Legend \u2014 solid arrows are compile-time calls; dotted arrows are the port/adapter contract, resolved by the composition root at startup.
"},{"location":"01_overview/#the-one-way-dependency-rule","title":"\ud83d\udd04 The one-way dependency rule","text":"Ring May import Never imports interfaces/ domain, framework glue (glue only) business logic, infra directly domain/ nothing but its own ports web frameworks, drivers, infra infra/ domain ports, drivers, libraries \u2014 init.py everything (the wiring point) \u2014"},{"location":"01_overview/#core-principles","title":"\ud83e\udded Core principles","text":" - One-way dependencies.
interfaces -> domain -> infra is allowed; the reverse is forbidden. - Ports in the domain, adapters in infra. A service depends on an abstract port; the composition root injects a concrete adapter.
- One composition root. Nothing constructs its own dependencies. All wiring lives in one module (
init.py) exposed as a flat resource registry. - Spec-first. The HTTP contract is declared in
openapi.yaml; route handlers are looked up by operationId. There are no decorator routers. - Reuse the libraries. Orchestration, DI, Mongo repositories, ingestion, auth, and docs come from internal libraries \u2014 see the Library Registry.
- Explicit state machines. Long-running work moves through explicit, durable statuses and can pause for human input.
"},{"location":"01_overview/#runtime-processes","title":"\u2699\ufe0f Runtime processes","text":"A standard app may run as up to three processes that share the same composition root:
Process Entry point Responsibility API main.py serve HTTP; start/stop resources via lifespan Worker worker.py poll durable jobs, run pipelines, publish progress Scripts python -m scripts.<name> one-shot operator tasks"},{"location":"01_overview/#read-next","title":"\u27a1\ufe0f Read Next","text":" - 02 \u2013 App Components \u00b7 03 \u2013 Build Recipe
"},{"location":"02_components/","title":"App Components","text":"What ships in every standard app, and how the pieces relate. This is the blueprint's component model \u2014 templates/app/ is a runnable instance of it.
"},{"location":"02_components/#component-model","title":"\ud83d\uddc2\ufe0f Component model","text":"%%{init: {\"flowchart\": {\"nodeSpacing\": 55, \"rankSpacing\": 120}}}%%\nflowchart TB\n subgraph EDGE[\"interfaces/ \u2014 HTTP edge\"]\n OAS[\"openapi.yaml\"]\n RU[\"resource routes\"]\n JU[\"job routes\"]\n end\n\n subgraph CORE[\"domain/ \u2014 business core\"]\n RI[\"<resource> \u00b7 models \u00b7 ports \u00b7 services\"]\n JI[\"<job/worker> \u00b7 models \u00b7 ports \u00b7 services\"]\n PI[\"pipelines/<family>\"]\n end\n\n subgraph INFRA[\"infra/ \u2014 adapters\"]\n DB[(\"MongoDB repos\")]\n CA[\"redis cache\"]\n ST[\"stores \u00b7 progress bus\"]\n LL[\"llm clients\"]\n end\n\n W[\"worker.py\"]\n CR[\"init.py \u2014 composition root\"]\n\n OAS --> RU\n OAS --> JU\n RU --> RI\n JU --> JI\n JI --> PI\n PI --> DB\n PI --> LL\n RI --> CA\n RI --> DB\n JI --> ST\n W --> JI\n W --> PI\n\n CR === RU\n CR === JU\n CR === RI\n CR === JI\n CR === PI\n CR === W\n CR === DB\n CR === CA\n CR === ST
Legend \u2014 solid arrows are direct calls; thick links are wires created by the composition root. Every component is injected, never self-constructed.
"},{"location":"02_components/#component-roles","title":"\ud83d\udce6 Component roles","text":""},{"location":"02_components/#1-interfaces-the-edge","title":"1. interfaces/ \u2014 the edge","text":"Thin handlers bound to openapi.yaml operationIds. They parse the request, call a service, and shape the response. No business rules, no I/O.
interfaces/openapi.py re-exports every route module so the OpenAPI-first app can resolve handlers. - Routes may mount the
cache decorator on read endpoints.
"},{"location":"02_components/#2-domain-the-core","title":"2. domain/ \u2014 the core","text":"Pure models, ports (abstract contracts), and services (use cases). No framework imports, no drivers.
<aggregate>/models.py \u2014 dataclasses, enums, document shapes. <aggregate>/ports/db.py \u2014 repo contract; <aggregate>/ports/service.py \u2014 service contract. - Services compose ports and enforce the business rules; the composition root injects the implementations.
"},{"location":"02_components/#3-infra-the-adapters","title":"3. infra/ \u2014 the adapters","text":"Concrete implementations of the domain ports plus cross-cutting infrastructure:
db/ \u2014 mongo-ops repositories and the connection lifecycle. adapters/ \u2014 cache store, progress bus, storages, fetchers (library-backed). - Anything else the domain needs, implemented against its ports.
"},{"location":"02_components/#4-pipelines-data-driven-families-optional","title":"4. pipelines/ \u2014 data-driven families (optional)","text":"Extraction and enrichment families, each a deterministic step chain (abc.py / impl.py / pipeline.yaml / resources/). Roots are registered in the pattern registry and driven either by a worker job or a CLI.
"},{"location":"02_components/#5-workerpy-the-job-worker-optional","title":"5. worker.py \u2014 the job worker (optional)","text":"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.
"},{"location":"02_components/#6-initpy-the-composition-root","title":"6. init.py \u2014 the composition root","text":"The only place anything is constructed: leaf repositories \u2192 dependent repositories \u2192 services \u2192 adapters. Exposes get_svc overloads and starts / stops resources on the app lifespan.
"},{"location":"02_components/#how-the-request-flows","title":"\ud83d\udd17 How the request flows","text":"sequenceDiagram\n participant C as Client\n participant R as route (interfaces)\n participant S as service (domain)\n participant P as repo (infra)\n participant DB as MongoDB\n\n C->>R: POST /items (operationId)\n R->>S: svc.create(payload)\n S->>P: repo.insert(doc)\n P->>DB: insert\n DB-->>P: ack\n P-->>S: doc\n S-->>R: result\n R-->>C: 200 JSON
"},{"location":"02_components/#related","title":"Related","text":" - 01 \u2013 Overview \u00b7 03 \u2013 Build Recipe \u00b7 04 \u2013 Testing & Quality
"},{"location":"03_build_recipe/","title":"Build Recipe","text":"The ordered, deterministic checklist for generating a new application of the standard shape. Every step is fixed; only steps 2 and 3 need app-specific authoring. Give an AI agent the copyable prompt below (or read it from the standards MCP bundle as doc://modules/build_recipe) and it can produce a complete app in one pass.
"},{"location":"03_build_recipe/#quick-start","title":"\ud83d\ude80 Quick start","text":"# Copy the runnable skeleton (from the standards docs checkout)\ncp -r <docs>/standards/src/templates/app apps/<your-app>\ncd apps/<your-app>\n
Then follow the numbered steps in the prompt.
"},{"location":"03_build_recipe/#agent-prompt-copyable","title":"\ud83d\udccb Agent prompt (copyable)","text":"# Build a new application from the Aetos Standards blueprint\n\nYou are coding a complete new backend application conforming to the Aetos\nstandard app blueprint. Execute every step in order. Only steps 2 and 3 use the\napp-specific details provided by the user; everything else is deterministic.\n\nBefore writing any code, connect to the `standards` MCP server and read:\n- `doc://nav` (the module list)\n- `doc://modules/layering_rules` (the dependency rules)\n- `doc://modules/genericity_matrix` (what is generic vs app-specific)\nThen call `find_library` with capabilities like `db`, `di`, `routing`,\n`ingestion`, `auth`, `docs`, `pipeline`, and note the libraries that already\nexist. Never reimplement one.\n\n1. **Scaffold** \u2014 copy `src/templates/app/` into `apps/<your-app>/`. Keep the\n package tree: `domain/`, `infra/`, `interfaces/`, `pipelines/`, `tests/`,\n `scripts/`, plus `pyproject.toml`, `openapi.yaml`, `config.py`, `init.py`,\n `main.py`, `worker.py`, `Dockerfile`, `docker-compose.yaml`, `.env.example`.\n\n2. **Author the spec (app-specific)** \u2014 write `openapi.yaml`: resources,\n schemas, `operationId`s, security, and extension metadata. This is the\n contract and drives all routing.\n\n3. **Write each CRUD slice (app-specific)** \u2014 per resource create the\n four-file slice: `models.py`, `ports/db.py`, `ports/service.py`, and\n optional `converters.py`. Follow the CRUD template.\n\n4. **Wire the composition root** \u2014 in `init.py`, build leaf repositories, then\n dependent repositories, then services; add infra adapters (db, cache,\n stores, progress bus, fetchers). Expose `get_svc` overloads.\n\n5. **Add routes** \u2014 create `interfaces/<resource>/routes.py` thin handlers\n bound to `operationId`s and re-export them in `interfaces/openapi.py`. Add\n the cache decorator to read endpoints.\n\n6. **Bootstrap app and worker** \u2014 confirm `main.py` runs the OpenAPI-first app\n with its lifespan. If the app has long-running jobs, adapt `worker.py` to\n poll the job collection, run the pipeline, and publish progress.\n\n7. **Add pipeline families (only if data-driven)** \u2014 create\n `pipelines/<family>/` with `abc.py`, `impl.py`, `pipeline.yaml`,\n `resources/`, and register the root. Add fixtures and a parity test.\n\n8. **Add infrastructure only where needed** \u2014 prefer a library-backed adapter.\n Never hand-write what an internal library already provides.\n\n9. **Tests** \u2014 integration tests with fakes at the HTTP edge; unit tests for\n domain and pipeline logic; a config-consistency test for rule-driven\n pipelines.\n\n10. **Scripts and seeds** \u2014 operator scripts under `scripts/` using\n `init()` / `get_svc()` / `shutdown()`.\n\n11. **Docs** \u2014 author GSDFC docstrings, generate `.pyi` stubs, mark the package\n `py.typed`, and build the docs.\n\n**Definition of done:**\n- `python -c \"import main\"` succeeds.\n- `pytest` passes.\n- The app serves every operation in the spec; the worker drains its queue.\n- No domain module imports a web framework.\n- No infrastructure was reimplemented that a library already provides.\n
"},{"location":"03_build_recipe/#where-the-steps-land","title":"\ud83e\udded Where the steps land","text":"The steps map onto the layers as follows:
Step Package / file Depends on 1 \u00b7 Scaffold whole templates/app/ \u2014 2 \u00b7 Spec openapi.yaml \u2014 3 \u00b7 CRUD slices domain/<resource>/ spec, CRUD template 4 \u00b7 Wiring init.py every slice 5 \u00b7 Routes interfaces/<resource>/ services, openapi route registry 6 \u00b7 Bootstrap main.py, worker.py composition root 7 \u00b7 Pipelines pipelines/<family>/ library pipeline engine 8 \u00b7 Infra infra/ domain ports 9 \u00b7 Tests tests/ app + fixtures 10 \u00b7 Scripts scripts/ composition root 11 \u00b7 Docs docstrings + docs build code, Doc Forge"},{"location":"03_build_recipe/#definition-of-done","title":"\u26a0\ufe0f Definition of done","text":" python -c \"import main\" succeeds. pytest passes. - The app serves the spec's operations; the worker drains its queue.
- No domain module imports a web framework.
- No infrastructure was reimplemented that a library already provides.
"},{"location":"03_build_recipe/#related","title":"Related","text":" - 01 \u2013 Overview \u00b7 02 \u2013 App Components \u00b7 04 \u2013 Testing & Quality
"},{"location":"04_development/01_testing/","title":"Testing & Quality Gates","text":"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.
"},{"location":"04_development/01_testing/#layout","title":"\ud83d\uddc2\ufe0f Layout","text":"tests/\n integration/ # TestClient(app) + mocked resources\n unit/ # models, converters, pipeline stages/roots\n conftest.py # fixtures\n
"},{"location":"04_development/01_testing/#integration-tests","title":"\ud83d\ude80 Integration tests","text":" - Build the app with
TestClient. - Replace
app.state.resources with fake repositories/services so no database is required. - Patch the auth introspection call (
httpx.AsyncClient.post) to return a canned identity. - Assert status codes and bodies for list/get/create/update/delete, plus
400, 404, 204, and the streaming endpoint's fallback.
client = TestClient(app)\napp.state.resources = {\"item_svc\": FakeItemService(), ...}\n\ndef test_create_item():\n r = client.post(\"/items\", json={\"name\": \"x\"})\n assert r.status_code == 200\n
"},{"location":"04_development/01_testing/#unit-tests","title":"\ud83e\uddea Unit tests","text":" - Domain models and converters: pure, no I/O.
- Pipeline stages: feed a fixture state, assert the transformed state.
- Assembled pipeline roots: fixture parity \u2014 replay a stored fixture input and compare against a golden output file.
"},{"location":"04_development/01_testing/#config-consistency-tests","title":"\ud83d\udd10 Config-consistency tests","text":"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.
"},{"location":"04_development/01_testing/#quality-gates","title":"\u2705 Quality gates","text":"A definition of done for any standard app:
python -c \"import main\" succeeds. pytest passes. - The app serves the spec's operations; the worker drains its queue.
- No domain module imports a web framework.
- No infrastructure was reimplemented that a library already provides.
"},{"location":"04_development/01_testing/#conventions","title":"\ud83d\udca1 Conventions","text":" asyncio_mode = \"auto\"; no manual event loops. - Fixtures are checked into the repo (inputs + golden outputs).
- Tests skip (not fail) when a required fixture is absent.
- Do not mock the domain; mock only the infrastructure edge.
"},{"location":"04_development/01_testing/#related","title":"Related","text":" - 02 \u2013 App Components \u00b7 03 \u2013 Build Recipe \u00b7 Overview
"}]}
\ No newline at end of file
+{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"\ud83e\udde9 Standards \u2014 the Aetos App Blueprint","text":"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.
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.
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 standards MCP bundle. Query find_library before writing any infrastructure.
"},{"location":"#key-features","title":"\ud83d\ude80 Key Features","text":" - \ud83e\udded Spec-first routing \u2014 the OpenAPI document, not decorator routers, is the source of truth (operationId lookup)
- \ud83c\udfd7\ufe0f One-way layering \u2014
interfaces \u2192 domain \u2192 infra, enforced - \ud83d\udd0c Single composition root \u2014 every dependency is wired in exactly one place
- \ud83e\udde9 Generic, not app-specific \u2014 reusable rules versus two authoring axes
- \ud83d\udcda Library-backed \u2014 DB, DI, pipelines, ingestion, auth, and docs come from internal libraries, never hand-written
- \ud83d\udea6 Explicit state machines \u2014 durable job statuses, progress, and human-in-the-loop pauses
"},{"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 Build Recipe The ordered, agentic checklist for a new app Testing & Quality Test layout and acceptance gates Reference (separate site, mirrors the MCP bundle): the enforceable rules \u2014 Layering, Composition Root, Spec-First Routing, CRUD Template, Pipeline Scaffold, Worker & Progress, Human-in-the-Loop, Infra Adapters, Genericity Matrix, Library Registry.
"},{"location":"#related-resources","title":"\ud83d\udd17 Related Resources","text":" - MCP bundle \u2014
standards server on :8009: read doc://index, doc://nav, doc://modules/{module}; call find_library(capability) - Library registry \u2014 the catalog of internal libraries behind every app (
libraries.json, served as registry://libraries) - Template \u2014 the runnable skeleton at
standards/src/templates/app/ - Copy the repo's
AGENTS.md-style build prompt from Build Recipe
\u00a9 Aetoskia Internal \u2014 standards
"},{"location":"01_overview/","title":"Overview","text":"The blueprint is a hexagonal (ports & 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.
"},{"location":"01_overview/#architecture","title":"\ud83c\udfd7\ufe0f Architecture","text":"flowchart LR\n subgraph EDGE[\"interfaces/ \u2014 HTTP edge\"]\n OAS[\"openapi.yaml<br/>operationId contract\"]\n RT[\"<resource>/routes.py<br/>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<br/>composition root\"]\n\n OAS --> RT\n RT --> SV\n SV --> PT\n SV --> RP\n PT -. depends on .- RP\n CR --- RT\n CR --- SV\n CR --- RP\n RP --> DB\n AD --- CR
The rule holds at every edge: code in an outer ring may depend on a ring closer to the core, never the reverse. domain/ never imports a web framework; only its ports/ seam reaches into infra/ to bind adapters.
Legend \u2014 solid arrows are compile-time calls; dotted arrows are the port/adapter binding, performed by the composition root at startup.
"},{"location":"01_overview/#the-one-way-dependency-rule","title":"\ud83d\udd04 The one-way dependency rule","text":"Ring May import Never imports interfaces/ domain, framework glue (glue only) business logic, infra directly domain/ its own models + its ports/ seam (which binds infra/libraries) web frameworks, interfaces/ infra/ domain ports, drivers, libraries \u2014 init.py everything (the wiring point) \u2014"},{"location":"01_overview/#core-principles","title":"\ud83e\udded Core principles","text":" - One-way dependencies.
interfaces -> domain -> infra is allowed; the reverse is forbidden. - 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.
- One composition root. Nothing constructs its own dependencies. All wiring lives in one module (
init.py) exposed as a flat resource registry. - Spec-first. The HTTP contract is declared in
openapi.yaml; route handlers are looked up by operationId. There are no decorator routers. - Reuse the libraries. Orchestration, DI, Mongo repositories, ingestion, auth, and docs come from internal libraries \u2014 see the Library Registry.
- Explicit state machines. Long-running work moves through explicit, durable statuses and can pause for human input.
"},{"location":"01_overview/#runtime-processes","title":"\u2699\ufe0f Runtime processes","text":"A standard app may run as up to three processes that share the same composition root:
Process Entry point Responsibility API main.py serve HTTP; start/stop resources via lifespan Worker worker.py poll durable jobs, run pipelines, publish progress Scripts python -m scripts.<name> one-shot operator tasks"},{"location":"01_overview/#read-next","title":"\u27a1\ufe0f Read Next","text":" - 02 \u2013 App Components \u00b7 03 \u2013 Build Recipe
"},{"location":"02_components/","title":"App Components","text":"What ships in every standard app, and how the pieces relate. This is the blueprint's component model \u2014 templates/app/ is a runnable instance of it.
"},{"location":"02_components/#component-model","title":"\ud83d\uddc2\ufe0f Component model","text":"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.
"},{"location":"02_components/#runtime-flow","title":"\ud83d\udd00 Runtime flow","text":"Who calls whom at request and job time:
%%{init: {\"flowchart\": {\"nodeSpacing\": 50, \"rankSpacing\": 80}}}%%\nflowchart TB\n subgraph REQ[\"Request path \u2014 API process\"]\n direction LR\n CL([\"Client<br/>operationId\"]) --> RT[\"interfaces/<resource>/routes.py\"]\n RT --> SVC[\"<resource>_svc\"]\n SVC --> REPO[\"<resource>_repo\"]\n end\n\n subgraph WRK[\"Background path \u2014 worker process\"]\n direction LR\n W[\"worker.py\"] --> JSVC[\"job_svc\"]\n W --> PIPE[\"pipelines/<family>\"]\n PIPE --> ISVC[\"item_svc\"]\n JSVC --> JREPO[\"job_repo\"]\n ISVC --> IREPO[\"item_repo\"]\n end\n\n subgraph INFRA[\"infra/ \u2014 adapters\"]\n MONGO[(\"MongoDB / mongo_ops\")]\n end\n\n REPO --> MONGO\n JREPO --> MONGO\n IREPO --> MONGO
Legend \u2014 solid arrows are runtime calls. Nothing is self-constructed.
"},{"location":"02_components/#composition-root-wiring","title":"\ud83e\uddf5 Composition-root wiring","text":"Who constructs what at startup:
%%{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/*<br/>factory seams\"]\n REPOS[\"repos<br/>infra/db/repos/mongo.py\"]\n SVCES[\"services<br/>CrudsServiceAdaptor\"]\n STATE[\"app.state.db_resources\"]\n HEXA[\"hexa.build\"]\n RT[\"routes.py\"]\n WK[\"worker.py\"]\n\n INIT --> SETUP\n INIT --> FACT\n FACT --> REPOS\n FACT --> SVCES\n REPOS --> SVCES\n HEXA -.-> SVCES\n INIT --> STATE\n STATE --> RT\n INIT --> WK
Legend \u2014 init.py is the only place that imports concrete adapters. The ports/ factory seams bind infra; the API reads resources from app.state.db_resources, the worker calls init() directly.
"},{"location":"02_components/#component-roles","title":"\ud83d\udce6 Component roles","text":""},{"location":"02_components/#1-interfaces-the-edge","title":"1. interfaces/ \u2014 the edge","text":"Thin handlers bound to openapi.yaml operationIds. They parse the request, call a service, and shape the response. No business rules, no I/O.
interfaces/openapi.py re-exports every route module so the OpenAPI-first app can resolve handlers. - Routes may mount the
cache decorator on read endpoints.
"},{"location":"02_components/#2-domain-the-core","title":"2. domain/ \u2014 the core","text":"Models, ports, and services (use cases). No framework imports. The ports/ package is the one seam allowed to reach into infra/: each port module declares the document shape and a factory that binds the adapter.
<aggregate>/models.py \u2014 dataclasses, enums, document shapes. <aggregate>/ports/db.py \u2014 document model + create_<aggregate>_repo(). <aggregate>/ports/service.py \u2014 service + get_<aggregate>_service(repo). - Services compose ports and enforce the business rules; the port factories bind the implementations, and the composition root calls them.
"},{"location":"02_components/#3-infra-the-adapters","title":"3. infra/ \u2014 the adapters","text":"Concrete implementations of the domain ports plus cross-cutting infrastructure:
db/ \u2014 mongo-ops repositories and the connection lifecycle. adapters/ \u2014 cache store, progress bus, storages, fetchers (library-backed). - Anything else the domain needs, implemented against its ports.
"},{"location":"02_components/#4-pipelines-data-driven-families-optional","title":"4. pipelines/ \u2014 data-driven families (optional)","text":"Extraction and enrichment families, each a deterministic step chain (abc.py / impl.py / pipeline.yaml / resources/). Roots are registered in the pattern registry and driven either by a worker job or a CLI.
"},{"location":"02_components/#5-workerpy-the-job-worker-optional","title":"5. worker.py \u2014 the job worker (optional)","text":"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.
"},{"location":"02_components/#6-initpy-the-composition-root","title":"6. init.py \u2014 the composition root","text":"The only place anything is constructed: leaf repositories \u2192 dependent repositories \u2192 services \u2192 adapters. Exposes get_svc overloads and starts / stops resources on the app lifespan.
"},{"location":"02_components/#how-the-request-flows","title":"\ud83d\udd17 How the request flows","text":"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->>R: POST /items (operationId)\n R->>S: svc.create(payload)\n S->>P: repo.insert(doc)\n P->>DB: insert\n DB-->>P: ack\n P-->>S: doc\n S-->>R: result\n R-->>C: 200 JSON
"},{"location":"02_components/#related","title":"Related","text":" - 01 \u2013 Overview \u00b7 03 \u2013 Build Recipe \u00b7 04 \u2013 Testing & Quality
"},{"location":"03_build_recipe/","title":"Build Recipe","text":"The ordered, deterministic checklist for generating a new application of the standard shape. Every step is fixed; only steps 2 and 3 need app-specific authoring. Give an AI agent the copyable prompt below (or read it from the standards MCP bundle as doc://modules/build_recipe) and it can produce a complete app in one pass.
"},{"location":"03_build_recipe/#quick-start","title":"\ud83d\ude80 Quick start","text":"# Copy the runnable skeleton (from the standards docs checkout)\ncp -r <docs>/standards/src/templates/app apps/<your-app>\ncd apps/<your-app>\n
Then follow the numbered steps in the prompt.
"},{"location":"03_build_recipe/#agent-prompt-copyable","title":"\ud83d\udccb Agent prompt (copyable)","text":"# Build a new application from the Aetos Standards blueprint\n\nYou are coding a complete new backend application conforming to the Aetos\nstandard app blueprint. Execute every step in order. Only steps 2 and 3 use the\napp-specific details provided by the user; everything else is deterministic.\n\nBefore writing any code, connect to the `standards` MCP server and read:\n- `doc://nav` (the module list)\n- `doc://modules/layering_rules` (the dependency rules)\n- `doc://modules/genericity_matrix` (what is generic vs app-specific)\nThen call `find_library` with capabilities like `db`, `di`, `routing`,\n`ingestion`, `auth`, `docs`, `pipeline`, and note the libraries that already\nexist. Never reimplement one.\n\n1. **Scaffold** \u2014 copy `src/templates/app/` into `apps/<your-app>/`. Keep the\n package tree: `domain/`, `infra/`, `interfaces/`, `pipelines/`, `tests/`,\n `scripts/`, plus `pyproject.toml`, `openapi.yaml`, `config.py`, `init.py`,\n `main.py`, `worker.py`, `Dockerfile`, `docker-compose.yaml`, `.env.example`.\n\n2. **Author the spec (app-specific)** \u2014 write `openapi.yaml`: resources,\n schemas, `operationId`s, security, and extension metadata. This is the\n contract and drives all routing.\n\n3. **Write each CRUD slice (app-specific)** \u2014 per resource create the\n four-file slice: `models.py`, `ports/db.py`, `ports/service.py`, and\n optional `converters.py`. Follow the CRUD template.\n\n4. **Wire the composition root** \u2014 in `init.py`, build leaf repositories, then\n dependent repositories, then services; add infra adapters (db, cache,\n stores, progress bus, fetchers). Expose `get_svc` overloads.\n\n5. **Add routes** \u2014 create `interfaces/<resource>/routes.py` thin handlers\n bound to `operationId`s and re-export them in `interfaces/openapi.py`. Add\n the cache decorator to read endpoints.\n\n6. **Bootstrap app and worker** \u2014 confirm `main.py` runs the OpenAPI-first app\n with its lifespan. If the app has long-running jobs, adapt `worker.py` to\n poll the job collection, run the pipeline, and publish progress.\n\n7. **Add pipeline families (only if data-driven)** \u2014 create\n `pipelines/<family>/` with `abc.py`, `impl.py`, `pipeline.yaml`,\n `resources/`, and register the root. Add fixtures and a parity test.\n\n8. **Add infrastructure only where needed** \u2014 prefer a library-backed adapter.\n Never hand-write what an internal library already provides.\n\n9. **Tests** \u2014 integration tests with fakes at the HTTP edge; unit tests for\n domain and pipeline logic; a config-consistency test for rule-driven\n pipelines.\n\n10. **Scripts and seeds** \u2014 operator scripts under `scripts/` using\n `init()` / `get_svc()` / `shutdown()`.\n\n11. **Docs** \u2014 author GSDFC docstrings, generate `.pyi` stubs, mark the package\n `py.typed`, and build the docs.\n\n**Definition of done:**\n- `python -c \"import main\"` succeeds.\n- `pytest` passes.\n- The app serves every operation in the spec; the worker drains its queue.\n- No domain module imports a web framework.\n- No infrastructure was reimplemented that a library already provides.\n
"},{"location":"03_build_recipe/#where-the-steps-land","title":"\ud83e\udded Where the steps land","text":"The steps map onto the layers as follows:
Step Package / file Depends on 1 \u00b7 Scaffold whole templates/app/ \u2014 2 \u00b7 Spec openapi.yaml \u2014 3 \u00b7 CRUD slices domain/<resource>/ spec, CRUD template 4 \u00b7 Wiring init.py every slice 5 \u00b7 Routes interfaces/<resource>/ services, openapi route registry 6 \u00b7 Bootstrap main.py, worker.py composition root 7 \u00b7 Pipelines pipelines/<family>/ library pipeline engine 8 \u00b7 Infra infra/ domain ports 9 \u00b7 Tests tests/ app + fixtures 10 \u00b7 Scripts scripts/ composition root 11 \u00b7 Docs docstrings + docs build code, Doc Forge"},{"location":"03_build_recipe/#definition-of-done","title":"\u26a0\ufe0f Definition of done","text":" python -c \"import main\" succeeds. pytest passes. - The app serves the spec's operations; the worker drains its queue.
- No domain module imports a web framework.
- No infrastructure was reimplemented that a library already provides.
"},{"location":"03_build_recipe/#related","title":"Related","text":" - 01 \u2013 Overview \u00b7 02 \u2013 App Components \u00b7 04 \u2013 Testing & Quality
"},{"location":"04_development/01_testing/","title":"Testing & Quality Gates","text":"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.
"},{"location":"04_development/01_testing/#layout","title":"\ud83d\uddc2\ufe0f Layout","text":"tests/\n integration/ # TestClient(app) + mocked resources\n unit/ # models, converters, pipeline stages/roots\n conftest.py # fixtures\n
"},{"location":"04_development/01_testing/#integration-tests","title":"\ud83d\ude80 Integration tests","text":" - Build the app with
TestClient. - Replace
app.state.db_resources with fake repositories/services so no database is required. - Patch the auth introspection call (
httpx.AsyncClient.post) to return a canned identity. - Assert status codes and bodies for list/get/create/update/delete, plus
400, 404, 204, and the streaming endpoint's fallback.
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
"},{"location":"04_development/01_testing/#unit-tests","title":"\ud83e\uddea Unit tests","text":" - Domain models and converters: pure, no I/O.
- Pipeline stages: feed a fixture state, assert the transformed state.
- Assembled pipeline roots: fixture parity \u2014 replay a stored fixture input and compare against a golden output file.
"},{"location":"04_development/01_testing/#config-consistency-tests","title":"\ud83d\udd10 Config-consistency tests","text":"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.
"},{"location":"04_development/01_testing/#quality-gates","title":"\u2705 Quality gates","text":"A definition of done for any standard app:
python -c \"import main\" succeeds. pytest passes. - The app serves the spec's operations; the worker drains its queue.
- No domain module imports a web framework.
- No infrastructure was reimplemented that a library already provides.
"},{"location":"04_development/01_testing/#conventions","title":"\ud83d\udca1 Conventions","text":" asyncio_mode = \"auto\"; no manual event loops. - Fixtures are checked into the repo (inputs + golden outputs).
- Tests skip (not fail) when a required fixture is absent.
- Do not mock the domain; mock only the infrastructure edge.
"},{"location":"04_development/01_testing/#related","title":"Related","text":" - 02 \u2013 App Components \u00b7 03 \u2013 Build Recipe \u00b7 Overview
"}]}
\ No newline at end of file