A generic GET cache sits behind a decorator.
| @cache_response(ttl=60)
@@ -735,14 +738,16 @@ Motor calls in the domain.
invalidation.
Event / progress bus
+Tier B — template. Copy from templates/app.
A port with an in-memory default adapter (publish, get_events). Swap for a
durable broker in production (see Worker & Progress).
External clients (ingestion)
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).
+Tier A — Libraries: Omniread (formats) and Mail Intake (email).
LLM clients
+Tier B — template. Copy the pool scaffold from templates/app.
When a pipeline calls a model provider, route through a pool adaptor that
handles provider selection, retries, and client lifecycle:
1
@@ -751,11 +756,19 @@ handles provider selection, retries, and client lifecycle:
|
Providers are selected by LLM_PROVIDER and configured by environment.
Key-value store
+Tier B — template. Copy from templates/app.
For report/snapshot storage, provide a pluggable store port with a Redis
adapter and an in-memory fallback.
Rule
-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.
+
+- Tier A (libraries: DB, DI, ingestion, mail, auth, docs, pipelines) — use
+ the library; writing a private copy is a defect. Query
find_library first.
+- Tier B (cache, progress bus, LLM pool, KV store, composition shape, worker,
+ tests) — these are template code, not libraries: copy them from
+
templates/app rather than searching the registry.
+- Tier C (models, business logic, pipeline families, rules) — author them
+ yourself.
+
diff --git a/standards/lib/12_genericity_matrix/index.html b/standards/lib/12_genericity_matrix/index.html
index f867d48..26b7d2e 100644
--- a/standards/lib/12_genericity_matrix/index.html
+++ b/standards/lib/12_genericity_matrix/index.html
@@ -482,27 +482,27 @@
-
+
- Reusable as-is (do not rewrite)
+ Tier A — Library-backed (use; never reimplement)
-
+
- Template to adapt (copy, then change names)
+ Tier B — Template-provided (copy from templates/app, then adapt)
-
+
- App-specific (must be authored)
+ Tier C — App-specific (must be authored)
@@ -583,27 +583,27 @@
-
+
- Reusable as-is (do not rewrite)
+ Tier A — Library-backed (use; never reimplement)
-
+
- Template to adapt (copy, then change names)
+ Tier B — Template-provided (copy from templates/app, then adapt)
-
+
- App-specific (must be authored)
+ Tier C — App-specific (must be authored)
@@ -636,101 +636,162 @@
Genericity Matrix
Summary
-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.
+This page states precisely what is library-backed, what is
+template-provided, and what is app-specific and must be authored. Use it
+to scope a new build and to know, for any given capability, whether to call a
+library, copy template code, or write it yourself.
-Reusable as-is (do not rewrite)
+Tier A — Library-backed (use; never reimplement)
+These capabilities already exist as internal libraries. Reach them through the
+library registry MCP (find_library(capability)) and read the library's own MCP
+for its API. Writing a private copy of any of these is a defect.
-| Area |
-Artifact |
+Capability |
+Library |
+MCP |
+
+
+
+
+| Persistence: repositories, cache, populate, transactions |
+Mongo Ops (mongo_ops) |
+:8007 |
+
+
+| Dependency injection / composition |
+Hexa (hexa) |
+:8008 |
+
+
+| Deterministic multi-step transformation engine |
+DAG Pipe (dagpipe) |
+:8006 |
+
+
+| Spec-first HTTP edge, client, codegen |
+OpenAPI First (openapi_first) |
+:8004 |
+
+
+| Format-agnostic fetch + parse (HTML, PDF, CSV, XLSX) |
+Omniread (omniread) |
+:8003 |
+
+
+| Read-only email ingestion |
+Mail Intake (mail_intake) |
+:8002 |
+
+
+| JWT auth primitives (register, login, introspect) |
+Py JWT (jwtlib) |
+:8005 |
+
+
+| Documentation build (MkDocs + MCP) |
+Doc Forge (docforge) |
+:8001 |
+
+
+
+Tier B — Template-provided (copy from templates/app, then adapt)
+These are reusable internal components shipped inside the template — they are
+not independent libraries. Copy them as-is, then rename/populate only what is
+app-specific. They give every app the same wiring shape as the reference app.
+
+
+
+| Component |
+Template location |
+Action |
| Layering & dependency rule |
-the package layout and import rules |
+the package layout + import rules |
+keep |
-| Composition root |
-the init.py shape, idempotency, get_svc overloading |
+Composition root shape |
+init.py (get_svc/get_db, idempotency) |
+keep |
-| Web edge |
-OpenAPIFirstApp bootstrap, handler-name = operationId, thin handlers |
+Spec-first web edge |
+interfaces/openapi.py, thin handlers bound to operationId |
+keep |
-| CRUD slice |
-the four-file pattern (model / ports-db / ports-service / converters) |
+CRUD slice pattern |
+domain/<resource>/{models.py, ports/db.py, ports/service.py, converters.py} |
+rename + fill |
| Generic CRUD service |
-the base service adaptor |
+infra/adapters/service.py (CrudsServiceAdaptor) |
+keep |
| Repository factory |
-base / populating / cached selection |
+infra/db/repos/mongo.py (_repo_factory) |
+keep |
-| Worker |
-the poll loop, backoff, retry, status machine |
+Worker poll + status loop |
+worker.py |
+keep |
-| Progress bus |
-the bus port + in-memory adapter + streaming endpoint |
+Progress bus + streaming |
+bus port + in-memory adapter + SSE endpoint |
+keep |
| Human-in-the-loop |
-the durable pause + question document + resolve endpoint + resume map |
+durable pause + question document + resolve endpoint + resume map |
+keep |
-| Infra adaptors |
-HTTP cache decorator, KV store port, LLM pool scaffold |
+HTTP response cache |
+cache_response decorator + generation invalidation |
+keep |
-| Testing |
-integration harness with fakes, fixture-parity pattern |
+Key-value store |
+KV store port + Redis adapter + in-memory fallback |
+keep |
-| Docs |
-GSDFC + generated stubs + MCP bundle pipeline |
+LLM client pool |
+provider-select + retry pool scaffold |
+keep |
+
+
+| Pipeline scaffold |
+pipelines/shared/{ports,loader,registry}.py + pipelines/<family>/ |
+rename + fill |
+
+
+| Operator scripts |
+scripts/ (init() / get_svc() / shutdown()) |
+rename |
+
+
+| Testing harness |
+integration fakes + fixture-parity pattern |
+keep |
+
+
+| Project scaffolding |
+pyproject.toml, Dockerfile, docker-compose.yaml, .env.example, openapi.yaml |
+rename + fill |
-Template to adapt (copy, then change names)
-
-
-
-| 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 |
-
-
-
-App-specific (must be authored)
+Tier C — App-specific (must be authored)
@@ -752,7 +813,7 @@ new build.
| domain behaviour |
-| Pipeline families (if any) |
+Pipeline families |
the transformation graph |
@@ -765,6 +826,7 @@ new build.
+
The two axes
Everything app-specific reduces to two axes:
@@ -772,7 +834,8 @@ new build.
the OpenAPI contract.
Pipeline family — only if inputs require multi-step transformation.
-If you can specify those two axes, the rest of the app follows mechanically.
+If you can specify those two axes, the rest of the app follows mechanically:
+Tier A is imported, Tier B is copied, and Tier C is the only thing you author.
diff --git a/standards/lib/13_library_registry/index.html b/standards/lib/13_library_registry/index.html
index 7a3ab34..bb820b4 100644
--- a/standards/lib/13_library_registry/index.html
+++ b/standards/lib/13_library_registry/index.html
@@ -665,7 +665,7 @@ can read a library's full API by connecting to its MCP and reading
hexa |
Declarative DI / composition-root tooling |
manual wiring, hand-written factories |
-(see note) |
+:8008 |
|
| Mongo Ops |
@@ -714,7 +714,7 @@ can read a library's full API by connecting to its MCP and reading
(service) |
Central identity service (register/login/me/introspect) |
building auth endpoints yourself |
-— |
+(deferred) |
@@ -767,9 +767,12 @@ matching library, its import name, its "use instead of" guidance, and its MCP
endpoint. Query it before writing any infrastructure.
Notes
-- 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.
+- Hexa is a full library with its own MCP server on
:8008: read
+ doc://index, doc://nav, and doc://modules/{module} (e.g.
+ doc://modules/hexa.container) to discover the DI surface — build(cls, *,
+ instances, config) and the PortNode model.
+- Auth Server is deferred: it has no
pyproject.toml/importable package and
+ no MCP bundle. Do not reach for it yet; py-jwt covers token primitives.
- Library MCP servers expose documentation resources (not executable tools):
they tell an agent the available API surface so it does not invent code.
diff --git a/standards/lib/index.html b/standards/lib/index.html
index f0143f3..8780c27 100644
--- a/standards/lib/index.html
+++ b/standards/lib/index.html
@@ -516,7 +516,7 @@ patterns, and the internal library catalog that agents consult while coding.
Worker & Progress — queued work, progress reporting, and visibility.
Human-in-the-Loop — guardrails and approvals inside automations.
Infra Adapters — how infrastructure connects without leaking into domain.
-
Genericity Matrix — what is generic vs app-specific.
+
Genericity Matrix — what is library-backed, template-provided, or authored.
Library Registry — the catalog of internal libraries behind all of it.
Read the Wiki for the philosophy and rationale; these pages are the
diff --git a/standards/lib/search/search_index.json b/standards/lib/search/search_index.json
index 7fd0e5e..38c20a2 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)\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
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 library-backed, template-provided, or authored.
- 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; where no library exists, copy the Tier B scaffold from templates/app. See the Genericity Matrix for the Tier A / B / C split.
"},{"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
PopulateRule(field=\"owner\", collection=\"users\",\n nested_rules=[], max_depth=2, projection=None)\n
- Transactions use a session context manager provided by the library.
Tier A \u2014 Library (Mongo Ops). Do not write raw Motor calls in the domain; consult the registry and the library's MCP before touching the driver.
"},{"location":"09_infra_adaptors/#http-response-cache","title":"HTTP response cache","text":"
Tier B \u2014 template. Copy from templates/app; not a standalone library.
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":"
Tier B \u2014 template. Copy from templates/app.
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.
Tier A \u2014 Libraries: Omniread (formats) and Mail Intake (email).
"},{"location":"09_infra_adaptors/#llm-clients","title":"LLM clients","text":"
Tier B \u2014 template. Copy the pool scaffold from templates/app.
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":"
Tier B \u2014 template. Copy from templates/app.
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":"
- Tier A (libraries: DB, DI, ingestion, mail, auth, docs, pipelines) \u2014 use the library; writing a private copy is a defect. Query
find_library first. - Tier B (cache, progress bus, LLM pool, KV store, composition shape, worker, tests) \u2014 these are template code, not libraries: copy them from
templates/app rather than searching the registry. - Tier C (models, business logic, pipeline families, rules) \u2014 author them yourself.
"},{"location":"12_genericity_matrix/","title":"Genericity Matrix","text":""},{"location":"12_genericity_matrix/#summary","title":"Summary","text":"
This page states precisely what is library-backed, what is template-provided, and what is app-specific and must be authored. Use it to scope a new build and to know, for any given capability, whether to call a library, copy template code, or write it yourself.
"},{"location":"12_genericity_matrix/#tier-a-library-backed-use-never-reimplement","title":"Tier A \u2014 Library-backed (use; never reimplement)","text":"
These capabilities already exist as internal libraries. Reach them through the library registry MCP (find_library(capability)) and read the library's own MCP for its API. Writing a private copy of any of these is a defect.
Capability Library MCP Persistence: repositories, cache, populate, transactions Mongo Ops (
mongo_ops)
:8007 Dependency injection / composition Hexa (
hexa)
:8008 Deterministic multi-step transformation engine DAG Pipe (
dagpipe)
:8006 Spec-first HTTP edge, client, codegen OpenAPI First (
openapi_first)
:8004 Format-agnostic fetch + parse (HTML, PDF, CSV, XLSX) Omniread (
omniread)
:8003 Read-only email ingestion Mail Intake (
mail_intake)
:8002 JWT auth primitives (register, login, introspect) Py JWT (
jwtlib)
:8005 Documentation build (MkDocs + MCP) Doc Forge (
docforge)
:8001"},{"location":"12_genericity_matrix/#tier-b-template-provided-copy-from-templatesapp-then-adapt","title":"Tier B \u2014 Template-provided (copy from
templates/app, then adapt)","text":"
These are reusable internal components shipped inside the template \u2014 they are not independent libraries. Copy them as-is, then rename/populate only what is app-specific. They give every app the same wiring shape as the reference app.
Component Template location Action Layering & dependency rule the package layout + import rules keep Composition root shape
init.py (
get_svc/
get_db, idempotency) keep Spec-first web edge
interfaces/openapi.py, thin handlers bound to
operationId keep CRUD slice pattern
domain/<resource>/{models.py, ports/db.py, ports/service.py, converters.py} rename + fill Generic CRUD service
infra/adapters/service.py (
CrudsServiceAdaptor) keep Repository factory
infra/db/repos/mongo.py (
_repo_factory) keep Worker poll + status loop
worker.py keep Progress bus + streaming bus port + in-memory adapter + SSE endpoint keep Human-in-the-loop durable pause + question document + resolve endpoint + resume map keep HTTP response cache
cache_response decorator + generation invalidation keep Key-value store KV store port + Redis adapter + in-memory fallback keep LLM client pool provider-select + retry pool scaffold keep Pipeline scaffold
pipelines/shared/{ports,loader,registry}.py +
pipelines/<family>/ rename + fill Operator scripts
scripts/ (
init() /
get_svc() /
shutdown()) rename Testing harness integration fakes + fixture-parity pattern keep Project scaffolding
pyproject.toml,
Dockerfile,
docker-compose.yaml,
.env.example,
openapi.yaml rename + fill"},{"location":"12_genericity_matrix/#tier-c-app-specific-must-be-authored","title":"Tier C \u2014 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 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: Tier A is imported, Tier B is copied, and Tier C is the only thing you author.
"},{"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
:8008 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 (deferred)"},{"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 is a full library with its own MCP server on
:8008: read doc://index, doc://nav, and doc://modules/{module} (e.g. doc://modules/hexa.container) to discover the DI surface \u2014 build(cls, *, instances, config) and the PortNode model. - Auth Server is deferred: it has no
pyproject.toml/importable package and no MCP bundle. Do not reach for it yet; py-jwt covers token primitives. - 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/components.json b/standards/mcp/components.json
new file mode 100644
index 0000000..b144741
--- /dev/null
+++ b/standards/mcp/components.json
@@ -0,0 +1,214 @@
+{
+ "project": "standards",
+ "template": "templates/app",
+ "seed": {
+ "resource": "item",
+ "family": "example"
+ },
+ "components": [
+ {
+ "kind": "model",
+ "path": "domain/item/models.py",
+ "placeholder": "resource",
+ "description": "Business Pydantic model for a resource."
+ },
+ {
+ "kind": "ports-db",
+ "path": "domain/item/ports/db.py",
+ "placeholder": "resource",
+ "description": "Document model + repository factory for a resource."
+ },
+ {
+ "kind": "ports-service",
+ "path": "domain/item/ports/service.py",
+ "placeholder": "resource",
+ "description": "CRUD service wired through the DI library."
+ },
+ {
+ "kind": "converters",
+ "path": "domain/item/ports/converters.py",
+ "placeholder": "resource",
+ "description": "Optional pure converters between domain types."
+ },
+ {
+ "kind": "routes",
+ "path": "interfaces/item/routes.py",
+ "placeholder": "resource",
+ "description": "Thin HTTP handlers bound to operationIds."
+ },
+ {
+ "kind": "test-model",
+ "path": "tests/unit/test_item_models.py",
+ "placeholder": "resource",
+ "description": "Unit test for the resource model."
+ },
+ {
+ "kind": "test-routes",
+ "path": "tests/integration/test_item_routes.py",
+ "placeholder": "resource",
+ "description": "Integration test at the HTTP edge."
+ },
+ {
+ "kind": "pipeline-abc",
+ "path": "pipelines/example/abc.py",
+ "placeholder": "family",
+ "description": "Port contracts for a pipeline family."
+ },
+ {
+ "kind": "pipeline-impl",
+ "path": "pipelines/example/impl.py",
+ "placeholder": "family",
+ "description": "Concrete stages, wired root, and family registration."
+ },
+ {
+ "kind": "pipeline-yaml",
+ "path": "pipelines/example/pipeline.yaml",
+ "placeholder": "family",
+ "description": "Graph spec (nodes, edges, roots, schema)."
+ },
+ {
+ "kind": "pipeline-resources",
+ "path": "pipelines/example/resources/pipeline.yaml",
+ "placeholder": "family",
+ "description": "Runtime spec consumed by the shared loader."
+ },
+ {
+ "kind": "pipeline-pattern",
+ "path": "pipelines/example/resources/patterns/example.yaml",
+ "placeholder": "family",
+ "description": "Pattern/rule fragment merged by the loader."
+ },
+ {
+ "kind": "pipeline-ports",
+ "path": "pipelines/shared/ports.py",
+ "placeholder": "",
+ "description": "Shared pipeline ports (FinalizePort, RegistryPort)."
+ },
+ {
+ "kind": "pipeline-loader",
+ "path": "pipelines/shared/loader.py",
+ "placeholder": "",
+ "description": "Shared loader: DI wiring + pattern merge."
+ },
+ {
+ "kind": "pipeline-registry",
+ "path": "pipelines/shared/registry.py",
+ "placeholder": "",
+ "description": "Shared registry: (kind, variant, mode) -> root class."
+ },
+ {
+ "kind": "pipeline-simple",
+ "path": "pipelines/item_sync.py",
+ "placeholder": "",
+ "description": "Smallest pipeline form (plain async steps)."
+ },
+ {
+ "kind": "job-model",
+ "path": "domain/job/models.py",
+ "placeholder": "",
+ "description": "Seeded second resource model (job)."
+ },
+ {
+ "kind": "job-ports-db",
+ "path": "domain/job/ports/db.py",
+ "placeholder": "",
+ "description": "Seeded second resource repository (job)."
+ },
+ {
+ "kind": "job-ports-service",
+ "path": "domain/job/ports/service.py",
+ "placeholder": "",
+ "description": "Seeded second resource service (job)."
+ },
+ {
+ "kind": "job-routes",
+ "path": "interfaces/job/routes.py",
+ "placeholder": "",
+ "description": "Seeded second resource routes (job)."
+ },
+ {
+ "kind": "config",
+ "path": "config.py",
+ "placeholder": "",
+ "description": "Settings dataclass + singleton."
+ },
+ {
+ "kind": "init",
+ "path": "init.py",
+ "placeholder": "",
+ "description": "Composition root (init/shutdown, get_svc)."
+ },
+ {
+ "kind": "main",
+ "path": "main.py",
+ "placeholder": "",
+ "description": "Spec-first app bootstrap + lifespan."
+ },
+ {
+ "kind": "worker",
+ "path": "worker.py",
+ "placeholder": "",
+ "description": "Poll loop + job status machine."
+ },
+ {
+ "kind": "openapi",
+ "path": "openapi.yaml",
+ "placeholder": "",
+ "description": "OpenAPI contract skeleton."
+ },
+ {
+ "kind": "pyproject",
+ "path": "pyproject.toml",
+ "placeholder": "",
+ "description": "Packaging + dependency pins."
+ },
+ {
+ "kind": "dockerfile",
+ "path": "Dockerfile",
+ "placeholder": "",
+ "description": "API + worker image with private-index wiring."
+ },
+ {
+ "kind": "compose",
+ "path": "docker-compose.yaml",
+ "placeholder": "",
+ "description": "api + worker + mongo compose file."
+ },
+ {
+ "kind": "env-example",
+ "path": ".env.example",
+ "placeholder": "",
+ "description": "Environment variable template."
+ },
+ {
+ "kind": "db-setup",
+ "path": "infra/db/setup.py",
+ "placeholder": "",
+ "description": "Mongo connection lifecycle."
+ },
+ {
+ "kind": "repo",
+ "path": "infra/db/repos/mongo.py",
+ "placeholder": "",
+ "description": "Repository factory (base/populating/cached)."
+ },
+ {
+ "kind": "service-adapter",
+ "path": "infra/adapters/service.py",
+ "placeholder": "",
+ "description": "Generic CRUD service adaptor."
+ },
+ {
+ "kind": "script-seed",
+ "path": "scripts/seed.py",
+ "placeholder": "",
+ "description": "Operator seed script."
+ },
+ {
+ "kind": "script-drop",
+ "path": "scripts/drop.py",
+ "placeholder": "",
+ "description": "Operator drop script."
+ }
+ ]
+}
\ No newline at end of file
diff --git a/standards/mcp/modules/genericity_matrix.json b/standards/mcp/modules/genericity_matrix.json
index 18253f3..688e482 100644
--- a/standards/mcp/modules/genericity_matrix.json
+++ b/standards/mcp/modules/genericity_matrix.json
@@ -2,7 +2,7 @@
"module": "genericity_matrix",
"content": {
"path": "genericity_matrix",
- "docstring": "# Genericity Matrix\n\n## Summary\n\nThis page states precisely what is **reusable as-is**, what is a **template to\nadapt**, and what is **app-specific and must be authored**. Use it to scope a\nnew build.\n\n---\n\n## Reusable as-is (do not rewrite)\n\n| Area | Artifact |\n|---|---|\n| Layering & dependency rule | the package layout and import rules |\n| Composition root | the `init.py` shape, idempotency, `get_svc` overloading |\n| Web edge | `OpenAPIFirstApp` bootstrap, handler-name = `operationId`, thin handlers |\n| CRUD slice | the four-file pattern (model / ports-db / ports-service / converters) |\n| Generic CRUD service | the base service adaptor |\n| Repository factory | base / populating / cached selection |\n| Worker | the poll loop, backoff, retry, status machine |\n| Progress bus | the bus port + in-memory adapter + streaming endpoint |\n| Human-in-the-loop | the durable pause + question document + resolve endpoint + resume map |\n| Infra adaptors | HTTP cache decorator, KV store port, LLM pool scaffold |\n| Testing | integration harness with fakes, fixture-parity pattern |\n| Docs | GSDFC + generated stubs + MCP bundle pipeline |\n\n## Template to adapt (copy, then change names)\n\n| Artifact | Change |\n|---|---|\n| `pyproject.toml` | project name, dependencies |\n| `Dockerfile` / `docker-compose.yaml` | service name, ports, services |\n| `.env.example` | connection strings, provider keys |\n| `openapi.yaml` | every path/schema/operationId |\n| `scripts/` | seed and operator tasks |\n\n## App-specific (must be authored)\n\n| Artifact | Why |\n|---|---|\n| Resource models | the business shape |\n| Repository populate/cache rules | the relationships |\n| Service business logic | domain behaviour |\n| Pipeline families (if any) | the transformation graph |\n| Pattern/rule resources | data-driven policy |\n| Fixtures & golden outputs | test data |\n\n## The two axes\n\nEverything app-specific reduces to **two axes**:\n\n1. **Resource model** — the set of resources, their fields, relationships, and\n the OpenAPI contract.\n2. **Pipeline family** — only if inputs require multi-step transformation.\n\nIf you can specify those two axes, the rest of the app follows mechanically.\n",
+ "docstring": "# Genericity Matrix\n\n## Summary\n\nThis page states precisely what is **library-backed**, what is\n**template-provided**, and what is **app-specific and must be authored**. Use it\nto scope a new build and to know, for any given capability, whether to call a\nlibrary, copy template code, or write it yourself.\n\n---\n\n## Tier A — Library-backed (use; never reimplement)\n\nThese capabilities already exist as internal libraries. Reach them through the\nlibrary registry MCP (`find_library(capability)`) and read the library's own MCP\nfor its API. Writing a private copy of any of these is a defect.\n\n| Capability | Library | MCP |\n|---|---|---|\n| Persistence: repositories, cache, populate, transactions | Mongo Ops (`mongo_ops`) | `:8007` |\n| Dependency injection / composition | Hexa (`hexa`) | `:8008` |\n| Deterministic multi-step transformation engine | DAG Pipe (`dagpipe`) | `:8006` |\n| Spec-first HTTP edge, client, codegen | OpenAPI First (`openapi_first`) | `:8004` |\n| Format-agnostic fetch + parse (HTML, PDF, CSV, XLSX) | Omniread (`omniread`) | `:8003` |\n| Read-only email ingestion | Mail Intake (`mail_intake`) | `:8002` |\n| JWT auth primitives (register, login, introspect) | Py JWT (`jwtlib`) | `:8005` |\n| Documentation build (MkDocs + MCP) | Doc Forge (`docforge`) | `:8001` |\n\n## Tier B — Template-provided (copy from `templates/app`, then adapt)\n\nThese are reusable internal components shipped **inside** the template — they are\nnot independent libraries. Copy them as-is, then rename/populate only what is\napp-specific. They give every app the same wiring shape as the reference app.\n\n| Component | Template location | Action |\n|---|---|---|\n| Layering & dependency rule | the package layout + import rules | keep |\n| Composition root shape | `init.py` (`get_svc`/`get_db`, idempotency) | keep |\n| Spec-first web edge | `interfaces/openapi.py`, thin handlers bound to `operationId` | keep |\n| CRUD slice pattern | `domain/
/{models.py, ports/db.py, ports/service.py, converters.py}` | rename + fill |\n| Generic CRUD service | `infra/adapters/service.py` (`CrudsServiceAdaptor`) | keep |\n| Repository factory | `infra/db/repos/mongo.py` (`_repo_factory`) | keep |\n| Worker poll + status loop | `worker.py` | keep |\n| Progress bus + streaming | bus port + in-memory adapter + SSE endpoint | keep |\n| Human-in-the-loop | durable pause + question document + resolve endpoint + resume map | keep |\n| HTTP response cache | `cache_response` decorator + generation invalidation | keep |\n| Key-value store | KV store port + Redis adapter + in-memory fallback | keep |\n| LLM client pool | provider-select + retry pool scaffold | keep |\n| Pipeline scaffold | `pipelines/shared/{ports,loader,registry}.py` + `pipelines//` | rename + fill |\n| Operator scripts | `scripts/` (`init()` / `get_svc()` / `shutdown()`) | rename |\n| Testing harness | integration fakes + fixture-parity pattern | keep |\n| Project scaffolding | `pyproject.toml`, `Dockerfile`, `docker-compose.yaml`, `.env.example`, `openapi.yaml` | rename + fill |\n\n## Tier C — App-specific (must be authored)\n\n| Artifact | Why |\n|---|---|\n| Resource models | the business shape |\n| Repository populate/cache rules | the relationships |\n| Service business logic | domain behaviour |\n| Pipeline families | the transformation graph |\n| Pattern/rule resources | data-driven policy |\n| Fixtures & golden outputs | test data |\n\n---\n\n## The two axes\n\nEverything app-specific reduces to **two axes**:\n\n1. **Resource model** — the set of resources, their fields, relationships, and\n the OpenAPI contract.\n2. **Pipeline family** — only if inputs require multi-step transformation.\n\nIf you can specify those two axes, the rest of the app follows mechanically:\nTier A is imported, Tier B is copied, and Tier C is the only thing you author.\n",
"objects": {}
}
}
\ No newline at end of file
diff --git a/standards/mcp/modules/index.json b/standards/mcp/modules/index.json
index 6ea50b1..625805d 100644
--- a/standards/mcp/modules/index.json
+++ b/standards/mcp/modules/index.json
@@ -2,7 +2,7 @@
"module": "index",
"content": {
"path": "index",
- "docstring": "# Standards Reference\n\nThe reference half of the Standards description: the enforceable rules,\npatterns, and the internal library catalog that agents consult while coding.\n\n- **Layering Rules** — the strict request → domain → repository layering and\n the rules that enforce it.\n- **Composition Root** — a single composition point wiring the whole app.\n- **Spec-First Routing** — the OpenAPI contract as the routing source of truth.\n- **CRUD Template** — the standardized generic resource endpoint scaffold.\n- **Pipeline Scaffold** — the deterministic pipeline skeleton for multi-stage\n jobs.\n- **Worker & Progress** — queued work, progress reporting, and visibility.\n- **Human-in-the-Loop** — guardrails and approvals inside automations.\n- **Infra Adapters** — how infrastructure connects without leaking into domain.\n- **Genericity Matrix** — what is generic vs app-specific.\n- **Library Registry** — the catalog of internal libraries behind all of it.\n\nRead the **Wiki** for the philosophy and rationale; these pages are the\nreference that mirrors the `standards` MCP bundle.\n",
+ "docstring": "# Standards Reference\n\nThe reference half of the Standards description: the enforceable rules,\npatterns, and the internal library catalog that agents consult while coding.\n\n- **Layering Rules** — the strict request → domain → repository layering and\n the rules that enforce it.\n- **Composition Root** — a single composition point wiring the whole app.\n- **Spec-First Routing** — the OpenAPI contract as the routing source of truth.\n- **CRUD Template** — the standardized generic resource endpoint scaffold.\n- **Pipeline Scaffold** — the deterministic pipeline skeleton for multi-stage\n jobs.\n- **Worker & Progress** — queued work, progress reporting, and visibility.\n- **Human-in-the-Loop** — guardrails and approvals inside automations.\n- **Infra Adapters** — how infrastructure connects without leaking into domain.\n- **Genericity Matrix** — what is library-backed, template-provided, or authored.\n- **Library Registry** — the catalog of internal libraries behind all of it.\n\nRead the **Wiki** for the philosophy and rationale; these pages are the\nreference that mirrors the `standards` MCP bundle.\n",
"objects": {}
}
}
\ No newline at end of file
diff --git a/standards/mcp/modules/infra_adaptors.json b/standards/mcp/modules/infra_adaptors.json
index bb00edb..fb3833a 100644
--- a/standards/mcp/modules/infra_adaptors.json
+++ b/standards/mcp/modules/infra_adaptors.json
@@ -2,7 +2,7 @@
"module": "infra_adaptors",
"content": {
"path": "infra_adaptors",
- "docstring": "# Infra Adapters\n\n## Summary\n\nAll external systems are reached through adapters in `infra/`. Prefer an\nexisting internal library over writing a new adapter.\n\n---\n\n## Database (MongoDB)\n\n- A single connection manager owns the client lifecycle.\n `setup_database(uri, db_name)` / `shutdown_database()`.\n- A repository **factory** selects the repository flavour:\n\n ```python\n repo_factory(collection, document, engine=None,\n populate_rules=None, cache=None)\n ```\n\n - base repository — plain CRUD;\n - populating repository — resolves referenced documents (foreign keys);\n - cached repository — an LRU/TTL cache in front of reads.\n\n- Referenced documents are described declaratively:\n\n ```python\n PopulateRule(field=\"owner\", collection=\"users\",\n nested_rules=[], max_depth=2, projection=None)\n ```\n\n- Transactions use a session context manager provided by the library.\n\n**Library:** the Mongo operations library (see registry). Do not write raw\nMotor calls in the domain.\n\n## HTTP response cache\n\nA generic GET cache sits behind a decorator.\n\n```python\n@cache_response(ttl=60)\nasync def list_items(request): ...\n```\n\n- Keyed on path + query + user.\n- No-op when no store is wired.\n- `invalidate(*resources)` bumps a generation counter for write-triggered\n invalidation.\n\n## Event / progress bus\n\nA port with an in-memory default adapter (`publish`, `get_events`). Swap for a\ndurable broker in production (see Worker & Progress).\n\n## External clients (ingestion)\n\nExternal fetchers (files, mail, HTTP) are adapters that wrap ingestion\nlibraries. Construct them in the composition root and inject into\norchestrators.\n\n**Libraries:** the ingestion library (formats) and the mail library (email).\n\n## LLM clients\n\nWhen a pipeline calls a model provider, route through a pool adaptor that\nhandles provider selection, retries, and client lifecycle:\n\n```python\npool = setup_async_pool(provider=settings.llm_provider)\nresult = await pool.complete(model=\"...\", messages=[...])\n```\n\nProviders are selected by `LLM_PROVIDER` and configured by environment.\n\n## Key-value store\n\nFor report/snapshot storage, provide a pluggable store port with a Redis\nadapter and an in-memory fallback.\n\n## Rule\n\nIf a capability already exists as a library (DB, DI, ingestion, mail, auth,\ndocs), **use it**. Writing a private copy of a library is a defect.\n",
+ "docstring": "# Infra Adapters\n\n## Summary\n\nAll external systems are reached through adapters in `infra/`. Prefer an\nexisting internal library over writing a new adapter; where no library exists,\ncopy the Tier B scaffold from `templates/app`. See the Genericity Matrix for the\nTier A / B / C split.\n\n---\n\n## Database (MongoDB)\n\n- A single connection manager owns the client lifecycle.\n `setup_database(uri, db_name)` / `shutdown_database()`.\n- A repository **factory** selects the repository flavour:\n\n ```python\n repo_factory(collection, document, engine=None,\n populate_rules=None, cache=None)\n ```\n\n - base repository — plain CRUD;\n - populating repository — resolves referenced documents (foreign keys);\n - cached repository — an LRU/TTL cache in front of reads.\n\n- Referenced documents are described declaratively:\n\n ```python\n PopulateRule(field=\"owner\", collection=\"users\",\n nested_rules=[], max_depth=2, projection=None)\n ```\n\n- Transactions use a session context manager provided by the library.\n\n**Tier A — Library (Mongo Ops).** Do not write raw Motor calls in the domain;\nconsult the registry and the library's MCP before touching the driver.\n\n## HTTP response cache\n\n**Tier B — template.** Copy from `templates/app`; not a standalone library.\n\nA generic GET cache sits behind a decorator.\n\n```python\n@cache_response(ttl=60)\nasync def list_items(request): ...\n```\n\n- Keyed on path + query + user.\n- No-op when no store is wired.\n- `invalidate(*resources)` bumps a generation counter for write-triggered\n invalidation.\n\n## Event / progress bus\n\n**Tier B — template.** Copy from `templates/app`.\n\nA port with an in-memory default adapter (`publish`, `get_events`). Swap for a\ndurable broker in production (see Worker & Progress).\n\n## External clients (ingestion)\n\nExternal fetchers (files, mail, HTTP) are adapters that wrap ingestion\nlibraries. Construct them in the composition root and inject into\norchestrators.\n\n**Tier A — Libraries:** Omniread (formats) and Mail Intake (email).\n\n## LLM clients\n\n**Tier B — template.** Copy the pool scaffold from `templates/app`.\n\nWhen a pipeline calls a model provider, route through a pool adaptor that\nhandles provider selection, retries, and client lifecycle:\n\n```python\npool = setup_async_pool(provider=settings.llm_provider)\nresult = await pool.complete(model=\"...\", messages=[...])\n```\n\nProviders are selected by `LLM_PROVIDER` and configured by environment.\n\n## Key-value store\n\n**Tier B — template.** Copy from `templates/app`.\n\nFor report/snapshot storage, provide a pluggable store port with a Redis\nadapter and an in-memory fallback.\n\n## Rule\n\n- **Tier A (libraries: DB, DI, ingestion, mail, auth, docs, pipelines)** — use\n the library; writing a private copy is a defect. Query `find_library` first.\n- **Tier B (cache, progress bus, LLM pool, KV store, composition shape, worker,\n tests)** — these are template code, not libraries: copy them from\n `templates/app` rather than searching the registry.\n- **Tier C (models, business logic, pipeline families, rules)** — author them\n yourself.\n",
"objects": {}
}
}
\ No newline at end of file
diff --git a/standards/mcp/modules/library_registry.json b/standards/mcp/modules/library_registry.json
index bee7171..91684d2 100644
--- a/standards/mcp/modules/library_registry.json
+++ b/standards/mcp/modules/library_registry.json
@@ -2,7 +2,7 @@
"module": "library_registry",
"content": {
"path": "library_registry",
- "docstring": "# Library Registry\n\n## Summary\n\nThe Aetos ecosystem provides internal libraries for the shared concerns of\nevery application. **Use them.** Writing a private copy of any capability below\nis a defect.\n\nEach library also exposes its own documentation as an MCP server. An AI agent\ncan read a library's full API by connecting to its MCP and reading\n`doc://index`, `doc://nav`, and `doc://modules/{module}`.\n\n---\n\n## Catalog\n\n| Library | Import | Purpose | Use instead of | MCP |\n|---|---|---|---|---|\n| DAG Pipe | `dagpipe` | Deterministic state-transformation DAG engine | hand-rolled orchestration/topological ordering | `:8006` |\n| Hexa | `hexa` | Declarative DI / composition-root tooling | manual wiring, hand-written factories | *(see note)* |\n| Mongo Ops | `mongo_ops` | Async MongoDB repositories, cache, populate, transactions | raw Motor CRUD and connection lifecycle | `:8007` |\n| Omniread | `omniread` | Format-agnostic fetch + parse (HTML, PDF, CSV, XLSX) | custom scrapers/parsers | `:8003` |\n| OpenAPI First | `openapi_first` | Spec-first FastAPI bootstrap, client, codegen | decorator routers and hand-written clients | `:8004` |\n| Mail Intake | `mail_intake` | Provider-agnostic read-only email ingestion | Gmail OAuth + MIME parsing | `:8002` |\n| Py JWT | `jwtlib` | JWT auth primitives (register/login/introspect) | password hashing + token code | `:8005` |\n| Doc Forge | `docforge` | Docstring compiler → MkDocs + MCP docs | custom doc generators | `:8001` |\n| Auth Server | *(service)* | Central identity service (register/login/me/introspect) | building auth endpoints yourself | — |\n\n## How to choose\n\n| You need to… | Use |\n|---|---|\n| persist and query documents | Mongo Ops |\n| compose dependencies | Hexa |\n| run a multi-step transformation | DAG Pipe |\n| read/parse an arbitrary input format | Omniread |\n| expose a contract-first HTTP API | OpenAPI First |\n| ingest email | Mail Intake |\n| authenticate users / verify tokens | Py JWT (+ Auth Server) |\n| generate documentation | Doc Forge |\n\n## Discovery\n\nA single **standards MCP** exposes `find_library(capability)`, which returns the\nmatching library, its import name, its \"use instead of\" guidance, and its MCP\nendpoint. Query it before writing any infrastructure.\n\n## Notes\n\n- **Hexa** ships an MCP bundle but is not yet assigned a dedicated port in the\n hub config; it is reachable via the standards registry entry and will be\n wired to a port.\n- Library MCP servers expose **documentation resources** (not executable tools):\n they tell an agent the available API surface so it does not invent code.\n",
+ "docstring": "# Library Registry\n\n## Summary\n\nThe Aetos ecosystem provides internal libraries for the shared concerns of\nevery application. **Use them.** Writing a private copy of any capability below\nis a defect.\n\nEach library also exposes its own documentation as an MCP server. An AI agent\ncan read a library's full API by connecting to its MCP and reading\n`doc://index`, `doc://nav`, and `doc://modules/{module}`.\n\n---\n\n## Catalog\n\n| Library | Import | Purpose | Use instead of | MCP |\n|---|---|---|---|---|\n| DAG Pipe | `dagpipe` | Deterministic state-transformation DAG engine | hand-rolled orchestration/topological ordering | `:8006` |\n| Hexa | `hexa` | Declarative DI / composition-root tooling | manual wiring, hand-written factories | `:8008` |\n| Mongo Ops | `mongo_ops` | Async MongoDB repositories, cache, populate, transactions | raw Motor CRUD and connection lifecycle | `:8007` |\n| Omniread | `omniread` | Format-agnostic fetch + parse (HTML, PDF, CSV, XLSX) | custom scrapers/parsers | `:8003` |\n| OpenAPI First | `openapi_first` | Spec-first FastAPI bootstrap, client, codegen | decorator routers and hand-written clients | `:8004` |\n| Mail Intake | `mail_intake` | Provider-agnostic read-only email ingestion | Gmail OAuth + MIME parsing | `:8002` |\n| Py JWT | `jwtlib` | JWT auth primitives (register/login/introspect) | password hashing + token code | `:8005` |\n| Doc Forge | `docforge` | Docstring compiler → MkDocs + MCP docs | custom doc generators | `:8001` |\n| Auth Server | *(service)* | Central identity service (register/login/me/introspect) | building auth endpoints yourself | *(deferred)* |\n\n## How to choose\n\n| You need to… | Use |\n|---|---|\n| persist and query documents | Mongo Ops |\n| compose dependencies | Hexa |\n| run a multi-step transformation | DAG Pipe |\n| read/parse an arbitrary input format | Omniread |\n| expose a contract-first HTTP API | OpenAPI First |\n| ingest email | Mail Intake |\n| authenticate users / verify tokens | Py JWT (+ Auth Server) |\n| generate documentation | Doc Forge |\n\n## Discovery\n\nA single **standards MCP** exposes `find_library(capability)`, which returns the\nmatching library, its import name, its \"use instead of\" guidance, and its MCP\nendpoint. Query it before writing any infrastructure.\n\n## Notes\n\n- **Hexa** is a full library with its own MCP server on `:8008`: read\n `doc://index`, `doc://nav`, and `doc://modules/{module}` (e.g.\n `doc://modules/hexa.container`) to discover the DI surface — `build(cls, *,\n instances, config)` and the `PortNode` model.\n- **Auth Server** is deferred: it has no `pyproject.toml`/importable package and\n no MCP bundle. Do not reach for it yet; `py-jwt` covers token primitives.\n- Library MCP servers expose **documentation resources** (not executable tools):\n they tell an agent the available API surface so it does not invent code.\n",
"objects": {}
}
}
\ No newline at end of file
diff --git a/standards/mcp/templates/app/.dockerignore b/standards/mcp/templates/app/.dockerignore
new file mode 100644
index 0000000..7143c6c
--- /dev/null
+++ b/standards/mcp/templates/app/.dockerignore
@@ -0,0 +1,8 @@
+.venv/
+__pycache__/
+*.pyc
+.env
+.git/
+docs/
+dist/
+build/
\ No newline at end of file
diff --git a/standards/mcp/templates/app/.env.example b/standards/mcp/templates/app/.env.example
new file mode 100644
index 0000000..3c7d11b
--- /dev/null
+++ b/standards/mcp/templates/app/.env.example
@@ -0,0 +1,5 @@
+DEBUG=false
+MONGO_URI=mongodb://localhost:27017
+MONGO_DB=app_template
+WORKER_POLL_SECONDS=30
+WORKER_MAX_RETRIES=3
\ No newline at end of file
diff --git a/standards/mcp/templates/app/Dockerfile b/standards/mcp/templates/app/Dockerfile
new file mode 100644
index 0000000..b39d077
--- /dev/null
+++ b/standards/mcp/templates/app/Dockerfile
@@ -0,0 +1,20 @@
+FROM python:3.11-slim
+
+WORKDIR /app
+
+# Private index for the internal Aetos libraries — pass credentials as build args.
+ARG PIP_REGISTRY_USERNAME
+ARG PIP_REGISTRY_PASSWORD
+ENV PIP_EXTRA_INDEX_URL=https://${PIP_REGISTRY_USERNAME}:${PIP_REGISTRY_PASSWORD}@pip.aetoskia.com/simple
+
+COPY pyproject.toml ./
+COPY . .
+RUN pip install --no-cache-dir --upgrade pip \
+ && pip install --no-cache-dir --extra-index-url "${PIP_EXTRA_INDEX_URL}" .
+
+EXPOSE 8000
+
+# Two processes, per the two axes:
+# - main.py : the spec-first FastAPI application
+# - worker.py : the pipeline consumer polling pending jobs
+CMD ["sh", "-c", "uvicorn main:app --host 0.0.0.0 --port 8000 & exec python worker.py"]
\ No newline at end of file
diff --git a/standards/mcp/templates/app/config.py b/standards/mcp/templates/app/config.py
new file mode 100644
index 0000000..a163e38
--- /dev/null
+++ b/standards/mcp/templates/app/config.py
@@ -0,0 +1,36 @@
+"""# Summary
+
+App configuration loaded from environment — a Settings dataclass with defaults
+plus a singleton settings instance populated from OS environment variables at
+import time.
+"""
+import os
+from dataclasses import dataclass
+
+
+@dataclass
+class Settings:
+ """Runtime configuration settings for the application.
+
+ Attributes:
+ debug (bool): Enable debug behavior. Defaults to False.
+ mongo_uri (str): MongoDB connection URI. Defaults to mongodb://localhost:27017.
+ mongo_db (str): MongoDB database name. Defaults to app_template.
+ worker_poll_seconds (int): Worker poll interval in seconds. Defaults to 30.
+ worker_max_retries (int): Maximum job retries before skipping. Defaults to 3.
+ """
+
+ debug: bool = False
+ mongo_uri: str = "mongodb://localhost:27017"
+ mongo_db: str = "app_template"
+ worker_poll_seconds: int = 30
+ worker_max_retries: int = 3
+
+
+settings = Settings(
+ debug=os.environ.get("DEBUG", "").lower() in ("1", "true", "yes"),
+ mongo_uri=os.environ.get("MONGO_URI", "mongodb://localhost:27017"),
+ mongo_db=os.environ.get("MONGO_DB", "app_template"),
+ worker_poll_seconds=int(os.environ.get("WORKER_POLL_SECONDS", "30")),
+ worker_max_retries=int(os.environ.get("WORKER_MAX_RETRIES", "3")),
+)
\ No newline at end of file
diff --git a/standards/mcp/templates/app/docker-compose.yaml b/standards/mcp/templates/app/docker-compose.yaml
new file mode 100644
index 0000000..d011d83
--- /dev/null
+++ b/standards/mcp/templates/app/docker-compose.yaml
@@ -0,0 +1,21 @@
+services:
+ api:
+ build: .
+ command: uvicorn main:app --host 0.0.0.0 --port 8000
+ env_file: .env
+ ports:
+ - "8000:8000"
+ depends_on:
+ - mongo
+
+ worker:
+ build: .
+ command: python worker.py
+ env_file: .env
+ depends_on:
+ - mongo
+
+ mongo:
+ image: mongo:7
+ ports:
+ - "27017:27017"
\ No newline at end of file
diff --git a/standards/mcp/templates/app/domain/__init__.py b/standards/mcp/templates/app/domain/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/standards/mcp/templates/app/domain/item/__init__.py b/standards/mcp/templates/app/domain/item/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/standards/mcp/templates/app/domain/item/models.py b/standards/mcp/templates/app/domain/item/models.py
new file mode 100644
index 0000000..63c3875
--- /dev/null
+++ b/standards/mcp/templates/app/domain/item/models.py
@@ -0,0 +1,19 @@
+"""# Summary
+
+Item domain model — the neutral resource used throughout the template.
+"""
+from pydantic import BaseModel, Field
+
+
+class Item(BaseModel):
+ """A single countable resource.
+
+ Attributes:
+ name (str): Item name.
+ quantity (int): Item quantity. Defaults to 1.
+ """
+
+ name: str
+ quantity: int = Field(default=1)
+
+ model_config = {"extra": "ignore"}
\ No newline at end of file
diff --git a/standards/mcp/templates/app/domain/item/ports/__init__.py b/standards/mcp/templates/app/domain/item/ports/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/standards/mcp/templates/app/domain/item/ports/converters.py b/standards/mcp/templates/app/domain/item/ports/converters.py
new file mode 100644
index 0000000..f386df4
--- /dev/null
+++ b/standards/mcp/templates/app/domain/item/ports/converters.py
@@ -0,0 +1,18 @@
+"""# Summary
+
+Optional converters for the item slice — pure functions mapping between domain
+types. Add a converter here when a service must reshape data; keep it pure.
+"""
+from domain.item.models import Item
+
+
+def to_item(data: dict) -> Item:
+ """Build an :class:`Item` from a raw mapping.
+
+ Args:
+ data (dict): Field mapping matching the item model.
+
+ Returns:
+ Item: The constructed item model.
+ """
+ return Item(**data)
diff --git a/standards/mcp/templates/app/domain/item/ports/db.py b/standards/mcp/templates/app/domain/item/ports/db.py
new file mode 100644
index 0000000..d1dbbb5
--- /dev/null
+++ b/standards/mcp/templates/app/domain/item/ports/db.py
@@ -0,0 +1,20 @@
+"""# Summary
+
+Mongo persistence for items: document schema and repository factory.
+"""
+from domain.item.models import Item
+from infra.db.repos.mongo import _repo_factory as mongo_repo_factory
+from mongo_ops import BaseDocument, BaseRepository
+
+
+class ItemDocument(Item, BaseDocument):
+ """Mongo document form of :class:`Item`."""
+
+
+def create_item_repo() -> BaseRepository:
+ """Create the Mongo-backed item repository.
+
+ Returns:
+ BaseRepository: Repository wired to the ``items`` collection.
+ """
+ return mongo_repo_factory("items", ItemDocument)
\ No newline at end of file
diff --git a/standards/mcp/templates/app/domain/item/ports/service.py b/standards/mcp/templates/app/domain/item/ports/service.py
new file mode 100644
index 0000000..59daba7
--- /dev/null
+++ b/standards/mcp/templates/app/domain/item/ports/service.py
@@ -0,0 +1,33 @@
+"""# Summary
+
+CRUD service wiring for the item aggregate via the hexa container.
+"""
+from hexa import build
+
+from domain.item.models import Item
+from domain.item.ports.db import ItemDocument
+from infra.adapters.service import CrudsServiceAdaptor
+
+
+class ItemCrudsService(CrudsServiceAdaptor[Item, ItemDocument]):
+ """Item CRUD service resolving :class:`ItemDocument` from a repo.
+
+ Notes:
+ Instances are created by :func:`get_item_service`. The ``model`` and
+ ``schema`` class attributes bind the domain model and its document type.
+ """
+
+ model = Item
+ schema = ItemDocument
+
+
+def get_item_service(repo) -> ItemCrudsService:
+ """Build an item CRUD service wired to ``repo``.
+
+ Args:
+ repo (Any): Mongo-backed item repository from :func:`create_item_repo`.
+
+ Returns:
+ ItemCrudsService: Ready-to-use item CRUD service.
+ """
+ return build(ItemCrudsService, instances={"repo": repo})
\ No newline at end of file
diff --git a/standards/mcp/templates/app/domain/job/__init__.py b/standards/mcp/templates/app/domain/job/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/standards/mcp/templates/app/domain/job/models.py b/standards/mcp/templates/app/domain/job/models.py
new file mode 100644
index 0000000..17ec51e
--- /dev/null
+++ b/standards/mcp/templates/app/domain/job/models.py
@@ -0,0 +1,26 @@
+"""# Summary
+
+Job domain model — a pipeline work item consumed by the background worker.
+"""
+from typing import Literal
+from pydantic import BaseModel, Field
+
+
+class Job(BaseModel):
+ """A unit of background work.
+
+ Attributes:
+ name (str): Job name.
+ status (str): Lifecycle status. Defaults to "pending".
+ retry_count (int): Times the job failed. Defaults to 0.
+ error_message (str | None): Last failure message. Defaults to None.
+ payload (dict | None): Arbitrary job payload. Defaults to None.
+ """
+
+ name: str
+ status: Literal["pending", "processing", "done", "failed"] = "pending"
+ retry_count: int = Field(default=0)
+ error_message: str | None = None
+ payload: dict | None = None
+
+ model_config = {"extra": "ignore"}
\ No newline at end of file
diff --git a/standards/mcp/templates/app/domain/job/ports/__init__.py b/standards/mcp/templates/app/domain/job/ports/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/standards/mcp/templates/app/domain/job/ports/db.py b/standards/mcp/templates/app/domain/job/ports/db.py
new file mode 100644
index 0000000..547e2b5
--- /dev/null
+++ b/standards/mcp/templates/app/domain/job/ports/db.py
@@ -0,0 +1,20 @@
+"""# Summary
+
+Mongo persistence for jobs: document schema and repository factory.
+"""
+from domain.job.models import Job
+from infra.db.repos.mongo import _repo_factory as mongo_repo_factory
+from mongo_ops import BaseDocument, BaseRepository
+
+
+class JobDocument(Job, BaseDocument):
+ """Mongo document form of :class:`Job`."""
+
+
+def create_job_repo() -> BaseRepository:
+ """Create the Mongo-backed job repository.
+
+ Returns:
+ BaseRepository: Repository wired to the ``jobs`` collection.
+ """
+ return mongo_repo_factory("jobs", JobDocument)
\ No newline at end of file
diff --git a/standards/mcp/templates/app/domain/job/ports/service.py b/standards/mcp/templates/app/domain/job/ports/service.py
new file mode 100644
index 0000000..9f6b714
--- /dev/null
+++ b/standards/mcp/templates/app/domain/job/ports/service.py
@@ -0,0 +1,33 @@
+"""# Summary
+
+CRUD service wiring for the job aggregate via the hexa container.
+"""
+from hexa import build
+
+from domain.job.models import Job
+from domain.job.ports.db import JobDocument
+from infra.adapters.service import CrudsServiceAdaptor
+
+
+class JobCrudsService(CrudsServiceAdaptor[Job, JobDocument]):
+ """Job CRUD service resolving :class:`JobDocument` from a repo.
+
+ Notes:
+ Instances are created by :func:`get_job_service`. The ``model`` and
+ ``schema`` class attributes bind the domain model and its document type.
+ """
+
+ model = Job
+ schema = JobDocument
+
+
+def get_job_service(repo) -> JobCrudsService:
+ """Build a job CRUD service wired to ``repo``.
+
+ Args:
+ repo (Any): Mongo-backed job repository from :func:`create_job_repo`.
+
+ Returns:
+ JobCrudsService: Ready-to-use job CRUD service.
+ """
+ return build(JobCrudsService, instances={"repo": repo})
\ No newline at end of file
diff --git a/standards/mcp/templates/app/infra/__init__.py b/standards/mcp/templates/app/infra/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/standards/mcp/templates/app/infra/adapters/__init__.py b/standards/mcp/templates/app/infra/adapters/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/standards/mcp/templates/app/infra/adapters/service.py b/standards/mcp/templates/app/infra/adapters/service.py
new file mode 100644
index 0000000..dfa46d8
--- /dev/null
+++ b/standards/mcp/templates/app/infra/adapters/service.py
@@ -0,0 +1,128 @@
+"""# Summary
+
+Generic CRUD service adaptor over a Mongo repository.
+
+Bridge pydantic models and mongo-ops documents, exposing create, read, update,
+patch, delete, and search operations for every domain that needs them.
+"""
+from typing import Generic, Type, TypeVar
+
+from infra.db.repos import mongo
+
+from pydantic import BaseModel
+from mongo_ops import BaseDocument
+from mongo_ops.models import PyObjectId
+
+Model = TypeVar("Model", bound=BaseModel)
+Schema = TypeVar("Schema", bound=BaseDocument)
+
+
+class CrudsServiceAdaptor(Generic[Model, Schema]):
+ """Adapt a generic Mongo repository to a model/schema CRUD service.
+
+ Attributes:
+ repo (mongo.BaseRepository): Repository backing all CRUD operations.
+ model (Type[Model]): Pydantic model type handled by this service.
+ schema (Type[Schema]): Document schema type persisted by the repository.
+ """
+
+ repo: mongo.BaseRepository
+ model: Type[Model]
+ schema: Type[Schema]
+
+ def __init__(self, *, repo=None):
+ """Initialize the adaptor with a backing repository.
+
+ Args:
+ repo (mongo.BaseRepository): Repository used for all operations.
+ """
+ self.repo = repo
+
+ @staticmethod
+ def model_to_dict(model_obj: Model) -> dict:
+ """Serialize a model into a sparse dictionary.
+
+ Args:
+ model_obj (Model): Model instance to serialize.
+
+ Returns:
+ dict: Fields that are set and not ``None``.
+ """
+ return model_obj.model_dump(exclude_unset=True, exclude_none=True)
+
+ async def create(self, data: Model | dict) -> Schema:
+ """Create a document from a model or raw mapping.
+
+ Args:
+ data (Model | dict): Model instance or field mapping to persist.
+
+ Returns:
+ Schema: Persisted document.
+ """
+ if isinstance(data, dict):
+ data = self.schema(**data)
+ return await self.repo.create(data)
+
+ async def read(self, id: PyObjectId) -> Schema | None:
+ """Read a document by primary key.
+
+ Args:
+ id (PyObjectId): Document identifier.
+
+ Returns:
+ Schema | None: Matching document, or ``None`` when absent.
+ """
+ return await self.repo.get_by_id(id)
+
+ async def update(self, id: PyObjectId, data: Model | dict) -> Schema | None:
+ """Replace a document by primary key.
+
+ Args:
+ id (PyObjectId): Document identifier.
+ data (Model | dict): Replacement model or field mapping.
+
+ Returns:
+ Schema | None: Updated document, or ``None`` when absent.
+ """
+ return await self.repo.update(
+ id,
+ data if isinstance(data, dict) else self.model_to_dict(data),
+ )
+
+ async def patch(self, id: PyObjectId, data: dict) -> Schema | None:
+ """Partially update a document by primary key.
+
+ Args:
+ id (PyObjectId): Document identifier.
+ data (dict): Fields to patch.
+
+ Returns:
+ Schema | None: Updated document, or ``None`` when absent.
+ """
+ return await self.repo.patch(id, data)
+
+ async def delete(self, id: PyObjectId) -> bool:
+ """Delete a document by primary key.
+
+ Args:
+ id (PyObjectId): Document identifier.
+
+ Returns:
+ bool: ``True`` when a document was deleted.
+ """
+ return await self.repo.delete(id)
+
+ async def search(self, filter: dict | None = None, *args, **kwargs) -> list[Schema]:
+ """List documents matching an optional filter.
+
+ Args:
+ filter (dict | None): Query filter passed to the repository.
+
+ Returns:
+ list[Schema]: Matching documents.
+
+ Notes:
+ Extra positional and keyword arguments are forwarded to the
+ repository.
+ """
+ return await self.repo.get_many(filter=filter, *args, **kwargs)
\ No newline at end of file
diff --git a/standards/mcp/templates/app/infra/db/__init__.py b/standards/mcp/templates/app/infra/db/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/standards/mcp/templates/app/infra/db/repos/__init__.py b/standards/mcp/templates/app/infra/db/repos/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/standards/mcp/templates/app/infra/db/repos/mongo.py b/standards/mcp/templates/app/infra/db/repos/mongo.py
new file mode 100644
index 0000000..a68f8f3
--- /dev/null
+++ b/standards/mcp/templates/app/infra/db/repos/mongo.py
@@ -0,0 +1,42 @@
+"""# Summary
+
+Generic MongoDB CRUD repo — single implementation for all domains.
+
+Select a concrete mongo-ops repository (base, cached, or populating) from the
+options supplied to the factory.
+"""
+from typing import List, Optional, Type
+
+from mongo_ops import BaseRepository, CachedBaseRepository, PopulatingRepository
+from mongo_ops.cache import CacheBackend, CacheConfig
+from mongo_ops.models import BaseDocument
+from mongo_ops.populate import PopulationEngine, PopulateRule
+
+
+def _repo_factory(
+ collection: str,
+ db_model: Type[BaseDocument],
+ cache_backend: Optional[CacheBackend] = None,
+ cache_config: Optional[CacheConfig] = None,
+ engine: Optional[PopulationEngine] = None,
+ populate_rules: Optional[List[PopulateRule]] = None,
+) -> BaseRepository:
+ """Build the appropriate mongo-ops repository.
+
+ Args:
+ collection (str): Collection name to operate on.
+ db_model (Type[BaseDocument]): Document model bound to the collection.
+ cache_backend (Optional[CacheBackend]): Cache backend enabling cached reads.
+ cache_config (Optional[CacheConfig]): Cache configuration for the backend.
+ engine (Optional[PopulationEngine]): Population engine for reference expansion.
+ populate_rules (Optional[List[PopulateRule]]): Rules applied by the engine.
+
+ Returns:
+ BaseRepository: Repository instance matching the supplied options.
+ """
+ if engine and populate_rules is not None:
+ return PopulatingRepository(collection, db_model, engine, populate_rules)
+ elif cache_backend is not None:
+ return CachedBaseRepository(collection, db_model, cache_backend, cache_config)
+ else:
+ return BaseRepository(collection, db_model)
\ No newline at end of file
diff --git a/standards/mcp/templates/app/infra/db/setup.py b/standards/mcp/templates/app/infra/db/setup.py
new file mode 100644
index 0000000..0cca126
--- /dev/null
+++ b/standards/mcp/templates/app/infra/db/setup.py
@@ -0,0 +1,23 @@
+"""# Summary
+
+Database setup — connection lifecycle only. No domain imports.
+
+Connect and disconnect the shared Mongo connection manager at application
+startup and shutdown.
+"""
+from mongo_ops import MongoConnectionManager
+
+
+async def setup_database(uri: str, db_name: str):
+ """Connect the shared Mongo connection manager.
+
+ Args:
+ uri (str): Mongo connection URI.
+ db_name (str): Target database name.
+ """
+ await MongoConnectionManager.connect(uri=uri, db_name=db_name)
+
+
+async def shutdown_database():
+ """Disconnect the shared Mongo connection manager."""
+ await MongoConnectionManager.disconnect()
\ No newline at end of file
diff --git a/standards/mcp/templates/app/init.py b/standards/mcp/templates/app/init.py
new file mode 100644
index 0000000..68a6fac
--- /dev/null
+++ b/standards/mcp/templates/app/init.py
@@ -0,0 +1,51 @@
+"""# Summary
+
+Infrastructure initialization — connects MongoDB and wires all repos and services.
+Shared bootstrap for both the FastAPI app and the worker, guarded against
+repeated initialization via a module-level flag.
+"""
+from typing import Any, Dict
+
+from config import settings
+from domain.item.ports.db import create_item_repo
+from domain.item.ports.service import get_item_service
+from domain.job.ports.db import create_job_repo
+from domain.job.ports.service import get_job_service
+from infra.db.setup import setup_database, shutdown_database
+
+_resources: Dict[str, Any] = {}
+_initialized: bool = False
+
+
+async def init() -> Dict[str, Any]:
+ """Connect MongoDB, wire all repos and services, and return the resources dict.
+
+ Idempotent: repeated calls short-circuit and return the cached resources.
+
+ Returns:
+ Dict[str, Any]: Named services keyed by resource name.
+ """
+ global _resources, _initialized
+ if _initialized:
+ return _resources
+
+ await setup_database(uri=settings.mongo_uri, db_name=settings.mongo_db)
+
+ item_repo = create_item_repo()
+ job_repo = create_job_repo()
+
+ _resources["item_repo"] = item_repo
+ _resources["job_repo"] = job_repo
+ _resources["item_svc"] = get_item_service(item_repo)
+ _resources["job_svc"] = get_job_service(job_repo)
+
+ _initialized = True
+ return _resources
+
+
+async def shutdown() -> None:
+ """Disconnect MongoDB and reset the resources cache."""
+ global _initialized
+ await shutdown_database()
+ _resources.clear()
+ _initialized = False
\ No newline at end of file
diff --git a/standards/mcp/templates/app/interfaces/__init__.py b/standards/mcp/templates/app/interfaces/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/standards/mcp/templates/app/interfaces/item/__init__.py b/standards/mcp/templates/app/interfaces/item/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/standards/mcp/templates/app/interfaces/item/routes.py b/standards/mcp/templates/app/interfaces/item/routes.py
new file mode 100644
index 0000000..995f782
--- /dev/null
+++ b/standards/mcp/templates/app/interfaces/item/routes.py
@@ -0,0 +1,42 @@
+"""# Summary
+
+Item route handlers — names must match openapi.yaml operationIds.
+"""
+from fastapi import Request
+from fastapi.responses import JSONResponse
+
+from domain.item.models import Item
+
+
+def svc(request: Request):
+ """Resolve the item CRUD service from app state."""
+ return request.app.state.db_resources["item_svc"]
+
+
+async def create_item(request: Request, body: dict):
+ """Create an item from the request body."""
+ data = body.model_dump() if hasattr(body, "model_dump") else dict(body)
+ doc = await svc(request).create(Item(**data))
+ return JSONResponse(status_code=201, content=doc.model_dump(mode="json", by_alias=True))
+
+
+async def get_item(request: Request, id: str):
+ """Fetch one item by id."""
+ doc = await svc(request).read(id)
+ if doc is None:
+ return JSONResponse(status_code=404, content={"detail": "item not found"})
+ return doc.model_dump(mode="json", by_alias=True)
+
+
+async def list_items(request: Request):
+ """List all items."""
+ docs = await svc(request).search({})
+ return [d.model_dump(mode="json", by_alias=True) for d in docs]
+
+
+async def delete_item(request: Request, id: str):
+ """Delete one item by id."""
+ ok = await svc(request).delete(id)
+ if not ok:
+ return JSONResponse(status_code=404, content={"detail": "item not found"})
+ return {"deleted": True}
\ No newline at end of file
diff --git a/standards/mcp/templates/app/interfaces/job/__init__.py b/standards/mcp/templates/app/interfaces/job/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/standards/mcp/templates/app/interfaces/job/routes.py b/standards/mcp/templates/app/interfaces/job/routes.py
new file mode 100644
index 0000000..3d4f80f
--- /dev/null
+++ b/standards/mcp/templates/app/interfaces/job/routes.py
@@ -0,0 +1,34 @@
+"""# Summary
+
+Job route handlers — names must match openapi.yaml operationIds.
+"""
+from fastapi import Request
+from fastapi.responses import JSONResponse
+
+from domain.job.models import Job
+
+
+def svc(request: Request):
+ """Resolve the job CRUD service from app state."""
+ return request.app.state.db_resources["job_svc"]
+
+
+async def create_job(request: Request, body: dict):
+ """Create a job from the request body."""
+ data = body.model_dump() if hasattr(body, "model_dump") else dict(body)
+ doc = await svc(request).create(Job(**data))
+ return JSONResponse(status_code=201, content=doc.model_dump(mode="json", by_alias=True))
+
+
+async def get_job(request: Request, id: str):
+ """Fetch one job by id."""
+ doc = await svc(request).read(id)
+ if doc is None:
+ return JSONResponse(status_code=404, content={"detail": "job not found"})
+ return doc.model_dump(mode="json", by_alias=True)
+
+
+async def list_jobs(request: Request):
+ """List all jobs."""
+ docs = await svc(request).search({})
+ return [d.model_dump(mode="json", by_alias=True) for d in docs]
\ No newline at end of file
diff --git a/standards/mcp/templates/app/interfaces/openapi.py b/standards/mcp/templates/app/interfaces/openapi.py
new file mode 100644
index 0000000..8c43aa7
--- /dev/null
+++ b/standards/mcp/templates/app/interfaces/openapi.py
@@ -0,0 +1,8 @@
+"""# Summary
+
+All handler functions — names must match openapi.yaml operationId values. The
+OpenAPI-first router resolves each operation to a handler by name, so this module
+re-exports every route handler from the subpackages into one flat namespace.
+"""
+from interfaces.item.routes import *
+from interfaces.job.routes import *
\ No newline at end of file
diff --git a/standards/mcp/templates/app/main.py b/standards/mcp/templates/app/main.py
new file mode 100644
index 0000000..0a451e3
--- /dev/null
+++ b/standards/mcp/templates/app/main.py
@@ -0,0 +1,54 @@
+"""# Summary
+
+FastAPI application bootstrap — uses OpenAPIFirstApp for spec-driven routing.
+Handlers are looked up by operationId in interfaces.openapi, so no router tables
+are declared here; the lifespan initializes and shuts down app resources.
+"""
+import logging
+from contextlib import asynccontextmanager
+
+from dotenv import load_dotenv
+from openapi_first.app import OpenAPIFirstApp
+from starlette.middleware.cors import CORSMiddleware
+
+from init import init, shutdown
+
+load_dotenv()
+
+logging.basicConfig(
+ level=logging.INFO,
+ format="%(asctime)s [%(name)s] %(levelname)s: %(message)s",
+)
+
+import interfaces.openapi as openapi_routes
+
+logger = logging.getLogger(__name__)
+
+
+@asynccontextmanager
+async def lifespan(app: OpenAPIFirstApp):
+ """Initialize resource wiring on startup and shut it down on exit.
+
+ Args:
+ app (OpenAPIFirstApp): The ASGI application exposing app.state.
+
+ Yields:
+ None: The application runs while the context is open.
+ """
+ resources = await init()
+ app.state.db_resources = resources
+
+ yield
+
+ await shutdown()
+
+
+app = OpenAPIFirstApp(
+ openapi_path="openapi.yaml",
+ routes_module=openapi_routes,
+ title="Item API",
+ version="0.1.0",
+ lifespan=lifespan,
+)
+
+app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])
\ No newline at end of file
diff --git a/standards/mcp/templates/app/openapi.yaml b/standards/mcp/templates/app/openapi.yaml
new file mode 100644
index 0000000..652471a
--- /dev/null
+++ b/standards/mcp/templates/app/openapi.yaml
@@ -0,0 +1,161 @@
+openapi: 3.0.3
+info:
+ title: Item API
+ version: 0.1.0
+paths:
+ /items:
+ get:
+ operationId: list_items
+ summary: List items
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Item"
+ post:
+ operationId: create_item
+ summary: Create an item
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/ItemInput"
+ responses:
+ "201":
+ description: Created
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/Item"
+ /items/{id}:
+ get:
+ operationId: get_item
+ summary: Get an item
+ parameters:
+ - name: id
+ in: path
+ required: true
+ schema:
+ type: string
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/Item"
+ "404":
+ description: Not found
+ delete:
+ operationId: delete_item
+ summary: Delete an item
+ parameters:
+ - name: id
+ in: path
+ required: true
+ schema:
+ type: string
+ responses:
+ "200":
+ description: Deleted
+ "404":
+ description: Not found
+ /jobs:
+ get:
+ operationId: list_jobs
+ summary: List jobs
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Job"
+ post:
+ operationId: create_job
+ summary: Create a job
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/JobInput"
+ responses:
+ "201":
+ description: Created
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/Job"
+ /jobs/{id}:
+ get:
+ operationId: get_job
+ summary: Get a job
+ parameters:
+ - name: id
+ in: path
+ required: true
+ schema:
+ type: string
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/Job"
+ "404":
+ description: Not found
+components:
+ schemas:
+ Item:
+ type: object
+ required: [name, quantity]
+ properties:
+ id:
+ type: string
+ name:
+ type: string
+ quantity:
+ type: integer
+ ItemInput:
+ type: object
+ required: [name]
+ properties:
+ name:
+ type: string
+ quantity:
+ type: integer
+ default: 1
+ Job:
+ type: object
+ required: [name, status]
+ properties:
+ id:
+ type: string
+ name:
+ type: string
+ status:
+ type: string
+ enum: [pending, processing, done, failed]
+ retry_count:
+ type: integer
+ error_message:
+ type: string
+ payload:
+ type: object
+ JobInput:
+ type: object
+ required: [name]
+ properties:
+ name:
+ type: string
+ payload:
+ type: object
\ No newline at end of file
diff --git a/standards/mcp/templates/app/pipelines/__init__.py b/standards/mcp/templates/app/pipelines/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/standards/mcp/templates/app/pipelines/example/__init__.py b/standards/mcp/templates/app/pipelines/example/__init__.py
new file mode 100644
index 0000000..196d174
--- /dev/null
+++ b/standards/mcp/templates/app/pipelines/example/__init__.py
@@ -0,0 +1,7 @@
+"""# Summary
+
+Example pipeline family — a reusable skeleton, not an app-specific pipeline.
+
+Replace ``Example*`` names and the three stages with your family's stages. The
+shape (abc → impl → pipeline.yaml → resources/) stays the same.
+"""
diff --git a/standards/mcp/templates/app/pipelines/example/abc.py b/standards/mcp/templates/app/pipelines/example/abc.py
new file mode 100644
index 0000000..ebbc570
--- /dev/null
+++ b/standards/mcp/templates/app/pipelines/example/abc.py
@@ -0,0 +1,53 @@
+"""# Summary
+
+Example pipeline family — port contracts (stage interfaces + root interface).
+
+Ports are declared as a DI tree: the root's annotated slots *are* the dependency
+decisions. The concrete subclass in ``impl.py`` pins the stage types.
+"""
+from abc import ABC, abstractmethod
+from typing import Any
+
+from pipelines.shared.ports import FinalizePort
+
+
+class IngestPort(ABC):
+ """Obtain the raw input as text/bytes."""
+
+ @abstractmethod
+ def materialize(self, source: Any) -> str:
+ """Return the raw input materialized as text."""
+
+
+class ReadPort(ABC):
+ """Parse materialized input into records."""
+
+ @abstractmethod
+ def read(self, content: str) -> list[dict[str, Any]]:
+ """Return parsed records."""
+
+
+class NormalizePort(ABC):
+ """Normalize raw records into the target shape."""
+
+ @abstractmethod
+ def build(self, records: list[dict[str, Any]]) -> list[dict[str, Any]]:
+ """Return normalized records."""
+
+
+class ExamplePipelinePort(ABC):
+ """Root of the example family.
+
+ Notes:
+ The slot annotations below are contracts. A concrete subclass re-annotates
+ them with stage implementations; the DI container instantiates the tree.
+ """
+
+ ingest: IngestPort
+ reader: ReadPort
+ normalize: NormalizePort
+ finalize: FinalizePort
+
+ @abstractmethod
+ async def run(self, source: Any) -> list[dict[str, Any]]:
+ """Run the full stage chain and return the normalized records."""
diff --git a/standards/mcp/templates/app/pipelines/example/impl.py b/standards/mcp/templates/app/pipelines/example/impl.py
new file mode 100644
index 0000000..d78c30e
--- /dev/null
+++ b/standards/mcp/templates/app/pipelines/example/impl.py
@@ -0,0 +1,71 @@
+"""# Summary
+
+Example pipeline family — concrete stages, the wired root, and its registration.
+
+The concrete subclass re-annotates each slot with a stage implementation; the DI
+container builds the whole tree. Register the family so the shared registry can
+resolve it by ``(kind, variant, mode)``.
+"""
+from pathlib import Path
+from typing import Any
+
+from pipelines.example.abc import (
+ ExamplePipelinePort,
+ IngestPort,
+ NormalizePort,
+ ReadPort,
+)
+from pipelines.shared.loader import build_pipeline
+from pipelines.shared.ports import FinalizePort
+from pipelines.shared.registry import register
+
+
+class TextIngest(IngestPort):
+ """Materialize any source as text."""
+
+ def materialize(self, source: Any) -> str:
+ return source if isinstance(source, str) else str(source)
+
+
+class LineReader(ReadPort):
+ """Read one record per non-empty line."""
+
+ def read(self, content: str) -> list[dict[str, Any]]:
+ return [{"line": line} for line in content.splitlines() if line.strip()]
+
+
+class PassthroughNormalize(NormalizePort):
+ """Return records unchanged (replace with real normalization)."""
+
+ def build(self, records: list[dict[str, Any]]) -> list[dict[str, Any]]:
+ return records
+
+
+class NoopFinalize(FinalizePort):
+ """Drop records (replace with a service-backed finalize stage)."""
+
+ async def finalize(self, records: list[dict[str, Any]]) -> list[Any]:
+ return records
+
+
+class ExamplePipeline(ExamplePipelinePort):
+ """Concrete example family — the dependency decisions live here."""
+
+ ingest: TextIngest
+ reader: LineReader
+ normalize: PassthroughNormalize
+ finalize: NoopFinalize
+
+ async def run(self, source: Any) -> list[dict[str, Any]]:
+ content = self.ingest.materialize(source)
+ records = self.reader.read(content)
+ return self.normalize.build(records)
+
+
+def get_example_pipeline() -> ExamplePipeline:
+ """Wire and return the example pipeline through the shared loader."""
+ resources = Path(__file__).parent / "resources"
+ return build_pipeline(ExamplePipeline, resources_dir=resources)
+
+
+register("example", "default", ExamplePipeline)
diff --git a/standards/mcp/templates/app/pipelines/example/pipeline.yaml b/standards/mcp/templates/app/pipelines/example/pipeline.yaml
new file mode 100644
index 0000000..0ebfe2d
--- /dev/null
+++ b/standards/mcp/templates/app/pipelines/example/pipeline.yaml
@@ -0,0 +1,18 @@
+# Graph spec for the example family: nodes, edges, and the root.
+# Mirrors the port tree in abc.py. Replace with your family's graph.
+schema: ExampleRecord
+nodes:
+ ingest:
+ port: IngestPort
+ reader:
+ port: ReadPort
+ normalize:
+ port: NormalizePort
+ finalize:
+ port: FinalizePort
+edges:
+ - ingest -> reader
+ - reader -> normalize
+ - normalize -> finalize
+roots:
+ default: ExamplePipeline
diff --git a/standards/mcp/templates/app/pipelines/example/resources/patterns/example.yaml b/standards/mcp/templates/app/pipelines/example/resources/patterns/example.yaml
new file mode 100644
index 0000000..0074eff
--- /dev/null
+++ b/standards/mcp/templates/app/pipelines/example/resources/patterns/example.yaml
@@ -0,0 +1,4 @@
+# Pattern / rule fragments for the example family.
+# The loader merges every patterns/*.yaml into one registry by key.
+example:
+ enabled: true
diff --git a/standards/mcp/templates/app/pipelines/example/resources/pipeline.yaml b/standards/mcp/templates/app/pipelines/example/resources/pipeline.yaml
new file mode 100644
index 0000000..8a670be
--- /dev/null
+++ b/standards/mcp/templates/app/pipelines/example/resources/pipeline.yaml
@@ -0,0 +1,5 @@
+# Runtime spec consumed by the shared loader.
+kind: example
+variant: default
+mode: default
+patterns: []
diff --git a/standards/mcp/templates/app/pipelines/item_sync.py b/standards/mcp/templates/app/pipelines/item_sync.py
new file mode 100644
index 0000000..ff71c0c
--- /dev/null
+++ b/standards/mcp/templates/app/pipelines/item_sync.py
@@ -0,0 +1,32 @@
+"""# Summary
+
+Item-sync pipeline — the demo pipeline axis of the blueprint.
+
+Runs on the worker, consumes a pending job, and materializes its payload as an
+item. Steps are plain async functions here; once the chain branches or needs
+deterministic replay, wrap the steps in a dagpipe DAG (see standards
+"06_pipeline_scaffold").
+"""
+import logging
+
+logger = logging.getLogger(__name__)
+
+
+async def run_item_sync(job, resources: dict) -> None:
+ """Materialize a pending job payload as an item.
+
+ Step 1: extract the payload from the job.
+ Step 2: create the item through the item service.
+
+ Args:
+ job: The pending job document.
+ resources (dict): Wired services and adaptors.
+ """
+ payload = job.payload or {}
+ item_svc = resources["item_svc"]
+
+ await item_svc.create({
+ "name": payload.get("name", job.name),
+ "quantity": payload.get("quantity", 1),
+ })
+ logger.info("Created item from job %s", str(job.id))
\ No newline at end of file
diff --git a/standards/mcp/templates/app/pipelines/shared/__init__.py b/standards/mcp/templates/app/pipelines/shared/__init__.py
new file mode 100644
index 0000000..8aef39c
--- /dev/null
+++ b/standards/mcp/templates/app/pipelines/shared/__init__.py
@@ -0,0 +1,5 @@
+"""# Summary
+
+Shared pipeline scaffolding — ports, loader, and registry reused by every
+pipeline family. See standards "06_pipeline_scaffold".
+"""
diff --git a/standards/mcp/templates/app/pipelines/shared/loader.py b/standards/mcp/templates/app/pipelines/shared/loader.py
new file mode 100644
index 0000000..10a3c2b
--- /dev/null
+++ b/standards/mcp/templates/app/pipelines/shared/loader.py
@@ -0,0 +1,54 @@
+"""# Summary
+
+Pipeline loader — wire a family root through the DI library and attach the
+merged pattern registry from ``resources/patterns/*.yaml``.
+
+All families share this loader so wiring is never hand-rolled.
+"""
+from pathlib import Path
+from typing import Any
+
+import yaml
+
+from hexa import build
+
+
+def load_patterns(resources_dir: str | Path) -> dict[str, Any]:
+ """Merge every pattern fragment under ``resources/patterns`` into one map.
+
+ Args:
+ resources_dir (str | Path): Family resources directory.
+
+ Returns:
+ dict[str, Any]: Merged pattern/rule fragments keyed by fragment name.
+ """
+ registry: dict[str, Any] = {}
+ patterns = Path(resources_dir) / "patterns"
+ if patterns.is_dir():
+ for path in sorted(patterns.glob("*.yaml")):
+ data = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
+ registry.update(data)
+ return registry
+
+
+def build_pipeline(
+ root_cls,
+ *,
+ resources_dir: str | Path = "resources",
+ instances: dict[str, Any] | None = None,
+ config: dict[str, Any] | None = None,
+):
+ """Wire ``root_cls`` and inject the merged pattern registry.
+
+ Args:
+ root_cls: The pipeline root class (a port tree).
+ resources_dir (str | Path): Directory holding ``patterns/*.yaml``.
+ instances (dict[str, Any] | None): Runtime objects injected by slot name.
+ config (dict[str, Any] | None): Config values propagated by field name.
+
+ Returns:
+ The fully wired pipeline root.
+ """
+ merged_instances = dict(instances or {})
+ merged_instances.setdefault("registry", load_patterns(resources_dir))
+ return build(root_cls, instances=merged_instances, config=config or {})
diff --git a/standards/mcp/templates/app/pipelines/shared/ports.py b/standards/mcp/templates/app/pipelines/shared/ports.py
new file mode 100644
index 0000000..ec15d8e
--- /dev/null
+++ b/standards/mcp/templates/app/pipelines/shared/ports.py
@@ -0,0 +1,25 @@
+"""# Summary
+
+Shared pipeline ports — the contracts every pipeline family reuses.
+
+A port is an abstract stage contract. Concrete families pin their implementations
+on a root's annotated slots; the DI library instantiates the tree.
+"""
+from abc import ABC, abstractmethod
+from typing import Any
+
+
+class FinalizePort(ABC):
+ """Terminal stage — turn normalized records into persisted resources."""
+
+ @abstractmethod
+ async def finalize(self, records: list[dict[str, Any]]) -> list[Any]:
+ """Persist records and return the created resources."""
+
+
+class RegistryPort(ABC):
+ """Resolve a pattern / rule fragment by key."""
+
+ @abstractmethod
+ def resolve(self, key: str) -> dict[str, Any]:
+ """Return the fragment registered under ``key``."""
diff --git a/standards/mcp/templates/app/pipelines/shared/registry.py b/standards/mcp/templates/app/pipelines/shared/registry.py
new file mode 100644
index 0000000..d5de929
--- /dev/null
+++ b/standards/mcp/templates/app/pipelines/shared/registry.py
@@ -0,0 +1,29 @@
+"""# Summary
+
+Family registry — resolve a ``(kind, variant, mode)`` triple to a root class.
+
+A single lookup replaces hand-rolled ``if/else`` family selection.
+"""
+from typing import Dict, Type
+
+# Populated by ``register`` as families import.
+_FAMILIES: Dict[str, Dict[str, Dict[str, Type]]] = {}
+
+
+def register(kind: str, variant: str, root_cls: Type, mode: str = "default") -> Type:
+ """Register ``root_cls`` for a ``(kind, variant, mode)`` triple.
+
+ Returns:
+ Type: The registered class (so it can be used as a decorator).
+ """
+ _FAMILIES.setdefault(kind, {}).setdefault(variant, {})[mode] = root_cls
+ return root_cls
+
+
+def resolve_root(kind: str, variant: str, mode: str = "default") -> Type:
+ """Return the root class for a ``(kind, variant, mode)`` triple.
+
+ Raises:
+ KeyError: When no family matches the triple.
+ """
+ return _FAMILIES[kind][variant][mode]
diff --git a/standards/mcp/templates/app/pyproject.toml b/standards/mcp/templates/app/pyproject.toml
new file mode 100644
index 0000000..48288f1
--- /dev/null
+++ b/standards/mcp/templates/app/pyproject.toml
@@ -0,0 +1,44 @@
+[build-system]
+requires = ["setuptools>=68"]
+build-backend = "setuptools.build_meta"
+
+[project]
+name = "app-template"
+version = "0.1.0"
+requires-python = ">=3.11"
+dependencies = [
+ "fastapi",
+ "uvicorn[standard]",
+ "pydantic>=2",
+ "motor",
+ "python-dotenv",
+ "pyyaml",
+ "openapi-first",
+ "mongo-ops",
+ "hexa",
+ "dagpipe",
+]
+
+[project.optional-dependencies]
+dev = [
+ "pytest",
+ "pytest-asyncio",
+ "httpx",
+ "ruff",
+ "doc-forge",
+]
+
+[tool.setuptools.packages.find]
+where = ["."]
+include = ["domain*", "infra*", "interfaces*", "pipelines*", "scripts*"]
+
+[tool.pytest.ini_options]
+asyncio_mode = "auto"
+testpaths = ["tests"]
+
+[tool.ruff]
+line-length = 100
+target-version = "py311"
+
+[tool.ruff.lint]
+select = ["E", "F", "I", "UP"]
\ No newline at end of file
diff --git a/standards/mcp/templates/app/scripts/__init__.py b/standards/mcp/templates/app/scripts/__init__.py
new file mode 100644
index 0000000..43e0740
--- /dev/null
+++ b/standards/mcp/templates/app/scripts/__init__.py
@@ -0,0 +1,7 @@
+"""# Summary
+
+Operator scripts — run from the project root as ``python -m scripts.``.
+
+They use the composition root (:func:`init.init` / :func:`init.shutdown`) so they
+share the exact wiring the API and worker use.
+"""
diff --git a/standards/mcp/templates/app/scripts/drop.py b/standards/mcp/templates/app/scripts/drop.py
new file mode 100644
index 0000000..042064a
--- /dev/null
+++ b/standards/mcp/templates/app/scripts/drop.py
@@ -0,0 +1,32 @@
+"""# Summary
+
+Operator script — drop the collections owned by each service for a clean slate.
+
+Run from the project root:
+
+ python -m scripts.drop
+"""
+import asyncio
+
+from init import init, shutdown
+
+
+async def main() -> None:
+ """Delete every service collection, best-effort, then shut down."""
+ resources = await init()
+ try:
+ dropped = 0
+ for name, svc in resources.items():
+ if not name.endswith("_svc"):
+ continue
+ delete_many = getattr(svc.repo, "delete_many", None)
+ if delete_many is not None:
+ await delete_many({})
+ dropped += 1
+ print(f"Dropped {dropped} service collection(s).")
+ finally:
+ await shutdown()
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/standards/mcp/templates/app/scripts/seed.py b/standards/mcp/templates/app/scripts/seed.py
new file mode 100644
index 0000000..2a2ad0f
--- /dev/null
+++ b/standards/mcp/templates/app/scripts/seed.py
@@ -0,0 +1,26 @@
+"""# Summary
+
+Operator script — seed demo data through the composition root.
+
+Run from the project root:
+
+ python -m scripts.seed
+"""
+import asyncio
+
+from init import init, shutdown
+
+
+async def main() -> None:
+ """Seed one demo item and shut down cleanly."""
+ resources = await init()
+ try:
+ item_svc = resources["item_svc"]
+ await item_svc.create({"name": "seed", "quantity": 1})
+ print("Seeded one item.")
+ finally:
+ await shutdown()
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/standards/mcp/templates/app/tests/__init__.py b/standards/mcp/templates/app/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/standards/mcp/templates/app/tests/integration/__init__.py b/standards/mcp/templates/app/tests/integration/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/standards/mcp/templates/app/tests/integration/test_item_routes.py b/standards/mcp/templates/app/tests/integration/test_item_routes.py
new file mode 100644
index 0000000..a7ecc0e
--- /dev/null
+++ b/standards/mcp/templates/app/tests/integration/test_item_routes.py
@@ -0,0 +1,21 @@
+import importlib
+
+import interfaces.openapi as routes_module
+
+from openapi_first.app import OpenAPIFirstApp
+
+
+def test_openapi_app_builds():
+ """The spec-first app must build and plant each route handler."""
+ module = importlib.import_module(routes_module.__name__)
+ for operation in ("create_item", "get_item", "list_items", "delete_item",
+ "create_job", "get_job", "list_jobs"):
+ assert hasattr(module, operation)
+
+ app = OpenAPIFirstApp(
+ openapi_path="openapi.yaml",
+ routes_module=module,
+ title="Item API",
+ version="0.1.0",
+ )
+ assert app is not None
\ No newline at end of file
diff --git a/standards/mcp/templates/app/tests/unit/__init__.py b/standards/mcp/templates/app/tests/unit/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/standards/mcp/templates/app/tests/unit/test_item_models.py b/standards/mcp/templates/app/tests/unit/test_item_models.py
new file mode 100644
index 0000000..10b58ab
--- /dev/null
+++ b/standards/mcp/templates/app/tests/unit/test_item_models.py
@@ -0,0 +1,22 @@
+from domain.item.models import Item
+from domain.job.models import Job
+
+
+def test_item_defaults():
+ item = Item(name="widget")
+ assert item.quantity == 1
+
+
+def test_job_defaults():
+ job = Job(name="sync")
+ assert job.status == "pending"
+ assert job.retry_count == 0
+ assert job.error_message is None
+
+
+def test_job_rejects_unknown_status():
+ try:
+ Job(name="sync", status="exploded")
+ except Exception:
+ return
+ raise AssertionError("status should be rejected")
\ No newline at end of file
diff --git a/standards/mcp/templates/app/tests/unit/test_item_sync.py b/standards/mcp/templates/app/tests/unit/test_item_sync.py
new file mode 100644
index 0000000..6bc2d99
--- /dev/null
+++ b/standards/mcp/templates/app/tests/unit/test_item_sync.py
@@ -0,0 +1,28 @@
+from types import SimpleNamespace
+
+from pipelines.item_sync import run_item_sync
+
+
+class FakeItemSvc:
+ def __init__(self):
+ self.created = []
+
+ async def create(self, data: dict):
+ self.created.append(dict(data))
+
+
+async def test_run_item_sync_creates_item():
+ svc = FakeItemSvc()
+ job = SimpleNamespace(id="j1", name="widget", payload={"quantity": 4})
+ await run_item_sync(job, {"item_svc": svc})
+
+ assert len(svc.created) == 1
+ assert svc.created[0] == {"name": "widget", "quantity": 4}
+
+
+async def test_run_item_sync_uses_payload_name():
+ svc = FakeItemSvc()
+ job = SimpleNamespace(id="j1", name="ignored", payload={"name": "gadget"})
+ await run_item_sync(job, {"item_svc": svc})
+
+ assert svc.created[0]["name"] == "gadget"
\ No newline at end of file
diff --git a/standards/mcp/templates/app/worker.py b/standards/mcp/templates/app/worker.py
new file mode 100644
index 0000000..c7fda13
--- /dev/null
+++ b/standards/mcp/templates/app/worker.py
@@ -0,0 +1,67 @@
+"""# Summary
+
+Background worker — polls for pending jobs and runs the item-sync pipeline.
+The poll loop is the pipeline axis of the blueprint: resources are created by
+the API axis, workers consume them on a fixed cadence.
+
+Notes:
+- This process is separate from the API process. Any in-memory event bus is per
+ process — if the pipeline must surface live progress to the API, swap the bus
+ adaptor for a broker-backed one (see standards 07_worker_and_progress).
+"""
+import asyncio
+import logging
+
+from config import settings
+from pipelines.item_sync import run_item_sync
+
+logging.basicConfig(
+ level=logging.INFO,
+ format="%(asctime)s [%(name)s] %(levelname)s: %(message)s",
+)
+
+logger = logging.getLogger(__name__)
+
+
+async def poll_jobs(resources: dict):
+ """Poll for pending jobs and run the item-sync pipeline on each.
+
+ Args:
+ resources (dict): Wired services from :func:`init.init`.
+ """
+ job_svc = resources["job_svc"]
+ logger.info("WORKER STARTED — poll=%ss max_retries=%d",
+ settings.worker_poll_seconds, settings.worker_max_retries)
+
+ while True:
+ pending = await job_svc.search(filter={"status": "pending"}, limit=10)
+ logger.info("Found %d pending job(s)", len(pending) if pending else 0)
+
+ for job in pending:
+ job_id = str(job.id)
+ if (job.retry_count or 0) >= settings.worker_max_retries:
+ logger.warning("SKIPPING job %s — retries exhausted", job_id)
+ continue
+
+ await job_svc.patch(job_id, {"status": "processing"})
+ try:
+ await run_item_sync(job, resources)
+ await job_svc.patch(job_id, {"status": "done", "error_message": ""})
+ except Exception as exc: # noqa: BLE001
+ logger.exception("job %s FAILED: %s", job_id, exc)
+ await job_svc.patch(job_id, {"status": "failed", "error_message": str(exc)})
+
+ await asyncio.sleep(settings.worker_poll_seconds)
+
+
+if __name__ == "__main__":
+ from init import init, shutdown
+
+ async def _run():
+ resources = await init()
+ try:
+ await poll_jobs(resources)
+ finally:
+ await shutdown()
+
+ asyncio.run(_run())
\ No newline at end of file