- move templates into src/templates; renumber wiki pages; add app-components page - embed agentic build prompt as a copyable markdown block; add mermaid diagrams - build.py writes <name>/mcp; config static bundle -> standards/mcp - registry reads libraries.json as bytes (committed file was UTF-16LE); normalize to UTF-8 - Dockerfile: drop stale blog copy; per-repo <name>/mcp copies - drop nested standards/mcp/standards duplicate and empty root mcp/
1 line
31 KiB
JSON
1 line
31 KiB
JSON
{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"Standards Reference","text":"<p>The reference half of the Standards description: the enforceable rules, patterns, and the internal library catalog that agents consult while coding.</p> <ul> <li>Layering Rules \u2014 the strict request \u2192 domain \u2192 repository layering and the rules that enforce it.</li> <li>Composition Root \u2014 a single composition point wiring the whole app.</li> <li>Spec-First Routing \u2014 the OpenAPI contract as the routing source of truth.</li> <li>CRUD Template \u2014 the standardized generic resource endpoint scaffold.</li> <li>Pipeline Scaffold \u2014 the deterministic pipeline skeleton for multi-stage jobs.</li> <li>Worker & Progress \u2014 queued work, progress reporting, and visibility.</li> <li>Human-in-the-Loop \u2014 guardrails and approvals inside automations.</li> <li>Infra Adapters \u2014 how infrastructure connects without leaking into domain.</li> <li>Genericity Matrix \u2014 what is generic vs app-specific.</li> <li>Library Registry \u2014 the catalog of internal libraries behind all of it.</li> </ul> <p>Read the Wiki for the philosophy and rationale; these pages are the reference that mirrors the <code>standards</code> MCP bundle.</p>"},{"location":"02_layering_rules/","title":"Layering Rules","text":""},{"location":"02_layering_rules/#summary","title":"Summary","text":"<p>The dependency direction is fixed. Violating it is a design defect.</p>"},{"location":"02_layering_rules/#allowed-imports","title":"Allowed imports","text":"<pre><code>interfaces/* -> domain/* (services, ports, models)\ninterfaces/* -> infra/adapters/* (only read-only value objects)\ndomain/* -> domain/* (same layer)\ndomain/* -> pipelines/* (call a pipeline via a port)\ninfra/adapters/* -> domain/ports/* (implement the interface)\nmain.py/init.py -> everything (composition root only)\n</code></pre>"},{"location":"02_layering_rules/#forbidden-imports","title":"Forbidden imports","text":"<pre><code>domain/* -> interfaces/* (never)\ndomain/* -> fastapi/starlette (never)\ninfra/adapters/* -> interfaces/* (never)\ninterfaces/* -> infra/db/* (state lives in resources, not imports)\n</code></pre>"},{"location":"02_layering_rules/#why","title":"Why","text":"<ul> <li>A pure domain is testable without a server, a database, or an event loop.</li> <li>Adapters can be swapped (in-memory fake, real database) without touching business logic.</li> <li>The HTTP edge stays thin and replaceable.</li> </ul>"},{"location":"02_layering_rules/#the-portadapter-contract","title":"The port/adapter contract","text":"<p>A port is an abstract class or protocol declared in <code>domain/<x>/ports/</code>. An adapter implements it in <code>infra/adapters/</code>. The composition root binds the two.</p> <pre><code>domain/<resource>/ports/db.py -> repository port + document model\ndomain/<resource>/ports/service.py -> service + factory\ninfra/db/repos/<vendor>.py -> repository adapter factory\ninfra/adapters/service.py -> generic CRUD service base\n</code></pre>"},{"location":"02_layering_rules/#enforcement","title":"Enforcement","text":"<ul> <li>Keep every domain module importable with only the standard library and Pydantic. If a domain import pulls in <code>fastapi</code>, the layering is broken.</li> <li>Reviewers (and CI) reject <code>import fastapi</code> inside <code>domain/</code> or <code>infra/</code>.</li> </ul>"},{"location":"03_composition_root/","title":"Composition Root","text":""},{"location":"03_composition_root/#summary","title":"Summary","text":"<p>All wiring happens in exactly one module. It builds a flat resource registry and exposes it to the HTTP layer and to scripts.</p>"},{"location":"03_composition_root/#the-resource-registry","title":"The resource registry","text":"<p><code>init.py</code> returns a plain <code>dict[str, Any]</code>. Keys follow two conventions:</p> <ul> <li><code><resource>_repo</code> \u2014 a repository adapter for a resource.</li> <li><code><resource>_svc</code> \u2014 a service for a resource.</li> </ul> <p>Plus named infrastructure adaptors:</p> <ul> <li><code>http_cache_store</code> \u2014 response cache backend.</li> <li><code>progress_bus</code> \u2014 progress/event bus.</li> <li><code><vendor>_<kind></code> \u2014 fetchers, clients, stores.</li> </ul> <p>The API stores the dict on <code>app.state.resources</code> during lifespan startup. Handlers read from it; nothing imports concrete adapters.</p>"},{"location":"03_composition_root/#shape","title":"Shape","text":"<pre><code>_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</code></pre>"},{"location":"03_composition_root/#typed-accessors","title":"Typed accessors","text":"<p>Expose a <code>get_svc(name)</code> helper with one <code>@overload</code> per key so scripts get typing without importing concrete classes:</p> <pre><code>@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</code></pre> <p>Also expose <code>get_db()</code> (raw database handle) and pure helpers such as <code>compute_fingerprint(payload)</code> (canonical hash used for deduplication).</p>"},{"location":"03_composition_root/#rules","title":"Rules","text":"<ul> <li><code>init()</code> is idempotent.</li> <li>The composition root is the only place that imports concrete adapters.</li> <li>Handlers never call <code>init()</code>; they read <code>request.app.state.resources</code>.</li> <li>Scripts call <code>init()</code> / <code>shutdown()</code> around their work.</li> </ul>"},{"location":"04_spec_first_routing/","title":"Spec-First Routing","text":""},{"location":"04_spec_first_routing/#summary","title":"Summary","text":"<p>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.</p>"},{"location":"04_spec_first_routing/#the-contract","title":"The contract","text":"<ul> <li><code>openapi.yaml</code> (or <code>.json</code>) declares paths, schemas, parameters, responses, and security.</li> <li>A custom <code>operationId</code> names the handler function.</li> <li>The framework (<code>openapi_first</code>) validates requests/responses against the spec and dispatches to the handler whose name equals <code>operationId</code>.</li> </ul>"},{"location":"04_spec_first_routing/#app-bootstrap","title":"App bootstrap","text":"<pre><code># 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</code></pre> <p><code>lifespan</code> calls the composition root:</p> <pre><code>@asynccontextmanager\nasync def lifespan(app):\n app.state.resources = await init()\n yield\n await shutdown()\n</code></pre>"},{"location":"04_spec_first_routing/#handler-registry","title":"Handler registry","text":"<p><code>interfaces/openapi.py</code> is a flat re-export of every handler, so the framework can resolve <code>operationId</code> across the whole app:</p> <pre><code>from interfaces.item.routes import *\nfrom interfaces.wallet.routes import *\nfrom interfaces.progress.sse import *\n</code></pre>"},{"location":"04_spec_first_routing/#handler-shape","title":"Handler shape","text":"<p>Handlers are thin and synchronous with respect to business logic:</p> <pre><code>async def create_item(request: Request, body: dict):\n svc = request.app.state.resources[\"item_svc\"]\n return await svc.create(Item.model_validate(body))\n</code></pre>"},{"location":"04_spec_first_routing/#conventions","title":"Conventions","text":"<ul> <li><code>operationId</code> naming: <code>list_<resource></code>, <code>create_<resource></code>, <code>get_<resource></code>, <code>update_<resource></code>, <code>delete_<resource></code>.</li> <li>Read-only endpoints may use a cache decorator keyed on path + query + user.</li> <li>Validation errors -> <code>400</code>; not found -> <code>404</code>; delete -> <code>204</code>.</li> <li>Security is declared once in the spec and enforced by the framework; handlers contain no auth code.</li> </ul>"},{"location":"04_spec_first_routing/#codegen","title":"Codegen","text":"<p>The <code>openapi_first</code> library can scaffold models and route stubs from the spec, and ships app templates (CRUD app, model app, service). Prefer codegen over hand-writing boilerplate.</p>"},{"location":"05_crud_template/","title":"CRUD Template","text":""},{"location":"05_crud_template/#summary","title":"Summary","text":"<p>Every resource follows the same four-file slice. Repetition is intentional: it makes the codebase predictable and generatable.</p>"},{"location":"05_crud_template/#the-slice","title":"The slice","text":"<pre><code>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</code></pre>"},{"location":"05_crud_template/#modelspy","title":"models.py","text":"<p>A framework-free Pydantic model describing the business shape.</p> <pre><code>class Item(BaseModel):\n name: str\n quantity: int = 0\n</code></pre>"},{"location":"05_crud_template/#portsdbpy","title":"ports/db.py","text":"<p>The persistence model extends the business model and adds database concerns. The repository factory selects the right repository flavour.</p> <pre><code>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</code></pre>"},{"location":"05_crud_template/#portsservicepy","title":"ports/service.py","text":"<p>The service is built through the DI library, injecting the repository. The generic CRUD base provides <code>create/read/update/patch/delete/search/...</code>.</p> <pre><code>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</code></pre>"},{"location":"05_crud_template/#naming-conventions","title":"Naming conventions","text":"Concept Convention collection plural snake case (<code>items</code>) document <code><Resource>Document</code> repository factory <code>create_<resource>_repo</code> service <code><Resource>Service</code> service factory <code>get_<resource>_service</code> resource key <code><resource>_svc</code> operationId <code>list_items</code>, <code>create_item</code>, ..."},{"location":"05_crud_template/#optional-files","title":"Optional files","text":"<ul> <li><code>converters.py</code> \u2014 pure functions mapping between domain types.</li> <li><code>ports/enrichment.py</code> \u2014 adapts a pipeline into an async callable the service can invoke (used when a resource is populated by a pipeline).</li> </ul>"},{"location":"06_pipeline_scaffold/","title":"Pipeline Scaffold","text":""},{"location":"06_pipeline_scaffold/#summary","title":"Summary","text":"<p>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.</p>"},{"location":"06_pipeline_scaffold/#when-to-use-a-pipeline","title":"When to use a pipeline","text":"<p>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.</p>"},{"location":"06_pipeline_scaffold/#structure-of-a-family","title":"Structure of a family","text":"<p>A family is one input shape (e.g. a file format or a message kind). Each family has the same skeleton:</p> <pre><code>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</code></pre>"},{"location":"06_pipeline_scaffold/#shared-scaffolding","title":"Shared scaffolding","text":"<pre><code>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</code></pre>"},{"location":"06_pipeline_scaffold/#the-loader","title":"The loader","text":"<pre><code>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</code></pre>"},{"location":"06_pipeline_scaffold/#the-registry","title":"The registry","text":"<p>A single lookup replaces hand-rolled <code>if/else</code> selection:</p> <pre><code>_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</code></pre> <p>Adding a new variant means adding one class and one registry entry.</p>"},{"location":"06_pipeline_scaffold/#driving-a-pipeline","title":"Driving a pipeline","text":"<p>Stages are driven explicitly so intermediate state can be persisted:</p> <pre><code>pipeline = build(resolve_root(kind, variant))\ncontent = pipeline.ingest.materialize(raw_bytes)\nrecords = pipeline.reader.read(content)\nitems = pipeline.normalize.build(records)\n</code></pre>"},{"location":"06_pipeline_scaffold/#testing","title":"Testing","text":"<p>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.</p>"},{"location":"07_worker_and_progress/","title":"Worker & Progress","text":""},{"location":"07_worker_and_progress/#summary","title":"Summary","text":"<p>Long-running work runs in a background worker. Jobs are durable documents that move through an explicit status machine. Progress is streamed to clients.</p>"},{"location":"07_worker_and_progress/#the-status-machine","title":"The status machine","text":"<pre><code>pending -> processing -> completed\n \\-> failed (retried up to N times)\n \\-> paused (waiting for human input)\n</code></pre> <ul> <li><code>pending</code> \u2014 queued, eligible for pickup.</li> <li><code>processing</code> \u2014 claimed by a worker.</li> <li><code>paused</code> \u2014 blocked on a human-in-the-loop question.</li> <li><code>failed</code> \u2014 error; <code>retry_count</code> increments until a max, then skipped.</li> <li><code>completed</code> \u2014 terminal success.</li> </ul>"},{"location":"07_worker_and_progress/#the-poll-loop","title":"The poll loop","text":"<pre><code>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</code></pre> <p>Tunables live in environment variables: <code>WORKER_POLL_MIN</code>, <code>WORKER_POLL_MAX</code>, <code>WORKER_MAX_RETRIES</code>.</p>"},{"location":"07_worker_and_progress/#progress-events","title":"Progress events","text":"<p>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.</p> <pre><code>class ProgressEvent(BaseModel):\n job_id: str\n step: str\n status: str # started | progress | completed | skipped | paused\n message: dict | None = None\n</code></pre> <p>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:</p> <pre><code>await self.bus.publish(ProgressEvent(job_id=jid, step=\"normalize\",\n status=\"completed\", message={\"count\": n}))\n</code></pre>"},{"location":"07_worker_and_progress/#streaming-endpoint","title":"Streaming endpoint","text":"<pre><code>async def stream_job_events(request, id):\n bus = request.app.state.resources.get(\"progress_bus\")\n if bus is None:\n return {\"events\": []}\n\n async def gen():\n sent = 0\n while True:\n events = bus.get_events(id)\n for e in events[sent:]:\n yield f\"data: {e.model_dump_json(exclude_none=True)}\\n\\n\"\n sent += 1\n await asyncio.sleep(0.5)\n\n return StreamingResponse(gen(), media_type=\"text/event-stream\")\n</code></pre> <p>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.</p>"},{"location":"08_human_in_loop/","title":"Human-in-the-Loop","text":""},{"location":"08_human_in_loop/#summary","title":"Summary","text":"<p>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.</p>"},{"location":"08_human_in_loop/#the-mechanism","title":"The mechanism","text":"<ol> <li>During processing, a stage raises a domain exception carrying the context needed to ask the question:</li> </ol> <pre><code>raise PauseForResolution(\n job_id=jid,\n reference=\"<stable key for this decision>\",\n options=[...],\n context={...},\n)\n</code></pre> <ol> <li>The orchestrator catches it, records a question document, and re-raises so the worker marks the job <code>paused</code>.</li> <li>A client lists pending questions and answers one:</li> </ol> <pre><code>GET /jobs/{id}/questions -> list pending questions\nPOST /questions/{qid}/resolve {choice} -> record the choice\n</code></pre> <ol> <li>When the last pending question is resolved, the resolve endpoint flips the parent job back to <code>pending</code>.</li> <li>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.</li> </ol>"},{"location":"08_human_in_loop/#the-question-document","title":"The question document","text":"<pre><code>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</code></pre>"},{"location":"08_human_in_loop/#resume-map","title":"Resume map","text":"<p>The worker turns answered questions into a lookup keyed by <code>reference</code>, then injects it into the pipeline so that stage can skip re-raising:</p> <pre><code>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</code></pre>"},{"location":"08_human_in_loop/#resolve-endpoint","title":"Resolve endpoint","text":"<pre><code>async def resolve_question(request, id, body):\n svc = request.app.state.resources[\"question_svc\"]\n q = await svc.read(id)\n await svc.update(id, {\"chosen\": body[\"chosen\"], \"status\": \"resolved\",\n \"resolved_at\": now()})\n\n pending = await svc.search(filter={\"job_id\": q.job_id, \"status\": \"pending\"},\n limit=1)\n if not pending:\n jobs = request.app.state.resources[\"job_svc\"]\n job = await jobs.read(q.job_id)\n if job and job.status == \"paused\":\n await jobs.update_status(q.job_id, \"pending\")\n return q\n</code></pre>"},{"location":"08_human_in_loop/#cli-parity","title":"CLI parity","text":"<p>The same flow is available through scripts/CLI: <code>list</code>, <code>get</code>, <code>create</code>, <code>questions</code>, <code>resolve</code>, <code>retry</code>.</p>"},{"location":"09_infra_adaptors/","title":"Infra Adapters","text":""},{"location":"09_infra_adaptors/#summary","title":"Summary","text":"<p>All external systems are reached through adapters in <code>infra/</code>. Prefer an existing internal library over writing a new adapter.</p>"},{"location":"09_infra_adaptors/#database-mongodb","title":"Database (MongoDB)","text":"<ul> <li>A single connection manager owns the client lifecycle. <code>setup_database(uri, db_name)</code> / <code>shutdown_database()</code>.</li> <li>A repository factory selects the repository flavour:</li> </ul> <pre><code>repo_factory(collection, document, engine=None,\n populate_rules=None, cache=None)\n</code></pre> <ul> <li>base repository \u2014 plain CRUD;</li> <li>populating repository \u2014 resolves referenced documents (foreign keys);</li> <li> <p>cached repository \u2014 an LRU/TTL cache in front of reads.</p> </li> <li> <p>Referenced documents are described declaratively:</p> </li> </ul> <pre><code>PopulateRule(field=\"owner\", collection=\"users\",\n nested_rules=[], max_depth=2, projection=None)\n</code></pre> <ul> <li>Transactions use a session context manager provided by the library.</li> </ul> <p>Library: the Mongo operations library (see registry). Do not write raw Motor calls in the domain.</p>"},{"location":"09_infra_adaptors/#http-response-cache","title":"HTTP response cache","text":"<p>A generic GET cache sits behind a decorator.</p> <pre><code>@cache_response(ttl=60)\nasync def list_items(request): ...\n</code></pre> <ul> <li>Keyed on path + query + user.</li> <li>No-op when no store is wired.</li> <li><code>invalidate(*resources)</code> bumps a generation counter for write-triggered invalidation.</li> </ul>"},{"location":"09_infra_adaptors/#event-progress-bus","title":"Event / progress bus","text":"<p>A port with an in-memory default adapter (<code>publish</code>, <code>get_events</code>). Swap for a durable broker in production (see Worker & Progress).</p>"},{"location":"09_infra_adaptors/#external-clients-ingestion","title":"External clients (ingestion)","text":"<p>External fetchers (files, mail, HTTP) are adapters that wrap ingestion libraries. Construct them in the composition root and inject into orchestrators.</p> <p>Libraries: the ingestion library (formats) and the mail library (email).</p>"},{"location":"09_infra_adaptors/#llm-clients","title":"LLM clients","text":"<p>When a pipeline calls a model provider, route through a pool adaptor that handles provider selection, retries, and client lifecycle:</p> <pre><code>pool = setup_async_pool(provider=settings.llm_provider)\nresult = await pool.complete(model=\"...\", messages=[...])\n</code></pre> <p>Providers are selected by <code>LLM_PROVIDER</code> and configured by environment.</p>"},{"location":"09_infra_adaptors/#key-value-store","title":"Key-value store","text":"<p>For report/snapshot storage, provide a pluggable store port with a Redis adapter and an in-memory fallback.</p>"},{"location":"09_infra_adaptors/#rule","title":"Rule","text":"<p>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.</p>"},{"location":"12_genericity_matrix/","title":"Genericity Matrix","text":""},{"location":"12_genericity_matrix/#summary","title":"Summary","text":"<p>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.</p>"},{"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 <code>init.py</code> shape, idempotency, <code>get_svc</code> overloading Web edge <code>OpenAPIFirstApp</code> bootstrap, handler-name = <code>operationId</code>, 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 <code>pyproject.toml</code> project name, dependencies <code>Dockerfile</code> / <code>docker-compose.yaml</code> service name, ports, services <code>.env.example</code> connection strings, provider keys <code>openapi.yaml</code> every path/schema/operationId <code>scripts/</code> 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":"<p>Everything app-specific reduces to two axes:</p> <ol> <li>Resource model \u2014 the set of resources, their fields, relationships, and the OpenAPI contract.</li> <li>Pipeline family \u2014 only if inputs require multi-step transformation.</li> </ol> <p>If you can specify those two axes, the rest of the app follows mechanically.</p>"},{"location":"13_library_registry/","title":"Library Registry","text":""},{"location":"13_library_registry/#summary","title":"Summary","text":"<p>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.</p> <p>Each library also exposes its own documentation as an MCP server. An AI agent can read a library's full API by connecting to its MCP and reading <code>doc://index</code>, <code>doc://nav</code>, and <code>doc://modules/{module}</code>.</p>"},{"location":"13_library_registry/#catalog","title":"Catalog","text":"Library Import Purpose Use instead of MCP DAG Pipe <code>dagpipe</code> Deterministic state-transformation DAG engine hand-rolled orchestration/topological ordering <code>:8006</code> Hexa <code>hexa</code> Declarative DI / composition-root tooling manual wiring, hand-written factories (see note) Mongo Ops <code>mongo_ops</code> Async MongoDB repositories, cache, populate, transactions raw Motor CRUD and connection lifecycle <code>:8007</code> Omniread <code>omniread</code> Format-agnostic fetch + parse (HTML, PDF, CSV, XLSX) custom scrapers/parsers <code>:8003</code> OpenAPI First <code>openapi_first</code> Spec-first FastAPI bootstrap, client, codegen decorator routers and hand-written clients <code>:8004</code> Mail Intake <code>mail_intake</code> Provider-agnostic read-only email ingestion Gmail OAuth + MIME parsing <code>:8002</code> Py JWT <code>jwtlib</code> JWT auth primitives (register/login/introspect) password hashing + token code <code>:8005</code> Doc Forge <code>docforge</code> Docstring compiler \u2192 MkDocs + MCP docs custom doc generators <code>:8001</code> 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":"<p>A single standards MCP exposes <code>find_library(capability)</code>, which returns the matching library, its import name, its \"use instead of\" guidance, and its MCP endpoint. Query it before writing any infrastructure.</p>"},{"location":"13_library_registry/#notes","title":"Notes","text":"<ul> <li>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.</li> <li>Library MCP servers expose documentation resources (not executable tools): they tell an agent the available API surface so it does not invent code.</li> </ul>"}]} |