docs(standards): mirror doc-forge wiki style; mermaid + copyable build prompt
- 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/
This commit is contained in:
13
Dockerfile
13
Dockerfile
@@ -40,13 +40,20 @@ COPY py-jwt /usr/share/nginx/html/py-jwt
|
||||
COPY hexa /usr/share/nginx/html/hexa
|
||||
COPY mongo-ops /usr/share/nginx/html/mongo-ops
|
||||
COPY auth-server /usr/share/nginx/html/auth-server
|
||||
COPY blog /usr/share/nginx/html/blog
|
||||
COPY media-manager /usr/share/nginx/html/media-manager
|
||||
COPY khata-app /usr/share/nginx/html/khata-app
|
||||
COPY standards /usr/share/nginx/html/standards
|
||||
|
||||
# --- MCP bundles -----------------------------------------------------------
|
||||
COPY mcp /app/mcp
|
||||
# --- MCP bundles (one per repo under /app/<name>/mcp) ----------------------
|
||||
COPY openapi-first/mcp /app/openapi-first/mcp
|
||||
COPY doc-forge/mcp /app/doc-forge/mcp
|
||||
COPY dagpipe/mcp /app/dagpipe/mcp
|
||||
COPY mail-intake/mcp /app/mail-intake/mcp
|
||||
COPY omniread/mcp /app/omniread/mcp
|
||||
COPY py-jwt/mcp /app/py-jwt/mcp
|
||||
COPY hexa/mcp /app/hexa/mcp
|
||||
COPY mongo-ops/mcp /app/mongo-ops/mcp
|
||||
COPY standards/mcp /app/standards/mcp
|
||||
|
||||
EXPOSE 8000 8001 8002 8003 8004 8005 8006 8007 8008 8009 9000
|
||||
|
||||
|
||||
6
build.py
6
build.py
@@ -7,7 +7,7 @@ hub. Any top-level folder with ``src/mkdocs.*.yml`` gets, for each config:
|
||||
|
||||
1. a MkDocs HTML build into the target dir the config declares
|
||||
(e.g. ``standards/src/mkdocs.wiki.yml`` -> ``standards/wiki/``), and
|
||||
2. a doc-forge-shaped MCP bundle under ``mcp/<name>/``
|
||||
2. a doc-forge-shaped MCP bundle under ``<name>/mcp/``
|
||||
(``index.json``, ``nav.json``, ``modules/*.json``), regenerated from the
|
||||
lib pages (``src/lib/*.md``) so the bundle mirrors the lib site.
|
||||
|
||||
@@ -100,7 +100,7 @@ def _read_title(path: Path) -> str:
|
||||
|
||||
|
||||
def build_bundle(folder: Path, dry_run: bool = False) -> None:
|
||||
"""Regenerate ``mcp/<name>`` from ``folder/src/lib`` markdown.
|
||||
"""Regenerate ``<name>/mcp`` from ``folder/src/lib`` markdown.
|
||||
|
||||
The bundle mirrors the lib site (in the same way doc-forge's MCP output
|
||||
mirrors its lib documentation). Writes ``index.json``, ``nav.json`` and
|
||||
@@ -115,7 +115,7 @@ def build_bundle(folder: Path, dry_run: bool = False) -> None:
|
||||
lib = folder / "src" / "lib"
|
||||
if not lib.is_dir():
|
||||
return
|
||||
out_dir = ROOT / "mcp" / name
|
||||
out_dir = folder / "mcp"
|
||||
modules_dir = out_dir / "modules"
|
||||
if dry_run:
|
||||
print(f"[MCP] {name} {lib.relative_to(ROOT)}/*.md "
|
||||
|
||||
@@ -69,7 +69,7 @@ def load_config(path: Path | str = "config.yml") -> ServiceConfig:
|
||||
mcp_static: dict[str, MCPBundle] = {}
|
||||
for entry in data.get("mcp_static", []):
|
||||
mcp_static[entry["name"]] = MCPBundle(
|
||||
root=Path("mcp") / entry["bundle"],
|
||||
root=Path(entry["bundle"]),
|
||||
name=entry.get("server") or entry["name"],
|
||||
port=entry["port"],
|
||||
kind=entry.get("kind", "docs"),
|
||||
|
||||
@@ -143,7 +143,7 @@ mcp_static:
|
||||
# Hand-authored bundles that live in this repo (not collected from a source repo).
|
||||
- name: standards
|
||||
title: Standards
|
||||
bundle: standards
|
||||
bundle: standards/mcp
|
||||
server: standards
|
||||
port: 8009
|
||||
kind: registry
|
||||
|
||||
@@ -44,7 +44,7 @@ class StandardsMCPServer(MCPServer):
|
||||
"path": str(path),
|
||||
"libraries": [],
|
||||
}
|
||||
return json.loads(path.read_text(encoding="utf-8"))
|
||||
return json.loads(path.read_bytes())
|
||||
|
||||
def _register_standards(self) -> None:
|
||||
@self.app.resource("registry://libraries")
|
||||
|
||||
@@ -639,7 +639,7 @@ every application. <strong>Use them.</strong> Writing a private copy of any capa
|
||||
is a defect.</p>
|
||||
<p>Each library also exposes its own documentation as an MCP server. An AI agent
|
||||
can read a library's full API by connecting to its MCP and reading
|
||||
<code>docs://index</code>, <code>docs://nav</code>, and <code>docs://modules/{module}</code>.</p>
|
||||
<code>doc://index</code>, <code>doc://nav</code>, and <code>doc://modules/{module}</code>.</p>
|
||||
<hr />
|
||||
<h2 id="catalog">Catalog</h2>
|
||||
<table>
|
||||
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -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`docs://index`, `docs://nav`, and `docs://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 | *(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",
|
||||
"objects": {}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"project": "standards",
|
||||
"type": "standards",
|
||||
"modules_count": 11,
|
||||
"source": "hand-authored"
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"module": "composition_root",
|
||||
"content": {
|
||||
"path": "composition_root",
|
||||
"docstring": "# Composition Root\n\n## Summary\n\nAll wiring happens in exactly one module. It builds a **flat resource\nregistry** and exposes it to the HTTP layer and to scripts.\n\n---\n\n## The resource registry\n\n`init.py` returns a plain `dict[str, Any]`. Keys follow two conventions:\n\n- `<resource>_repo` — a repository adapter for a resource.\n- `<resource>_svc` — a service for a resource.\n\nPlus named infrastructure adaptors:\n\n- `http_cache_store` — response cache backend.\n- `progress_bus` — progress/event bus.\n- `<vendor>_<kind>` — fetchers, clients, stores.\n\nThe API stores the dict on `app.state.resources` during lifespan startup.\nHandlers read from it; nothing imports concrete adapters.\n\n## Shape\n\n```python\n_resources: dict = {}\n_initialized = False\n\nasync def init(settings=None) -> dict:\n global _initialized\n if _initialized:\n return _resources\n\n await setup_database(uri=settings.mongo_uri, db_name=settings.mongo_db)\n\n # 1. leaf repositories\n user_repo = create_user_repo()\n\n # 2. dependent repositories (dependencies passed by keyword)\n wallet_repo = create_wallet_repo(user_repo=user_repo)\n\n # 3. services (build via the DI library, injecting the repo)\n _resources[\"user_repo\"] = user_repo\n _resources[\"wallet_repo\"] = wallet_repo\n _resources[\"user_svc\"] = get_user_service(user_repo)\n _resources[\"wallet_svc\"] = get_wallet_service(wallet_repo)\n\n # 4. infrastructure adaptors (optional)\n _resources[\"http_cache_store\"] = HttpCacheStore(...)\n _resources[\"progress_bus\"] = ProgressEventBus()\n\n _initialized = True\n return _resources\n\nasync def shutdown() -> None:\n global _initialized\n await close_stores(_resources)\n await shutdown_database()\n _resources.clear()\n _initialized = False\n```\n\n## Typed accessors\n\nExpose a `get_svc(name)` helper with one `@overload` per key so scripts get\ntyping without importing concrete classes:\n\n```python\n@overload\ndef get_svc(name: Literal[\"user_svc\"]) -> UserService: ...\n@overload\ndef get_svc(name: Literal[\"wallet_svc\"]) -> WalletService: ...\n\ndef get_svc(name: str):\n return _resources[name]\n```\n\nAlso expose `get_db()` (raw database handle) and pure helpers such as\n`compute_fingerprint(payload)` (canonical hash used for deduplication).\n\n## Rules\n\n- `init()` is **idempotent**.\n- The composition root is the only place that imports concrete adapters.\n- Handlers never call `init()`; they read `request.app.state.resources`.\n- Scripts call `init()` / `shutdown()` around their work.\n",
|
||||
"objects": {}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"module": "crud_template",
|
||||
"content": {
|
||||
"path": "crud_template",
|
||||
"docstring": "# CRUD Template\n\n## Summary\n\nEvery resource follows the same four-file slice. Repetition is intentional: it\nmakes the codebase predictable and generatable.\n\n---\n\n## The slice\n\n```\ndomain/<resource>/\n models.py # pure Pydantic model (no DB fields)\n ports/\n db.py # document model + repository factory\n service.py # service class + factory\n converters.py # (optional) transforms\n```\n\n## models.py\n\nA framework-free Pydantic model describing the business shape.\n\n```python\nclass Item(BaseModel):\n name: str\n quantity: int = 0\n```\n\n## ports/db.py\n\nThe persistence model extends the business model and adds database concerns.\nThe repository factory selects the right repository flavour.\n\n```python\nclass ItemDocument(Item, BaseDocument): # BaseDocument adds id/created_at/...\n pass\n\ndef create_item_repo(*, owner_repo=None) -> ItemRepository:\n return repo_factory(\n collection=\"items\",\n document=ItemDocument,\n populate_rules=[PopulateRule(field=\"owner\", collection=\"users\")],\n cache=None, # or a CachedBaseRepository config\n )\n```\n\n## ports/service.py\n\nThe service is built through the DI library, injecting the repository. The\ngeneric CRUD base provides `create/read/update/patch/delete/search/...`.\n\n```python\nclass ItemService(CrudsServiceAdaptor[Item, ItemDocument]):\n model = Item\n schema = ItemDocument\n\ndef get_item_service(repo) -> ItemService:\n return build(ItemService, instances={\"repo\": repo})\n```\n\n## Naming conventions\n\n| Concept | Convention |\n|---|---|\n| collection | plural snake case (`items`) |\n| document | `<Resource>Document` |\n| repository factory | `create_<resource>_repo` |\n| service | `<Resource>Service` |\n| service factory | `get_<resource>_service` |\n| resource key | `<resource>_svc` |\n| operationId | `list_items`, `create_item`, ... |\n\n## Optional files\n\n- `converters.py` — pure functions mapping between domain types.\n- `ports/enrichment.py` — adapts a pipeline into an async callable the service\n can invoke (used when a resource is populated by a pipeline).\n",
|
||||
"objects": {}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"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",
|
||||
"objects": {}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"module": "human_in_loop",
|
||||
"content": {
|
||||
"path": "human_in_loop",
|
||||
"docstring": "# Human-in-the-Loop\n\n## Summary\n\nSome inputs are ambiguous and must be resolved by a human before processing can\ncontinue. The blueprint models this as a **durable pause**: an exception is\nraised, a question is recorded, the job pauses, and later resumes.\n\n---\n\n## The mechanism\n\n1. During processing, a stage raises a domain exception carrying the context\n needed to ask the question:\n\n ```python\n raise PauseForResolution(\n job_id=jid,\n reference=\"<stable key for this decision>\",\n options=[...],\n context={...},\n )\n ```\n\n2. The orchestrator catches it, records a **question document**, and re-raises\n so the worker marks the job `paused`.\n3. A client lists pending questions and answers one:\n\n ```\n GET /jobs/{id}/questions -> list pending questions\n POST /questions/{qid}/resolve {choice} -> record the choice\n ```\n\n4. When the **last** pending question is resolved, the resolve endpoint flips\n the parent job back to `pending`.\n5. The worker picks it up again, rebuilds a **resolution map** from previously\n answered questions, and seeds it into the pipeline so the same decision is\n not asked twice.\n\n## The question document\n\n```python\nclass Question(BaseDocument):\n job_id: str\n reference: str # stable key: stage|field|value\n status: str # pending | resolved\n options: list[dict]\n context: dict\n chosen: dict | None = None\n resolved_at: str | None = None\n```\n\n## Resume map\n\nThe worker turns answered questions into a lookup keyed by `reference`, then\ninjects it into the pipeline so that stage can skip re-raising:\n\n```python\nresolved = {q.reference: q.chosen for q in await svc.search(\n filter={\"job_id\": jid, \"status\": \"resolved\"})}\n\npipeline = build(root, instances={\"pause\": handler}, config={\"resolved\": resolved})\n```\n\n## Resolve endpoint\n\n```python\nasync def resolve_question(request, id, body):\n svc = request.app.state.resources[\"question_svc\"]\n q = await svc.read(id)\n await svc.update(id, {\"chosen\": body[\"chosen\"], \"status\": \"resolved\",\n \"resolved_at\": now()})\n\n pending = await svc.search(filter={\"job_id\": q.job_id, \"status\": \"pending\"},\n limit=1)\n if not pending:\n jobs = request.app.state.resources[\"job_svc\"]\n job = await jobs.read(q.job_id)\n if job and job.status == \"paused\":\n await jobs.update_status(q.job_id, \"pending\")\n return q\n```\n\n## CLI parity\n\nThe same flow is available through scripts/CLI: `list`, `get`, `create`,\n`questions`, `resolve`, `retry`.\n",
|
||||
"objects": {}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"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",
|
||||
"objects": {}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"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",
|
||||
"objects": {}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"module": "layering_rules",
|
||||
"content": {
|
||||
"path": "layering_rules",
|
||||
"docstring": "# Layering Rules\n\n## Summary\n\nThe dependency direction is fixed. Violating it is a design defect.\n\n---\n\n## Allowed imports\n\n```\ninterfaces/* -> domain/* (services, ports, models)\ninterfaces/* -> infra/adapters/* (only read-only value objects)\ndomain/* -> domain/* (same layer)\ndomain/* -> pipelines/* (call a pipeline via a port)\ninfra/adapters/* -> domain/ports/* (implement the interface)\nmain.py/init.py -> everything (composition root only)\n```\n\n## Forbidden imports\n\n```\ndomain/* -> interfaces/* (never)\ndomain/* -> fastapi/starlette (never)\ninfra/adapters/* -> interfaces/* (never)\ninterfaces/* -> infra/db/* (state lives in resources, not imports)\n```\n\n## Why\n\n- A pure domain is testable without a server, a database, or an event loop.\n- Adapters can be swapped (in-memory fake, real database) without touching\n business logic.\n- The HTTP edge stays thin and replaceable.\n\n## The port/adapter contract\n\nA **port** is an abstract class or protocol declared in `domain/<x>/ports/`.\nAn **adapter** implements it in `infra/adapters/`. The composition root binds\nthe two.\n\n```\ndomain/<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```\n\n## Enforcement\n\n- Keep every domain module importable with only the standard library and\n Pydantic. If a domain import pulls in `fastapi`, the layering is broken.\n- Reviewers (and CI) reject `import fastapi` inside `domain/` or `infra/`.\n",
|
||||
"objects": {}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"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`docs://index`, `docs://nav`, and `docs://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",
|
||||
"objects": {}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"module": "pipeline_scaffold",
|
||||
"content": {
|
||||
"path": "pipeline_scaffold",
|
||||
"docstring": "# Pipeline Scaffold\n\n## Summary\n\nData-driven processing is expressed as a **composition graph**. The graph is\nbuilt by a DI library; execution is driven by a DAG engine. Stages are pure and\nindividually testable.\n\n---\n\n## When to use a pipeline\n\nUse a pipeline when a resource is produced by a **multi-step transformation**\nof an input (a document, a file, a message) rather than by a simple CRUD write.\n\n## Structure of a family\n\nA **family** is one input shape (e.g. a file format or a message kind). Each\nfamily has the same skeleton:\n\n```\npipelines/<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```\n\n## Shared scaffolding\n\n```\npipelines/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```\n\n## The loader\n\n```python\ndef 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```\n\n## The registry\n\nA single lookup replaces hand-rolled `if/else` selection:\n\n```python\n_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```\n\nAdding a new variant means adding one class and one registry entry.\n\n## Driving a pipeline\n\nStages are driven explicitly so intermediate state can be persisted:\n\n```python\npipeline = build(resolve_root(kind, variant))\ncontent = pipeline.ingest.materialize(raw_bytes)\nrecords = pipeline.reader.read(content)\nitems = pipeline.normalize.build(records)\n```\n\n## Testing\n\nBecause stages are pure, test each stage in isolation and test the assembled\nroot against **fixture inputs** whose expected outputs are stored as golden\nfiles. A parity test replays fixtures headlessly.\n",
|
||||
"objects": {}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"module": "spec_first_routing",
|
||||
"content": {
|
||||
"path": "spec_first_routing",
|
||||
"docstring": "# Spec-First Routing\n\n## Summary\n\nThe OpenAPI document is the single source of truth for the HTTP contract.\nHandlers are bound to it by name; there is no decorator router.\n\n---\n\n## The contract\n\n- `openapi.yaml` (or `.json`) declares paths, schemas, parameters, responses,\n and security.\n- A custom `operationId` names the handler function.\n- The framework (`openapi_first`) validates requests/responses against the\n spec and dispatches to the handler whose name equals `operationId`.\n\n## App bootstrap\n\n```python\n# main.py\nfrom openapi_first.app import OpenAPIFirstApp\n\napp = OpenAPIFirstApp(\n openapi_path=\"openapi.yaml\",\n routes_module=interfaces.openapi, # module whose namespace holds handlers\n title=\"<App> API\",\n version=\"0.1.0\",\n lifespan=lifespan,\n)\n```\n\n`lifespan` calls the composition root:\n\n```python\n@asynccontextmanager\nasync def lifespan(app):\n app.state.resources = await init()\n yield\n await shutdown()\n```\n\n## Handler registry\n\n`interfaces/openapi.py` is a flat re-export of every handler, so the framework\ncan resolve `operationId` across the whole app:\n\n```python\nfrom interfaces.item.routes import *\nfrom interfaces.wallet.routes import *\nfrom interfaces.progress.sse import *\n```\n\n## Handler shape\n\nHandlers are thin and synchronous with respect to business logic:\n\n```python\nasync def create_item(request: Request, body: dict):\n svc = request.app.state.resources[\"item_svc\"]\n return await svc.create(Item.model_validate(body))\n```\n\n## Conventions\n\n- `operationId` naming: `list_<resource>`, `create_<resource>`,\n `get_<resource>`, `update_<resource>`, `delete_<resource>`.\n- Read-only endpoints may use a cache decorator keyed on path + query + user.\n- Validation errors -> `400`; not found -> `404`; delete -> `204`.\n- Security is declared once in the spec and enforced by the framework; handlers\n contain no auth code.\n\n## Codegen\n\nThe `openapi_first` library can scaffold models and route stubs from the spec,\nand ships app templates (CRUD app, model app, service). Prefer codegen over\nhand-writing boilerplate.\n",
|
||||
"objects": {}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"module": "worker_and_progress",
|
||||
"content": {
|
||||
"path": "worker_and_progress",
|
||||
"docstring": "# Worker & Progress\n\n## Summary\n\nLong-running work runs in a background worker. Jobs are durable documents that\nmove through an explicit status machine. Progress is streamed to clients.\n\n---\n\n## The status machine\n\n```\npending -> processing -> completed\n \\-> failed (retried up to N times)\n \\-> paused (waiting for human input)\n```\n\n- `pending` — queued, eligible for pickup.\n- `processing` — claimed by a worker.\n- `paused` — blocked on a human-in-the-loop question.\n- `failed` — error; `retry_count` increments until a max, then skipped.\n- `completed` — terminal success.\n\n## The poll loop\n\n```python\nasync def poll(resources):\n backoff = MIN_POLL\n while True:\n jobs = await job_svc.search(filter={\"status\": \"pending\"}, limit=100)\n backoff = MIN_POLL if jobs else min(backoff * 2, MAX_POLL)\n for job in jobs:\n if (job.retry_count or 0) >= MAX_RETRIES:\n continue\n await job_svc.update_status(job.id, \"processing\")\n try:\n await process(job, resources)\n except PauseForResolution:\n await job_svc.update_status(job.id, \"paused\")\n except Exception as e:\n await job_svc.update_status(\n job.id, \"failed\", error=str(e),\n retry_count=(job.retry_count or 0) + 1,\n )\n await asyncio.sleep(backoff)\n```\n\nTunables live in environment variables: `WORKER_POLL_MIN`, `WORKER_POLL_MAX`,\n`WORKER_MAX_RETRIES`.\n\n## Progress events\n\nA worker publishes structured progress events to a **bus** (a port). The HTTP\nedge exposes a streaming endpoint that replays events for a given job id.\n\n```python\nclass ProgressEvent(BaseModel):\n job_id: str\n step: str\n status: str # started | progress | completed | skipped | paused\n message: dict | None = None\n```\n\nThe bus is an in-memory adapter by default; because it is a port, it can be\nswapped for a durable broker. Emit from the orchestrator at each stage:\n\n```python\nawait self.bus.publish(ProgressEvent(job_id=jid, step=\"normalize\",\n status=\"completed\", message={\"count\": n}))\n```\n\n## Streaming endpoint\n\n```python\nasync def stream_job_events(request, id):\n bus = request.app.state.resources.get(\"progress_bus\")\n if bus is None:\n return {\"events\": []}\n\n async def gen():\n sent = 0\n while True:\n events = bus.get_events(id)\n for e in events[sent:]:\n yield f\"data: {e.model_dump_json(exclude_none=True)}\\n\\n\"\n sent += 1\n await asyncio.sleep(0.5)\n\n return StreamingResponse(gen(), media_type=\"text/event-stream\")\n```\n\n> **Known limitation.** The default in-memory bus is per-process. If the worker\n> and the API run as separate processes, events published by the worker are not\n> visible to the API. For production, back the bus with a shared broker. The\n> port boundary makes this a one-adapter change.\n",
|
||||
"objects": {}
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
[
|
||||
{
|
||||
"module": "layering_rules",
|
||||
"resource": "doc://modules/layering_rules",
|
||||
"title": "Layering Rules"
|
||||
},
|
||||
{
|
||||
"module": "composition_root",
|
||||
"resource": "doc://modules/composition_root",
|
||||
"title": "Composition Root"
|
||||
},
|
||||
{
|
||||
"module": "spec_first_routing",
|
||||
"resource": "doc://modules/spec_first_routing",
|
||||
"title": "Spec-First Routing"
|
||||
},
|
||||
{
|
||||
"module": "crud_template",
|
||||
"resource": "doc://modules/crud_template",
|
||||
"title": "CRUD Template"
|
||||
},
|
||||
{
|
||||
"module": "pipeline_scaffold",
|
||||
"resource": "doc://modules/pipeline_scaffold",
|
||||
"title": "Pipeline Scaffold"
|
||||
},
|
||||
{
|
||||
"module": "worker_and_progress",
|
||||
"resource": "doc://modules/worker_and_progress",
|
||||
"title": "Worker & Progress"
|
||||
},
|
||||
{
|
||||
"module": "human_in_loop",
|
||||
"resource": "doc://modules/human_in_loop",
|
||||
"title": "Human-in-the-Loop"
|
||||
},
|
||||
{
|
||||
"module": "infra_adaptors",
|
||||
"resource": "doc://modules/infra_adaptors",
|
||||
"title": "Infra Adapters"
|
||||
},
|
||||
{
|
||||
"module": "genericity_matrix",
|
||||
"resource": "doc://modules/genericity_matrix",
|
||||
"title": "Genericity Matrix"
|
||||
},
|
||||
{
|
||||
"module": "library_registry",
|
||||
"resource": "doc://modules/library_registry",
|
||||
"title": "Library Registry"
|
||||
},
|
||||
{
|
||||
"module": "index",
|
||||
"resource": "doc://modules/index",
|
||||
"title": "Standards Reference"
|
||||
}
|
||||
]
|
||||
@@ -8,7 +8,7 @@ is a defect.
|
||||
|
||||
Each library also exposes its own documentation as an MCP server. An AI agent
|
||||
can read a library's full API by connecting to its MCP and reading
|
||||
`docs://index`, `docs://nav`, and `docs://modules/{module}`.
|
||||
`doc://index`, `doc://nav`, and `doc://modules/{module}`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -29,7 +29,11 @@ theme:
|
||||
- search.suggest
|
||||
|
||||
markdown_extensions:
|
||||
- pymdownx.superfences
|
||||
- pymdownx.superfences:
|
||||
custom_fences:
|
||||
- name: mermaid
|
||||
class: mermaid
|
||||
format: !!python/name:pymdownx.superfences.fence_code_format
|
||||
- pymdownx.inlinehilite
|
||||
- pymdownx.snippets
|
||||
- admonition
|
||||
@@ -52,8 +56,12 @@ markdown_extensions:
|
||||
plugins:
|
||||
- search
|
||||
|
||||
extra_javascript:
|
||||
- https://unpkg.com/mermaid@10.9.1/dist/mermaid.min.js
|
||||
|
||||
nav:
|
||||
- Home: index.md
|
||||
- Overview: 01_overview.md
|
||||
- Testing: 10_testing.md
|
||||
- Build Recipe: 11_build_recipe.md
|
||||
- App Components: 02_components.md
|
||||
- Build Recipe: 03_build_recipe.md
|
||||
- Testing & Quality: 04_development/01_testing.md
|
||||
@@ -13,7 +13,7 @@ than delete unused code. The domain is a neutral "item", not an app noun.
|
||||
|
||||
## Instantiate a new app
|
||||
|
||||
cp -r templates/app apps/<your-app>
|
||||
cp -r src/templates/app apps/<your-app>
|
||||
cd apps/<your-app>
|
||||
|
||||
1. `pyproject.toml` — rename the package; keep the internal library pins.
|
||||
@@ -1,54 +1,99 @@
|
||||
# Overview
|
||||
|
||||
## Summary
|
||||
|
||||
The blueprint is a **hexagonal (ports and adapters) application** with a
|
||||
The blueprint is a **hexagonal (ports & adapters) application** with a
|
||||
**spec-first** web edge and a **single composition root**. It favors explicit
|
||||
wiring over implicit magic, and it prefers existing internal libraries over
|
||||
hand-written infrastructure.
|
||||
wiring over implicit magic and existing internal libraries over hand-written
|
||||
infrastructure. Start here to build the mental model; the enforceable details
|
||||
live in the reference pages.
|
||||
|
||||
---
|
||||
|
||||
## The three layers
|
||||
## 🏗️ Architecture
|
||||
|
||||
```
|
||||
interfaces/ ← HTTP edge: thin handlers bound to an OpenAPI operationId
|
||||
domain/ ← business core: models, ports (interfaces), services, orchestrators
|
||||
infra/ ← adapters: databases, caches, external clients, hardware
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph EDGE["interfaces/ — HTTP edge"]
|
||||
OAS["openapi.yaml<br/>operationId contract"]
|
||||
RT["<resource>/routes.py<br/>thin handlers"]
|
||||
end
|
||||
|
||||
subgraph CORE["domain/ — pure business core"]
|
||||
MD["models"]
|
||||
PT["ports (abstract)"]
|
||||
SV["services"]
|
||||
end
|
||||
|
||||
subgraph INFRA["infra/ — adapters"]
|
||||
RP["mongo repos (mongo-ops)"]
|
||||
AD["cache · stores · llm · mail"]
|
||||
DB[("MongoDB")]
|
||||
end
|
||||
|
||||
CR["init.py<br/>composition root"]
|
||||
|
||||
OAS --> RT
|
||||
RT --> SV
|
||||
SV --> PT
|
||||
SV --> RP
|
||||
PT -. depends on .- RP
|
||||
CR --- RT
|
||||
CR --- SV
|
||||
CR --- RP
|
||||
RP --> DB
|
||||
AD --- CR
|
||||
```
|
||||
|
||||
Plus two optional layers:
|
||||
The rule holds at every edge: code in an outer ring may depend on a ring closer
|
||||
to the core, never the reverse. `domain/` never imports a web framework or a
|
||||
database driver.
|
||||
|
||||
```
|
||||
pipelines/ ← data-driven transformation graphs (extraction, enrichment)
|
||||
scripts/ ← operator scripts that reuse the composition root
|
||||
```
|
||||
> **Legend** — solid arrows are compile-time calls; dotted arrows are the
|
||||
> port/adapter contract, resolved by the composition root at startup.
|
||||
|
||||
## Core principles
|
||||
---
|
||||
|
||||
1. **One-way dependencies.** `interfaces -> domain -> infra` is allowed;
|
||||
the reverse is forbidden. The domain never imports a framework it does not
|
||||
own.
|
||||
2. **Ports in the domain, adapters in infra.** A service depends on an
|
||||
abstract port; the composition root injects a concrete adapter.
|
||||
## 🔄 The one-way dependency rule
|
||||
|
||||
| Ring | May import | Never imports |
|
||||
|-------------|------------------------------------|----------------------------------|
|
||||
| `interfaces/` | `domain`, framework glue (glue only) | business logic, `infra` directly |
|
||||
| `domain/` | nothing but its own ports | web frameworks, drivers, `infra` |
|
||||
| `infra/` | `domain` ports, drivers, libraries | — |
|
||||
| `init.py` | everything (the wiring point) | — |
|
||||
|
||||
---
|
||||
|
||||
## 🧭 Core principles
|
||||
|
||||
1. **One-way dependencies.** `interfaces -> domain -> infra` is allowed; the
|
||||
reverse is forbidden.
|
||||
2. **Ports in the domain, adapters in infra.** A service depends on an abstract
|
||||
port; the composition root injects a concrete adapter.
|
||||
3. **One composition root.** Nothing constructs its own dependencies. All
|
||||
wiring lives in one module (`init.py`) and is exposed as a flat resource
|
||||
registry.
|
||||
4. **Spec-first.** The HTTP contract is declared in an OpenAPI document. Route
|
||||
wiring lives in one module (`init.py`) exposed as a flat resource registry.
|
||||
4. **Spec-first.** The HTTP contract is declared in `openapi.yaml`; route
|
||||
handlers are looked up by `operationId`. There are no decorator routers.
|
||||
5. **Reuse the libraries.** Pipeline orchestration, DI, Mongo repositories,
|
||||
ingestion, auth, and docs are provided by internal libraries. Do not
|
||||
reimplement them — see the library registry.
|
||||
5. **Reuse the libraries.** Orchestration, DI, Mongo repositories, ingestion,
|
||||
auth, and docs come from internal libraries — see the [Library
|
||||
Registry](../lib/13_library_registry/).
|
||||
6. **Explicit state machines.** Long-running work moves through explicit,
|
||||
durable statuses and can pause for human input.
|
||||
|
||||
## Runtime processes
|
||||
---
|
||||
|
||||
A standard app may run as up to three processes that share the same
|
||||
composition root:
|
||||
## ⚙️ Runtime processes
|
||||
|
||||
| Process | Entry point | Responsibility |
|
||||
|---|---|---|
|
||||
| API | `main.py` | serve HTTP; start/stop resources via lifespan |
|
||||
| Worker | `worker.py` | poll durable jobs, run pipelines, publish progress |
|
||||
| Scripts | `python -m scripts.<name>` | one-shot operator tasks |
|
||||
A standard app may run as up to three processes that share the same composition
|
||||
root:
|
||||
|
||||
| Process | Entry point | Responsibility |
|
||||
|---------|------------------------|-------------------------------------------|
|
||||
| API | `main.py` | serve HTTP; start/stop resources via lifespan |
|
||||
| Worker | `worker.py` | poll durable jobs, run pipelines, publish progress |
|
||||
| Scripts | `python -m scripts.<name>` | one-shot operator tasks |
|
||||
|
||||
---
|
||||
|
||||
## ➡️ Read Next
|
||||
|
||||
- [02 – App Components](02_components.md) · [03 – Build Recipe](03_build_recipe.md)
|
||||
137
standards/src/wiki/02_components.md
Normal file
137
standards/src/wiki/02_components.md
Normal file
@@ -0,0 +1,137 @@
|
||||
# App Components
|
||||
|
||||
What ships in every standard app, and how the pieces relate. This is the
|
||||
blueprint's component model — `templates/app/` is a runnable instance of it.
|
||||
|
||||
---
|
||||
|
||||
## 🗂️ Component model
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph EDGE["interfaces/ — HTTP edge"]
|
||||
OAS["openapi.yaml"]
|
||||
RU["resource routes"]
|
||||
JU["job routes"]
|
||||
end
|
||||
|
||||
subgraph CORE["domain/ — business core"]
|
||||
RI["<resource> · models · ports · services"]
|
||||
JI["<job/worker> · models · ports · services"]
|
||||
PI["pipelines/<family>"]
|
||||
end
|
||||
|
||||
subgraph INFRA["infra/ — adapters"]
|
||||
DB[("MongoDB repos")]
|
||||
CA["redis cache"]
|
||||
ST["stores · progress bus"]
|
||||
LL["llm clients"]
|
||||
end
|
||||
|
||||
W["worker.py"]
|
||||
CR["init.py — composition root"]
|
||||
|
||||
OAS --> RU
|
||||
OAS --> JU
|
||||
RU --> RI
|
||||
JU --> JI
|
||||
JI --> PI
|
||||
PI --> DB
|
||||
PI --> LL
|
||||
RI --> CA
|
||||
RI --> DB
|
||||
JI --> ST
|
||||
W --> JI
|
||||
W --> PI
|
||||
|
||||
CR === RU
|
||||
CR === JU
|
||||
CR === RI
|
||||
CR === JI
|
||||
CR === PI
|
||||
CR === W
|
||||
CR === DB
|
||||
CR === CA
|
||||
CR === ST
|
||||
```
|
||||
|
||||
> **Legend** — solid arrows are direct calls; thick links are wires created by
|
||||
> the composition root. Every component is injected, never self-constructed.
|
||||
|
||||
---
|
||||
|
||||
## 📦 Component roles
|
||||
|
||||
### 1. `interfaces/` — the edge
|
||||
|
||||
Thin handlers bound to `openapi.yaml` `operationId`s. They parse the request,
|
||||
call a service, and shape the response. No business rules, no I/O.
|
||||
|
||||
- `interfaces/openapi.py` re-exports every route module so the OpenAPI-first
|
||||
app can resolve handlers.
|
||||
- Routes may mount the `cache` decorator on read endpoints.
|
||||
|
||||
### 2. `domain/` — the core
|
||||
|
||||
Pure models, **ports** (abstract contracts), and services (use cases). No
|
||||
framework imports, no drivers.
|
||||
|
||||
- `<aggregate>/models.py` — dataclasses, enums, document shapes.
|
||||
- `<aggregate>/ports/db.py` — repo contract; `<aggregate>/ports/service.py` —
|
||||
service contract.
|
||||
- Services compose ports and enforce the business rules; the composition root
|
||||
injects the implementations.
|
||||
|
||||
### 3. `infra/` — the adapters
|
||||
|
||||
Concrete implementations of the domain ports plus cross-cutting infrastructure:
|
||||
|
||||
- `db/` — mongo-ops repositories and the connection lifecycle.
|
||||
- `adapters/` — cache store, progress bus, storages, fetchers (library-backed).
|
||||
- Anything else the domain needs, implemented against its ports.
|
||||
|
||||
### 4. `pipelines/` — data-driven families *(optional)*
|
||||
|
||||
Extraction and enrichment families, each a deterministic step chain
|
||||
(`abc.py` / `impl.py` / `pipeline.yaml` / `resources/`). Roots are registered
|
||||
in the pattern registry and driven either by a worker job or a CLI.
|
||||
|
||||
### 5. `worker.py` — the job worker *(optional)*
|
||||
|
||||
A separate process that polls durable job collections, runs pipeline roots, and
|
||||
publishes progress to the progress bus. It shares the same composition root as
|
||||
the API.
|
||||
|
||||
### 6. `init.py` — the composition root
|
||||
|
||||
The only place anything is constructed: leaf repositories → dependent
|
||||
repositories → services → adapters. Exposes `get_svc` overloads and starts /
|
||||
stops resources on the app lifespan.
|
||||
|
||||
---
|
||||
|
||||
## 🔗 How the request flows
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant C as Client
|
||||
participant R as route (interfaces)
|
||||
participant S as service (domain)
|
||||
participant P as repo (infra)
|
||||
participant DB as MongoDB
|
||||
|
||||
C->>R: POST /items (operationId)
|
||||
R->>S: svc.create(payload)
|
||||
S->>P: repo.insert(doc)
|
||||
P->>DB: insert
|
||||
DB-->>P: ack
|
||||
P-->>S: doc
|
||||
S-->>R: result
|
||||
R-->>C: 200 JSON
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Related
|
||||
|
||||
- [01 – Overview](01_overview.md) · [03 – Build Recipe](03_build_recipe.md) · [04 – Testing & Quality](04_development/01_testing.md)
|
||||
124
standards/src/wiki/03_build_recipe.md
Normal file
124
standards/src/wiki/03_build_recipe.md
Normal file
@@ -0,0 +1,124 @@
|
||||
# Build Recipe
|
||||
|
||||
The ordered, deterministic checklist for generating a new application of the
|
||||
standard shape. Every step is fixed; only steps 2 and 3 need app-specific
|
||||
authoring. Give an AI agent the copyable prompt below (or read it from the
|
||||
`standards` MCP bundle as `doc://modules/build_recipe`) and it can produce a
|
||||
complete app in one pass.
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Quick start
|
||||
|
||||
```bash
|
||||
# Copy the runnable skeleton (from the standards docs checkout)
|
||||
cp -r <docs>/standards/src/templates/app apps/<your-app>
|
||||
cd apps/<your-app>
|
||||
```
|
||||
|
||||
Then follow the numbered steps in the prompt.
|
||||
|
||||
---
|
||||
|
||||
## 📋 Agent prompt (copyable)
|
||||
|
||||
````markdown
|
||||
# Build a new application from the Aetos Standards blueprint
|
||||
|
||||
You are coding a complete new backend application conforming to the Aetos
|
||||
standard app blueprint. Execute every step in order. Only steps 2 and 3 use the
|
||||
app-specific details provided by the user; everything else is deterministic.
|
||||
|
||||
Before writing any code, connect to the `standards` MCP server and read:
|
||||
- `doc://nav` (the module list)
|
||||
- `doc://modules/layering_rules` (the dependency rules)
|
||||
- `doc://modules/genericity_matrix` (what is generic vs app-specific)
|
||||
Then call `find_library` with capabilities like `db`, `di`, `routing`,
|
||||
`ingestion`, `auth`, `docs`, `pipeline`, and note the libraries that already
|
||||
exist. Never reimplement one.
|
||||
|
||||
1. **Scaffold** — copy `src/templates/app/` into `apps/<your-app>/`. Keep the
|
||||
package tree: `domain/`, `infra/`, `interfaces/`, `pipelines/`, `tests/`,
|
||||
`scripts/`, plus `pyproject.toml`, `openapi.yaml`, `config.py`, `init.py`,
|
||||
`main.py`, `worker.py`, `Dockerfile`, `docker-compose.yaml`, `.env.example`.
|
||||
|
||||
2. **Author the spec (app-specific)** — write `openapi.yaml`: resources,
|
||||
schemas, `operationId`s, security, and extension metadata. This is the
|
||||
contract and drives all routing.
|
||||
|
||||
3. **Write each CRUD slice (app-specific)** — per resource create the
|
||||
four-file slice: `models.py`, `ports/db.py`, `ports/service.py`, and
|
||||
optional `converters.py`. Follow the CRUD template.
|
||||
|
||||
4. **Wire the composition root** — in `init.py`, build leaf repositories, then
|
||||
dependent repositories, then services; add infra adapters (db, cache,
|
||||
stores, progress bus, fetchers). Expose `get_svc` overloads.
|
||||
|
||||
5. **Add routes** — create `interfaces/<resource>/routes.py` thin handlers
|
||||
bound to `operationId`s and re-export them in `interfaces/openapi.py`. Add
|
||||
the cache decorator to read endpoints.
|
||||
|
||||
6. **Bootstrap app and worker** — confirm `main.py` runs the OpenAPI-first app
|
||||
with its lifespan. If the app has long-running jobs, adapt `worker.py` to
|
||||
poll the job collection, run the pipeline, and publish progress.
|
||||
|
||||
7. **Add pipeline families (only if data-driven)** — create
|
||||
`pipelines/<family>/` with `abc.py`, `impl.py`, `pipeline.yaml`,
|
||||
`resources/`, and register the root. Add fixtures and a parity test.
|
||||
|
||||
8. **Add infrastructure only where needed** — prefer a library-backed adapter.
|
||||
Never hand-write what an internal library already provides.
|
||||
|
||||
9. **Tests** — integration tests with fakes at the HTTP edge; unit tests for
|
||||
domain and pipeline logic; a config-consistency test for rule-driven
|
||||
pipelines.
|
||||
|
||||
10. **Scripts and seeds** — operator scripts under `scripts/` using
|
||||
`init()` / `get_svc()` / `shutdown()`.
|
||||
|
||||
11. **Docs** — author GSDFC docstrings, generate `.pyi` stubs, mark the package
|
||||
`py.typed`, and build the docs.
|
||||
|
||||
**Definition of done:**
|
||||
- `python -c "import main"` succeeds.
|
||||
- `pytest` passes.
|
||||
- The app serves every operation in the spec; the worker drains its queue.
|
||||
- No domain module imports a web framework.
|
||||
- No infrastructure was reimplemented that a library already provides.
|
||||
````
|
||||
|
||||
---
|
||||
|
||||
## 🧭 Where the steps land
|
||||
|
||||
The steps map onto the layers as follows:
|
||||
|
||||
| Step | Package / file | Depends on |
|
||||
|---------------------|-----------------------------------|---------------------------------------------|
|
||||
| 1 · Scaffold | whole `templates/app/` | — |
|
||||
| 2 · Spec | `openapi.yaml` | — |
|
||||
| 3 · CRUD slices | `domain/<resource>/` | spec, CRUD template |
|
||||
| 4 · Wiring | `init.py` | every slice |
|
||||
| 5 · Routes | `interfaces/<resource>/` | services, openapi route registry |
|
||||
| 6 · Bootstrap | `main.py`, `worker.py` | composition root |
|
||||
| 7 · Pipelines | `pipelines/<family>/` | library pipeline engine |
|
||||
| 8 · Infra | `infra/` | domain ports |
|
||||
| 9 · Tests | `tests/` | app + fixtures |
|
||||
| 10 · Scripts | `scripts/` | composition root |
|
||||
| 11 · Docs | docstrings + docs build | code, Doc Forge |
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Definition of done
|
||||
|
||||
- `python -c "import main"` succeeds.
|
||||
- `pytest` passes.
|
||||
- The app serves the spec's operations; the worker drains its queue.
|
||||
- No domain module imports a web framework.
|
||||
- No infrastructure was reimplemented that a library already provides.
|
||||
|
||||
---
|
||||
|
||||
## Related
|
||||
|
||||
- [01 – Overview](01_overview.md) · [02 – App Components](02_components.md) · [04 – Testing & Quality](04_development/01_testing.md)
|
||||
@@ -1,13 +1,12 @@
|
||||
# Testing
|
||||
# Testing & Quality Gates
|
||||
|
||||
## Summary
|
||||
|
||||
Two test suites: **integration** (the HTTP edge with fakes) and **unit** (pure
|
||||
domain and pipeline logic). Both run under pytest with asyncio auto mode.
|
||||
Two suites run under pytest with asyncio auto mode: **integration** (the HTTP
|
||||
edge against fakes) and **unit** (pure domain and pipeline logic). Finish a
|
||||
blueprint app only when every gate below passes.
|
||||
|
||||
---
|
||||
|
||||
## Layout
|
||||
## 🗂️ Layout
|
||||
|
||||
```
|
||||
tests/
|
||||
@@ -16,7 +15,9 @@ tests/
|
||||
conftest.py # fixtures
|
||||
```
|
||||
|
||||
## Integration tests
|
||||
---
|
||||
|
||||
## 🚀 Integration tests
|
||||
|
||||
- Build the app with `TestClient`.
|
||||
- Replace `app.state.resources` with fake repositories/services so no database
|
||||
@@ -35,22 +36,46 @@ def test_create_item():
|
||||
assert r.status_code == 200
|
||||
```
|
||||
|
||||
## Unit tests
|
||||
---
|
||||
|
||||
## 🧪 Unit tests
|
||||
|
||||
- Domain models and converters: pure, no I/O.
|
||||
- Pipeline stages: feed a fixture state, assert the transformed state.
|
||||
- Assembled pipeline roots: **fixture parity** — replay a stored fixture input
|
||||
and compare against a golden output file.
|
||||
|
||||
## Config-consistency tests
|
||||
---
|
||||
|
||||
## 🔐 Config-consistency tests
|
||||
|
||||
For rule-driven pipelines, add a test that cross-references rules against the
|
||||
declared pattern registry: every flag emitted must be defined; every defined
|
||||
flag must be reachable. This catches drift without running the pipeline.
|
||||
|
||||
## Conventions
|
||||
---
|
||||
|
||||
## ✅ Quality gates
|
||||
|
||||
A **definition of done** for any standard app:
|
||||
|
||||
- `python -c "import main"` succeeds.
|
||||
- `pytest` passes.
|
||||
- The app serves the spec's operations; the worker drains its queue.
|
||||
- No domain module imports a web framework.
|
||||
- No infrastructure was reimplemented that a library already provides.
|
||||
|
||||
---
|
||||
|
||||
## 💡 Conventions
|
||||
|
||||
- `asyncio_mode = "auto"`; no manual event loops.
|
||||
- Fixtures are checked into the repo (inputs + golden outputs).
|
||||
- Tests skip (not fail) when a required fixture is absent.
|
||||
- Do not mock the domain; mock only the infrastructure edge.
|
||||
|
||||
---
|
||||
|
||||
## Related
|
||||
|
||||
- [02 – App Components](02_components.md) · [03 – Build Recipe](03_build_recipe.md) · [Overview](../01_overview.md)
|
||||
@@ -1,82 +0,0 @@
|
||||
# Build Recipe
|
||||
|
||||
## Summary
|
||||
|
||||
Follow this ordered checklist to generate a new application of the standard
|
||||
shape. Every step is deterministic; only steps 2 and 7 require app-specific
|
||||
authoring.
|
||||
|
||||
---
|
||||
|
||||
## 0. Query the registry
|
||||
|
||||
Before writing anything, read the **library registry** and note which internal
|
||||
libraries provide DB, DI, routing, ingestion, auth, and docs. Do not
|
||||
reimplement them.
|
||||
|
||||
## 1. Scaffold the repository
|
||||
|
||||
Copy the `templates/` skeleton. It provides: `pyproject.toml`, `openapi.yaml`,
|
||||
`main.py`, `init.py`, `worker.py`, `config.py`, `Dockerfile`,
|
||||
`docker-compose.yaml`, `.env.example`, and the package tree
|
||||
(`domain/ infra/ interfaces/ pipelines/ tests/ scripts/`).
|
||||
|
||||
## 2. Author the spec *(app-specific)*
|
||||
|
||||
Write `openapi.yaml`: resources, schemas, `operationId`s, security, and any
|
||||
extension metadata. This is the contract.
|
||||
|
||||
## 3. Write each CRUD slice *(app-specific)*
|
||||
|
||||
For each resource, create the four-file slice (see **CRUD Template**):
|
||||
`models.py`, `ports/db.py`, `ports/service.py`, optional `converters.py`.
|
||||
|
||||
## 4. Wire the composition root
|
||||
|
||||
In `init.py`: build leaf repositories, then dependent repositories, then
|
||||
services; add infrastructure adaptors (cache store, progress bus, stores,
|
||||
fetchers). Add `get_svc` overloads.
|
||||
|
||||
## 5. Add routes
|
||||
|
||||
Create `interfaces/<resource>/routes.py` (thin handlers) and re-export them in
|
||||
`interfaces/openapi.py`. Add a cache decorator to read endpoints.
|
||||
|
||||
## 6. Bootstrap the app and worker
|
||||
|
||||
Confirm `main.py` wires the OpenAPI-first app with the lifespan. If the app has
|
||||
long-running jobs, adapt `worker.py` to poll the relevant job collection.
|
||||
|
||||
## 7. Add a pipeline family *(only if data-driven)*
|
||||
|
||||
Create `pipelines/<family>/` with `abc.py`, `impl.py`, `pipeline.yaml`,
|
||||
`resources/`, and register the root in `registry.py`. Add fixtures and a parity
|
||||
test.
|
||||
|
||||
## 8. Add infrastructure as needed
|
||||
|
||||
Only where the standard adaptors do not cover the need. Prefer library-backed
|
||||
adapters.
|
||||
|
||||
## 9. Tests
|
||||
|
||||
Add integration tests with fakes and unit tests for domain/pipeline logic; add
|
||||
a config-consistency test if using rule-driven pipelines.
|
||||
|
||||
## 10. Scripts and seeds
|
||||
|
||||
Add operator scripts under `scripts/` that use `init()` / `get_svc()` /
|
||||
`shutdown()`.
|
||||
|
||||
## 11. Docs
|
||||
|
||||
Author GSDFC docstrings, generate the type stubs (`.pyi`), mark the package
|
||||
`py.typed`, and build docs.
|
||||
|
||||
## Definition of done
|
||||
|
||||
- `python -c "import main"` succeeds.
|
||||
- `pytest` passes.
|
||||
- The app serves the spec's operations; the worker drains its queue.
|
||||
- No domain module imports a web framework.
|
||||
- No infrastructure was reimplemented that a library already provides.
|
||||
@@ -1,45 +1,68 @@
|
||||
# The Aetos Standard App Blueprint
|
||||
# 🧩 Standards — the Aetos App Blueprint
|
||||
|
||||
A single, reusable architecture for backend applications in the Aetos ecosystem.
|
||||
One reusable architecture for every backend application in the Aetos ecosystem:
|
||||
a spec-first HTTP edge, a pure domain core, a pluggable infrastructure layer,
|
||||
and a single composition root.
|
||||
|
||||
Every application built with this blueprint has the same shape:
|
||||
Fill in **two axes only** and a new app is done: your **resource model**, and
|
||||
(if needed) your **pipeline family**. The blueprint itself never names a
|
||||
business domain — expenses, health metrics, inventory, and analytics are all
|
||||
the same shape underneath.
|
||||
|
||||
- a **spec-first HTTP layer** driven by an OpenAPI document;
|
||||
- a **domain layer** of pure models, ports, and services;
|
||||
- an **infrastructure layer** of adapters (MongoDB, cache, workers, LLM, mail);
|
||||
- a **composition root** that wires everything in exactly one place;
|
||||
- an optional **pipeline layer** for data-driven extraction/enrichment;
|
||||
- an optional **background worker** that runs long-running jobs and reports progress.
|
||||
> **Doc model:** this wiki is written for humans — how-to guides, the build
|
||||
> recipe, and quality gates. The enforceable rules (layering, composition root,
|
||||
> CRUD template, pipeline scaffold) and the library registry live in the
|
||||
> **reference** pages, mirrored by the machine-readable `standards` MCP bundle.
|
||||
> Query `find_library` before writing any infrastructure.
|
||||
|
||||
> This blueprints is deliberately **app-agnostic**. It never names a concrete
|
||||
> business domain. Any application — expenses, health metrics, inventory,
|
||||
> analytics — is produced by filling in **two axes only**: the resource model
|
||||
> and (if needed) the pipeline family.
|
||||
---
|
||||
|
||||
## How to read this
|
||||
## 🚀 Key Features
|
||||
|
||||
| Page | What it covers |
|
||||
|---|---|
|
||||
| Overview | The philosophy and the one-way dependency rule |
|
||||
| Layering Rules | Which package may import which |
|
||||
| Composition Root | The single wiring point |
|
||||
| Spec-First Routing | OpenAPI as the source of truth |
|
||||
| CRUD Template | The canonical resource slice |
|
||||
| Pipeline Scaffold | Data-driven processing families |
|
||||
| Worker & Progress | Async jobs, status machine, streaming progress |
|
||||
| Human-in-the-Loop | Pausing a job to ask a question |
|
||||
| Infra Adapters | MongoDB, cache, stores, LLM, mail |
|
||||
| Testing | Integration + unit conventions |
|
||||
| Build Recipe | The ordered checklist to generate a new app |
|
||||
| Genericity Matrix | What is reusable vs what you must write |
|
||||
- 🧭 **Spec-first routing** — the OpenAPI document, not decorator routers, is
|
||||
the source of truth (operationId lookup)
|
||||
- 🏗️ **One-way layering** — `interfaces → domain → infra`, enforced
|
||||
- 🔌 **Single composition root** — every dependency is wired in exactly one place
|
||||
- 🧩 **Generic, not app-specific** — reusable rules versus two authoring axes
|
||||
- 📚 **Library-backed** — DB, DI, pipelines, ingestion, auth, and docs come from
|
||||
internal libraries, never hand-written
|
||||
- 🚦 **Explicit state machines** — durable job statuses, progress, and
|
||||
human-in-the-loop pauses
|
||||
|
||||
## For AI agents
|
||||
---
|
||||
|
||||
This content is served as MCP resources under `docs://index`, `docs://nav`,
|
||||
and `docs://modules/{module}`. To build a new app:
|
||||
## 📁 Documentation Structure
|
||||
|
||||
1. Read `build_recipe` first.
|
||||
2. Read `genericity_matrix` to know what you must author.
|
||||
3. Query the **library registry** (`find_library`) before writing any
|
||||
infrastructure yourself.
|
||||
4. Copy the `templates/` skeleton and fill the two app-specific axes.
|
||||
| Section | Description |
|
||||
|-----------------------------------------------------|--------------------------------------------------------|
|
||||
| [Overview](01_overview.md) | Philosophy, the one-way dependency rule, architecture |
|
||||
| [App Components](02_components.md) | The component model and how the pieces wire together |
|
||||
| [Build Recipe](03_build_recipe.md) | The ordered, agentic checklist for a new app |
|
||||
| [Testing & Quality](04_development/01_testing.md) | Test layout and acceptance gates |
|
||||
|
||||
**Reference** (separate site, mirrors the MCP bundle): the enforceable
|
||||
rules — [Layering](../lib/02_layering_rules/), [Composition
|
||||
Root](../lib/03_composition_root/), [Spec-First
|
||||
Routing](../lib/04_spec_first_routing/), [CRUD
|
||||
Template](../lib/05_crud_template/), [Pipeline
|
||||
Scaffold](../lib/06_pipeline_scaffold/), [Worker &
|
||||
Progress](../lib/07_worker_and_progress/), [Human-in-the-Loop](../lib/08_human_in_loop/),
|
||||
[Infra Adapters](../lib/09_infra_adaptors/), [Genericity
|
||||
Matrix](../lib/12_genericity_matrix/), [Library
|
||||
Registry](../lib/13_library_registry/).
|
||||
|
||||
---
|
||||
|
||||
## 🔗 Related Resources
|
||||
|
||||
- **MCP bundle** — `standards` server on `:8009`: read `doc://index`,
|
||||
`doc://nav`, `doc://modules/{module}`; call `find_library(capability)`
|
||||
- **Library registry** — the catalog of internal libraries behind every app
|
||||
(`libraries.json`, served as `registry://libraries`)
|
||||
- **Template** — the runnable skeleton at `standards/src/templates/app/`
|
||||
- **Copy the repo's `AGENTS.md`-style build prompt** from
|
||||
[Build Recipe](03_build_recipe.md)
|
||||
|
||||
---
|
||||
|
||||
© Aetoskia Internal — `standards`
|
||||
@@ -14,7 +14,7 @@
|
||||
<link rel="prev" href="..">
|
||||
|
||||
|
||||
<link rel="next" href="../10_testing/">
|
||||
<link rel="next" href="../02_components/">
|
||||
|
||||
|
||||
<link rel="icon" href="../assets/images/favicon.png">
|
||||
@@ -297,18 +297,18 @@
|
||||
<ul class="md-nav__list" data-md-component="toc" data-md-scrollfix>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#summary" class="md-nav__link">
|
||||
<a href="#architecture" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
Summary
|
||||
🏗️ Architecture
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#the-three-layers" class="md-nav__link">
|
||||
<a href="#the-one-way-dependency-rule" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
The three layers
|
||||
🔄 The one-way dependency rule
|
||||
</span>
|
||||
</a>
|
||||
|
||||
@@ -317,7 +317,7 @@
|
||||
<li class="md-nav__item">
|
||||
<a href="#core-principles" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
Core principles
|
||||
🧭 Core principles
|
||||
</span>
|
||||
</a>
|
||||
|
||||
@@ -326,7 +326,16 @@
|
||||
<li class="md-nav__item">
|
||||
<a href="#runtime-processes" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
Runtime processes
|
||||
⚙️ Runtime processes
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#read-next" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
➡️ Read Next
|
||||
</span>
|
||||
</a>
|
||||
|
||||
@@ -347,12 +356,12 @@
|
||||
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="../10_testing/" class="md-nav__link">
|
||||
<a href="../02_components/" class="md-nav__link">
|
||||
|
||||
|
||||
|
||||
<span class="md-ellipsis">
|
||||
Testing
|
||||
App Components
|
||||
|
||||
</span>
|
||||
|
||||
@@ -369,7 +378,7 @@
|
||||
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="../11_build_recipe/" class="md-nav__link">
|
||||
<a href="../03_build_recipe/" class="md-nav__link">
|
||||
|
||||
|
||||
|
||||
@@ -384,6 +393,28 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="../04_development/01_testing/" class="md-nav__link">
|
||||
|
||||
|
||||
|
||||
<span class="md-ellipsis">
|
||||
Testing & Quality
|
||||
|
||||
</span>
|
||||
|
||||
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
@@ -411,18 +442,18 @@
|
||||
<ul class="md-nav__list" data-md-component="toc" data-md-scrollfix>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#summary" class="md-nav__link">
|
||||
<a href="#architecture" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
Summary
|
||||
🏗️ Architecture
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#the-three-layers" class="md-nav__link">
|
||||
<a href="#the-one-way-dependency-rule" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
The three layers
|
||||
🔄 The one-way dependency rule
|
||||
</span>
|
||||
</a>
|
||||
|
||||
@@ -431,7 +462,7 @@
|
||||
<li class="md-nav__item">
|
||||
<a href="#core-principles" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
Core principles
|
||||
🧭 Core principles
|
||||
</span>
|
||||
</a>
|
||||
|
||||
@@ -440,7 +471,16 @@
|
||||
<li class="md-nav__item">
|
||||
<a href="#runtime-processes" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
Runtime processes
|
||||
⚙️ Runtime processes
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#read-next" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
➡️ Read Next
|
||||
</span>
|
||||
</a>
|
||||
|
||||
@@ -463,45 +503,104 @@
|
||||
|
||||
|
||||
<h1 id="overview">Overview</h1>
|
||||
<h2 id="summary">Summary</h2>
|
||||
<p>The blueprint is a <strong>hexagonal (ports and adapters) application</strong> with a
|
||||
<p>The blueprint is a <strong>hexagonal (ports & adapters) application</strong> with a
|
||||
<strong>spec-first</strong> web edge and a <strong>single composition root</strong>. It favors explicit
|
||||
wiring over implicit magic, and it prefers existing internal libraries over
|
||||
hand-written infrastructure.</p>
|
||||
wiring over implicit magic and existing internal libraries over hand-written
|
||||
infrastructure. Start here to build the mental model; the enforceable details
|
||||
live in the reference pages.</p>
|
||||
<hr />
|
||||
<h2 id="the-three-layers">The three layers</h2>
|
||||
<div class="language-text highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal"><a href="#__codelineno-0-1">1</a></span>
|
||||
<span class="normal"><a href="#__codelineno-0-2">2</a></span>
|
||||
<span class="normal"><a href="#__codelineno-0-3">3</a></span></pre></div></td><td class="code"><div><pre><span></span><code><span id="__span-0-1"><a id="__codelineno-0-1" name="__codelineno-0-1"></a>interfaces/ ← HTTP edge: thin handlers bound to an OpenAPI operationId
|
||||
</span><span id="__span-0-2"><a id="__codelineno-0-2" name="__codelineno-0-2"></a>domain/ ← business core: models, ports (interfaces), services, orchestrators
|
||||
</span><span id="__span-0-3"><a id="__codelineno-0-3" name="__codelineno-0-3"></a>infra/ ← adapters: databases, caches, external clients, hardware
|
||||
</span></code></pre></div></td></tr></table></div>
|
||||
<p>Plus two optional layers:</p>
|
||||
<div class="language-text highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal"><a href="#__codelineno-1-1">1</a></span>
|
||||
<span class="normal"><a href="#__codelineno-1-2">2</a></span></pre></div></td><td class="code"><div><pre><span></span><code><span id="__span-1-1"><a id="__codelineno-1-1" name="__codelineno-1-1"></a>pipelines/ ← data-driven transformation graphs (extraction, enrichment)
|
||||
</span><span id="__span-1-2"><a id="__codelineno-1-2" name="__codelineno-1-2"></a>scripts/ ← operator scripts that reuse the composition root
|
||||
</span></code></pre></div></td></tr></table></div>
|
||||
<h2 id="core-principles">Core principles</h2>
|
||||
<h2 id="architecture">🏗️ Architecture</h2>
|
||||
<pre class="mermaid"><code>flowchart LR
|
||||
subgraph EDGE["interfaces/ — HTTP edge"]
|
||||
OAS["openapi.yaml<br/>operationId contract"]
|
||||
RT["<resource>/routes.py<br/>thin handlers"]
|
||||
end
|
||||
|
||||
subgraph CORE["domain/ — pure business core"]
|
||||
MD["models"]
|
||||
PT["ports (abstract)"]
|
||||
SV["services"]
|
||||
end
|
||||
|
||||
subgraph INFRA["infra/ — adapters"]
|
||||
RP["mongo repos (mongo-ops)"]
|
||||
AD["cache · stores · llm · mail"]
|
||||
DB[("MongoDB")]
|
||||
end
|
||||
|
||||
CR["init.py<br/>composition root"]
|
||||
|
||||
OAS --> RT
|
||||
RT --> SV
|
||||
SV --> PT
|
||||
SV --> RP
|
||||
PT -. depends on .- RP
|
||||
CR --- RT
|
||||
CR --- SV
|
||||
CR --- RP
|
||||
RP --> DB
|
||||
AD --- CR</code></pre>
|
||||
<p>The rule holds at every edge: code in an outer ring may depend on a ring closer
|
||||
to the core, never the reverse. <code>domain/</code> never imports a web framework or a
|
||||
database driver.</p>
|
||||
<blockquote>
|
||||
<p><strong>Legend</strong> — solid arrows are compile-time calls; dotted arrows are the
|
||||
port/adapter contract, resolved by the composition root at startup.</p>
|
||||
</blockquote>
|
||||
<hr />
|
||||
<h2 id="the-one-way-dependency-rule">🔄 The one-way dependency rule</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Ring</th>
|
||||
<th>May import</th>
|
||||
<th>Never imports</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>interfaces/</code></td>
|
||||
<td><code>domain</code>, framework glue (glue only)</td>
|
||||
<td>business logic, <code>infra</code> directly</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>domain/</code></td>
|
||||
<td>nothing but its own ports</td>
|
||||
<td>web frameworks, drivers, <code>infra</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>infra/</code></td>
|
||||
<td><code>domain</code> ports, drivers, libraries</td>
|
||||
<td>—</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>init.py</code></td>
|
||||
<td>everything (the wiring point)</td>
|
||||
<td>—</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr />
|
||||
<h2 id="core-principles">🧭 Core principles</h2>
|
||||
<ol>
|
||||
<li><strong>One-way dependencies.</strong> <code>interfaces -> domain -> infra</code> is allowed;
|
||||
the reverse is forbidden. The domain never imports a framework it does not
|
||||
own.</li>
|
||||
<li><strong>Ports in the domain, adapters in infra.</strong> A service depends on an
|
||||
abstract port; the composition root injects a concrete adapter.</li>
|
||||
<li><strong>One-way dependencies.</strong> <code>interfaces -> domain -> infra</code> is allowed; the
|
||||
reverse is forbidden.</li>
|
||||
<li><strong>Ports in the domain, adapters in infra.</strong> A service depends on an abstract
|
||||
port; the composition root injects a concrete adapter.</li>
|
||||
<li><strong>One composition root.</strong> Nothing constructs its own dependencies. All
|
||||
wiring lives in one module (<code>init.py</code>) and is exposed as a flat resource
|
||||
registry.</li>
|
||||
<li><strong>Spec-first.</strong> The HTTP contract is declared in an OpenAPI document. Route
|
||||
wiring lives in one module (<code>init.py</code>) exposed as a flat resource registry.</li>
|
||||
<li><strong>Spec-first.</strong> The HTTP contract is declared in <code>openapi.yaml</code>; route
|
||||
handlers are looked up by <code>operationId</code>. There are no decorator routers.</li>
|
||||
<li><strong>Reuse the libraries.</strong> Pipeline orchestration, DI, Mongo repositories,
|
||||
ingestion, auth, and docs are provided by internal libraries. Do not
|
||||
reimplement them — see the library registry.</li>
|
||||
<li><strong>Reuse the libraries.</strong> Orchestration, DI, Mongo repositories, ingestion,
|
||||
auth, and docs come from internal libraries — see the <a href="../lib/13_library_registry/">Library
|
||||
Registry</a>.</li>
|
||||
<li><strong>Explicit state machines.</strong> Long-running work moves through explicit,
|
||||
durable statuses and can pause for human input.</li>
|
||||
</ol>
|
||||
<h2 id="runtime-processes">Runtime processes</h2>
|
||||
<p>A standard app may run as up to three processes that share the same
|
||||
composition root:</p>
|
||||
<hr />
|
||||
<h2 id="runtime-processes">⚙️ Runtime processes</h2>
|
||||
<p>A standard app may run as up to three processes that share the same composition
|
||||
root:</p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -528,6 +627,11 @@ composition root:</p>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr />
|
||||
<h2 id="read-next">➡️ Read Next</h2>
|
||||
<ul>
|
||||
<li><a href="../02_components/">02 – App Components</a> · <a href="../03_build_recipe/">03 – Build Recipe</a></li>
|
||||
</ul>
|
||||
|
||||
|
||||
|
||||
@@ -589,6 +693,8 @@ composition root:</p>
|
||||
|
||||
<script src="../assets/javascripts/bundle.f55a23d4.min.js"></script>
|
||||
|
||||
<script src="https://unpkg.com/mermaid@10.9.1/dist/mermaid.min.js"></script>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -11,16 +11,18 @@
|
||||
|
||||
|
||||
|
||||
<link rel="prev" href="../10_testing/">
|
||||
<link rel="prev" href="../01_overview/">
|
||||
|
||||
|
||||
<link rel="next" href="../03_build_recipe/">
|
||||
|
||||
|
||||
<link rel="icon" href="../assets/images/favicon.png">
|
||||
<meta name="generator" content="mkdocs-1.6.1, mkdocs-material-9.6.23">
|
||||
|
||||
|
||||
|
||||
<title>Build Recipe - Aetos Standards — Wiki</title>
|
||||
<title>App Components - Aetos Standards — Wiki</title>
|
||||
|
||||
|
||||
|
||||
@@ -72,7 +74,7 @@
|
||||
<div data-md-component="skip">
|
||||
|
||||
|
||||
<a href="#build-recipe" class="md-skip">
|
||||
<a href="#app-components" class="md-skip">
|
||||
Skip to content
|
||||
</a>
|
||||
|
||||
@@ -108,7 +110,7 @@
|
||||
<div class="md-header__topic" data-md-component="header-topic">
|
||||
<span class="md-ellipsis">
|
||||
|
||||
Build Recipe
|
||||
App Components
|
||||
|
||||
</span>
|
||||
</div>
|
||||
@@ -264,28 +266,6 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="../10_testing/" class="md-nav__link">
|
||||
|
||||
|
||||
|
||||
<span class="md-ellipsis">
|
||||
Testing
|
||||
|
||||
</span>
|
||||
|
||||
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -303,7 +283,7 @@
|
||||
|
||||
|
||||
<span class="md-ellipsis">
|
||||
Build Recipe
|
||||
App Components
|
||||
|
||||
</span>
|
||||
|
||||
@@ -316,7 +296,7 @@
|
||||
|
||||
|
||||
<span class="md-ellipsis">
|
||||
Build Recipe
|
||||
App Components
|
||||
|
||||
</span>
|
||||
|
||||
@@ -339,126 +319,96 @@
|
||||
<ul class="md-nav__list" data-md-component="toc" data-md-scrollfix>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#summary" class="md-nav__link">
|
||||
<a href="#component-model" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
Summary
|
||||
🗂️ Component model
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#0-query-the-registry" class="md-nav__link">
|
||||
<a href="#component-roles" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
0. Query the registry
|
||||
📦 Component roles
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<nav class="md-nav" aria-label="📦 Component roles">
|
||||
<ul class="md-nav__list">
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#1-interfaces-the-edge" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
1. interfaces/ — the edge
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#2-domain-the-core" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
2. domain/ — the core
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#3-infra-the-adapters" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
3. infra/ — the adapters
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#4-pipelines-data-driven-families-optional" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
4. pipelines/ — data-driven families (optional)
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#5-workerpy-the-job-worker-optional" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
5. worker.py — the job worker (optional)
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#6-initpy-the-composition-root" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
6. init.py — the composition root
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#how-the-request-flows" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
🔗 How the request flows
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#1-scaffold-the-repository" class="md-nav__link">
|
||||
<a href="#related" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
1. Scaffold the repository
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#2-author-the-spec-app-specific" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
2. Author the spec (app-specific)
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#3-write-each-crud-slice-app-specific" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
3. Write each CRUD slice (app-specific)
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#4-wire-the-composition-root" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
4. Wire the composition root
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#5-add-routes" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
5. Add routes
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#6-bootstrap-the-app-and-worker" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
6. Bootstrap the app and worker
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#7-add-a-pipeline-family-only-if-data-driven" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
7. Add a pipeline family (only if data-driven)
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#8-add-infrastructure-as-needed" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
8. Add infrastructure as needed
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#9-tests" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
9. Tests
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#10-scripts-and-seeds" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
10. Scripts and seeds
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#11-docs" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
11. Docs
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#definition-of-done" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
Definition of done
|
||||
Related
|
||||
</span>
|
||||
</a>
|
||||
|
||||
@@ -472,6 +422,50 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="../03_build_recipe/" class="md-nav__link">
|
||||
|
||||
|
||||
|
||||
<span class="md-ellipsis">
|
||||
Build Recipe
|
||||
|
||||
</span>
|
||||
|
||||
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="../04_development/01_testing/" class="md-nav__link">
|
||||
|
||||
|
||||
|
||||
<span class="md-ellipsis">
|
||||
Testing & Quality
|
||||
|
||||
</span>
|
||||
|
||||
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
@@ -499,126 +493,96 @@
|
||||
<ul class="md-nav__list" data-md-component="toc" data-md-scrollfix>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#summary" class="md-nav__link">
|
||||
<a href="#component-model" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
Summary
|
||||
🗂️ Component model
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#0-query-the-registry" class="md-nav__link">
|
||||
<a href="#component-roles" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
0. Query the registry
|
||||
📦 Component roles
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<nav class="md-nav" aria-label="📦 Component roles">
|
||||
<ul class="md-nav__list">
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#1-interfaces-the-edge" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
1. interfaces/ — the edge
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#2-domain-the-core" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
2. domain/ — the core
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#3-infra-the-adapters" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
3. infra/ — the adapters
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#4-pipelines-data-driven-families-optional" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
4. pipelines/ — data-driven families (optional)
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#5-workerpy-the-job-worker-optional" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
5. worker.py — the job worker (optional)
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#6-initpy-the-composition-root" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
6. init.py — the composition root
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#how-the-request-flows" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
🔗 How the request flows
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#1-scaffold-the-repository" class="md-nav__link">
|
||||
<a href="#related" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
1. Scaffold the repository
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#2-author-the-spec-app-specific" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
2. Author the spec (app-specific)
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#3-write-each-crud-slice-app-specific" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
3. Write each CRUD slice (app-specific)
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#4-wire-the-composition-root" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
4. Wire the composition root
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#5-add-routes" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
5. Add routes
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#6-bootstrap-the-app-and-worker" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
6. Bootstrap the app and worker
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#7-add-a-pipeline-family-only-if-data-driven" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
7. Add a pipeline family (only if data-driven)
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#8-add-infrastructure-as-needed" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
8. Add infrastructure as needed
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#9-tests" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
9. Tests
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#10-scripts-and-seeds" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
10. Scripts and seeds
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#11-docs" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
11. Docs
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#definition-of-done" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
Definition of done
|
||||
Related
|
||||
</span>
|
||||
</a>
|
||||
|
||||
@@ -640,60 +604,120 @@
|
||||
|
||||
|
||||
|
||||
<h1 id="build-recipe">Build Recipe</h1>
|
||||
<h2 id="summary">Summary</h2>
|
||||
<p>Follow this ordered checklist to generate a new application of the standard
|
||||
shape. Every step is deterministic; only steps 2 and 7 require app-specific
|
||||
authoring.</p>
|
||||
<h1 id="app-components">App Components</h1>
|
||||
<p>What ships in every standard app, and how the pieces relate. This is the
|
||||
blueprint's component model — <code>templates/app/</code> is a runnable instance of it.</p>
|
||||
<hr />
|
||||
<h2 id="0-query-the-registry">0. Query the registry</h2>
|
||||
<p>Before writing anything, read the <strong>library registry</strong> and note which internal
|
||||
libraries provide DB, DI, routing, ingestion, auth, and docs. Do not
|
||||
reimplement them.</p>
|
||||
<h2 id="1-scaffold-the-repository">1. Scaffold the repository</h2>
|
||||
<p>Copy the <code>templates/</code> skeleton. It provides: <code>pyproject.toml</code>, <code>openapi.yaml</code>,
|
||||
<code>main.py</code>, <code>init.py</code>, <code>worker.py</code>, <code>config.py</code>, <code>Dockerfile</code>,
|
||||
<code>docker-compose.yaml</code>, <code>.env.example</code>, and the package tree
|
||||
(<code>domain/ infra/ interfaces/ pipelines/ tests/ scripts/</code>).</p>
|
||||
<h2 id="2-author-the-spec-app-specific">2. Author the spec <em>(app-specific)</em></h2>
|
||||
<p>Write <code>openapi.yaml</code>: resources, schemas, <code>operationId</code>s, security, and any
|
||||
extension metadata. This is the contract.</p>
|
||||
<h2 id="3-write-each-crud-slice-app-specific">3. Write each CRUD slice <em>(app-specific)</em></h2>
|
||||
<p>For each resource, create the four-file slice (see <strong>CRUD Template</strong>):
|
||||
<code>models.py</code>, <code>ports/db.py</code>, <code>ports/service.py</code>, optional <code>converters.py</code>.</p>
|
||||
<h2 id="4-wire-the-composition-root">4. Wire the composition root</h2>
|
||||
<p>In <code>init.py</code>: build leaf repositories, then dependent repositories, then
|
||||
services; add infrastructure adaptors (cache store, progress bus, stores,
|
||||
fetchers). Add <code>get_svc</code> overloads.</p>
|
||||
<h2 id="5-add-routes">5. Add routes</h2>
|
||||
<p>Create <code>interfaces/<resource>/routes.py</code> (thin handlers) and re-export them in
|
||||
<code>interfaces/openapi.py</code>. Add a cache decorator to read endpoints.</p>
|
||||
<h2 id="6-bootstrap-the-app-and-worker">6. Bootstrap the app and worker</h2>
|
||||
<p>Confirm <code>main.py</code> wires the OpenAPI-first app with the lifespan. If the app has
|
||||
long-running jobs, adapt <code>worker.py</code> to poll the relevant job collection.</p>
|
||||
<h2 id="7-add-a-pipeline-family-only-if-data-driven">7. Add a pipeline family <em>(only if data-driven)</em></h2>
|
||||
<p>Create <code>pipelines/<family>/</code> with <code>abc.py</code>, <code>impl.py</code>, <code>pipeline.yaml</code>,
|
||||
<code>resources/</code>, and register the root in <code>registry.py</code>. Add fixtures and a parity
|
||||
test.</p>
|
||||
<h2 id="8-add-infrastructure-as-needed">8. Add infrastructure as needed</h2>
|
||||
<p>Only where the standard adaptors do not cover the need. Prefer library-backed
|
||||
adapters.</p>
|
||||
<h2 id="9-tests">9. Tests</h2>
|
||||
<p>Add integration tests with fakes and unit tests for domain/pipeline logic; add
|
||||
a config-consistency test if using rule-driven pipelines.</p>
|
||||
<h2 id="10-scripts-and-seeds">10. Scripts and seeds</h2>
|
||||
<p>Add operator scripts under <code>scripts/</code> that use <code>init()</code> / <code>get_svc()</code> /
|
||||
<code>shutdown()</code>.</p>
|
||||
<h2 id="11-docs">11. Docs</h2>
|
||||
<p>Author GSDFC docstrings, generate the type stubs (<code>.pyi</code>), mark the package
|
||||
<code>py.typed</code>, and build docs.</p>
|
||||
<h2 id="definition-of-done">Definition of done</h2>
|
||||
<h2 id="component-model">🗂️ Component model</h2>
|
||||
<pre class="mermaid"><code>flowchart LR
|
||||
subgraph EDGE["interfaces/ — HTTP edge"]
|
||||
OAS["openapi.yaml"]
|
||||
RU["resource routes"]
|
||||
JU["job routes"]
|
||||
end
|
||||
|
||||
subgraph CORE["domain/ — business core"]
|
||||
RI["<resource> · models · ports · services"]
|
||||
JI["<job/worker> · models · ports · services"]
|
||||
PI["pipelines/<family>"]
|
||||
end
|
||||
|
||||
subgraph INFRA["infra/ — adapters"]
|
||||
DB[("MongoDB repos")]
|
||||
CA["redis cache"]
|
||||
ST["stores · progress bus"]
|
||||
LL["llm clients"]
|
||||
end
|
||||
|
||||
W["worker.py"]
|
||||
CR["init.py — composition root"]
|
||||
|
||||
OAS --> RU
|
||||
OAS --> JU
|
||||
RU --> RI
|
||||
JU --> JI
|
||||
JI --> PI
|
||||
PI --> DB
|
||||
PI --> LL
|
||||
RI --> CA
|
||||
RI --> DB
|
||||
JI --> ST
|
||||
W --> JI
|
||||
W --> PI
|
||||
|
||||
CR === RU
|
||||
CR === JU
|
||||
CR === RI
|
||||
CR === JI
|
||||
CR === PI
|
||||
CR === W
|
||||
CR === DB
|
||||
CR === CA
|
||||
CR === ST</code></pre>
|
||||
<blockquote>
|
||||
<p><strong>Legend</strong> — solid arrows are direct calls; thick links are wires created by
|
||||
the composition root. Every component is injected, never self-constructed.</p>
|
||||
</blockquote>
|
||||
<hr />
|
||||
<h2 id="component-roles">📦 Component roles</h2>
|
||||
<h3 id="1-interfaces-the-edge">1. <code>interfaces/</code> — the edge</h3>
|
||||
<p>Thin handlers bound to <code>openapi.yaml</code> <code>operationId</code>s. They parse the request,
|
||||
call a service, and shape the response. No business rules, no I/O.</p>
|
||||
<ul>
|
||||
<li><code>python -c "import main"</code> succeeds.</li>
|
||||
<li><code>pytest</code> passes.</li>
|
||||
<li>The app serves the spec's operations; the worker drains its queue.</li>
|
||||
<li>No domain module imports a web framework.</li>
|
||||
<li>No infrastructure was reimplemented that a library already provides.</li>
|
||||
<li><code>interfaces/openapi.py</code> re-exports every route module so the OpenAPI-first
|
||||
app can resolve handlers.</li>
|
||||
<li>Routes may mount the <code>cache</code> decorator on read endpoints.</li>
|
||||
</ul>
|
||||
<h3 id="2-domain-the-core">2. <code>domain/</code> — the core</h3>
|
||||
<p>Pure models, <strong>ports</strong> (abstract contracts), and services (use cases). No
|
||||
framework imports, no drivers.</p>
|
||||
<ul>
|
||||
<li><code><aggregate>/models.py</code> — dataclasses, enums, document shapes.</li>
|
||||
<li><code><aggregate>/ports/db.py</code> — repo contract; <code><aggregate>/ports/service.py</code> —
|
||||
service contract.</li>
|
||||
<li>Services compose ports and enforce the business rules; the composition root
|
||||
injects the implementations.</li>
|
||||
</ul>
|
||||
<h3 id="3-infra-the-adapters">3. <code>infra/</code> — the adapters</h3>
|
||||
<p>Concrete implementations of the domain ports plus cross-cutting infrastructure:</p>
|
||||
<ul>
|
||||
<li><code>db/</code> — mongo-ops repositories and the connection lifecycle.</li>
|
||||
<li><code>adapters/</code> — cache store, progress bus, storages, fetchers (library-backed).</li>
|
||||
<li>Anything else the domain needs, implemented against its ports.</li>
|
||||
</ul>
|
||||
<h3 id="4-pipelines-data-driven-families-optional">4. <code>pipelines/</code> — data-driven families <em>(optional)</em></h3>
|
||||
<p>Extraction and enrichment families, each a deterministic step chain
|
||||
(<code>abc.py</code> / <code>impl.py</code> / <code>pipeline.yaml</code> / <code>resources/</code>). Roots are registered
|
||||
in the pattern registry and driven either by a worker job or a CLI.</p>
|
||||
<h3 id="5-workerpy-the-job-worker-optional">5. <code>worker.py</code> — the job worker <em>(optional)</em></h3>
|
||||
<p>A separate process that polls durable job collections, runs pipeline roots, and
|
||||
publishes progress to the progress bus. It shares the same composition root as
|
||||
the API.</p>
|
||||
<h3 id="6-initpy-the-composition-root">6. <code>init.py</code> — the composition root</h3>
|
||||
<p>The only place anything is constructed: leaf repositories → dependent
|
||||
repositories → services → adapters. Exposes <code>get_svc</code> overloads and starts /
|
||||
stops resources on the app lifespan.</p>
|
||||
<hr />
|
||||
<h2 id="how-the-request-flows">🔗 How the request flows</h2>
|
||||
<pre class="mermaid"><code>sequenceDiagram
|
||||
participant C as Client
|
||||
participant R as route (interfaces)
|
||||
participant S as service (domain)
|
||||
participant P as repo (infra)
|
||||
participant DB as MongoDB
|
||||
|
||||
C->>R: POST /items (operationId)
|
||||
R->>S: svc.create(payload)
|
||||
S->>P: repo.insert(doc)
|
||||
P->>DB: insert
|
||||
DB-->>P: ack
|
||||
P-->>S: doc
|
||||
S-->>R: result
|
||||
R-->>C: 200 JSON</code></pre>
|
||||
<hr />
|
||||
<h2 id="related">Related</h2>
|
||||
<ul>
|
||||
<li><a href="../01_overview/">01 – Overview</a> · <a href="../03_build_recipe/">03 – Build Recipe</a> · <a href="../04_development/01_testing/">04 – Testing & Quality</a></li>
|
||||
</ul>
|
||||
|
||||
|
||||
@@ -756,6 +780,8 @@ a config-consistency test if using rule-driven pipelines.</p>
|
||||
|
||||
<script src="../assets/javascripts/bundle.f55a23d4.min.js"></script>
|
||||
|
||||
<script src="https://unpkg.com/mermaid@10.9.1/dist/mermaid.min.js"></script>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
794
standards/wiki/03_build_recipe/index.html
Normal file
794
standards/wiki/03_build_recipe/index.html
Normal file
@@ -0,0 +1,794 @@
|
||||
|
||||
<!doctype html>
|
||||
<html lang="en" class="no-js">
|
||||
<head>
|
||||
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
|
||||
<meta name="description" content="The philosophy and prompts behind the standardized application blueprint.">
|
||||
|
||||
|
||||
|
||||
|
||||
<link rel="prev" href="../02_components/">
|
||||
|
||||
|
||||
<link rel="next" href="../04_development/01_testing/">
|
||||
|
||||
|
||||
<link rel="icon" href="../assets/images/favicon.png">
|
||||
<meta name="generator" content="mkdocs-1.6.1, mkdocs-material-9.6.23">
|
||||
|
||||
|
||||
|
||||
<title>Build Recipe - Aetos Standards — Wiki</title>
|
||||
|
||||
|
||||
|
||||
<link rel="stylesheet" href="../assets/stylesheets/main.84d31ad4.min.css">
|
||||
|
||||
|
||||
<link rel="stylesheet" href="../assets/stylesheets/palette.06af60db.min.css">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Inter:300,300i,400,400i,700,700i%7CJetBrains+Mono:400,400i,700,700i&display=fallback">
|
||||
<style>:root{--md-text-font:"Inter";--md-code-font:"JetBrains Mono"}</style>
|
||||
|
||||
|
||||
|
||||
<script>__md_scope=new URL("..",location),__md_hash=e=>[...e].reduce(((e,_)=>(e<<5)-e+_.charCodeAt(0)),0),__md_get=(e,_=localStorage,t=__md_scope)=>JSON.parse(_.getItem(t.pathname+"."+e)),__md_set=(e,_,t=localStorage,a=__md_scope)=>{try{t.setItem(a.pathname+"."+e,JSON.stringify(_))}catch(e){}}</script>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</head>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<body dir="ltr" data-md-color-scheme="slate" data-md-color-primary="deep-purple" data-md-color-accent="cyan">
|
||||
|
||||
|
||||
<input class="md-toggle" data-md-toggle="drawer" type="checkbox" id="__drawer" autocomplete="off">
|
||||
<input class="md-toggle" data-md-toggle="search" type="checkbox" id="__search" autocomplete="off">
|
||||
<label class="md-overlay" for="__drawer"></label>
|
||||
<div data-md-component="skip">
|
||||
|
||||
|
||||
<a href="#build-recipe" class="md-skip">
|
||||
Skip to content
|
||||
</a>
|
||||
|
||||
</div>
|
||||
<div data-md-component="announce">
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<header class="md-header md-header--shadow" data-md-component="header">
|
||||
<nav class="md-header__inner md-grid" aria-label="Header">
|
||||
<a href=".." title="Aetos Standards — Wiki" class="md-header__button md-logo" aria-label="Aetos Standards — Wiki" data-md-component="logo">
|
||||
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 8a3 3 0 0 0 3-3 3 3 0 0 0-3-3 3 3 0 0 0-3 3 3 3 0 0 0 3 3m0 3.54C9.64 9.35 6.5 8 3 8v11c3.5 0 6.64 1.35 9 3.54 2.36-2.19 5.5-3.54 9-3.54V8c-3.5 0-6.64 1.35-9 3.54"/></svg>
|
||||
|
||||
</a>
|
||||
<label class="md-header__button md-icon" for="__drawer">
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M3 6h18v2H3zm0 5h18v2H3zm0 5h18v2H3z"/></svg>
|
||||
</label>
|
||||
<div class="md-header__title" data-md-component="header-title">
|
||||
<div class="md-header__ellipsis">
|
||||
<div class="md-header__topic">
|
||||
<span class="md-ellipsis">
|
||||
Aetos Standards — Wiki
|
||||
</span>
|
||||
</div>
|
||||
<div class="md-header__topic" data-md-component="header-topic">
|
||||
<span class="md-ellipsis">
|
||||
|
||||
Build Recipe
|
||||
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<form class="md-header__option" data-md-component="palette">
|
||||
|
||||
|
||||
|
||||
|
||||
<input class="md-option" data-md-color-media="" data-md-color-scheme="slate" data-md-color-primary="deep-purple" data-md-color-accent="cyan" aria-hidden="true" type="radio" name="__palette" id="__palette_0">
|
||||
|
||||
|
||||
</form>
|
||||
|
||||
|
||||
|
||||
<script>var palette=__md_get("__palette");if(palette&&palette.color){if("(prefers-color-scheme)"===palette.color.media){var media=matchMedia("(prefers-color-scheme: light)"),input=document.querySelector(media.matches?"[data-md-color-media='(prefers-color-scheme: light)']":"[data-md-color-media='(prefers-color-scheme: dark)']");palette.color.media=input.getAttribute("data-md-color-media"),palette.color.scheme=input.getAttribute("data-md-color-scheme"),palette.color.primary=input.getAttribute("data-md-color-primary"),palette.color.accent=input.getAttribute("data-md-color-accent")}for(var[key,value]of Object.entries(palette.color))document.body.setAttribute("data-md-color-"+key,value)}</script>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<label class="md-header__button md-icon" for="__search">
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M9.5 3A6.5 6.5 0 0 1 16 9.5c0 1.61-.59 3.09-1.56 4.23l.27.27h.79l5 5-1.5 1.5-5-5v-.79l-.27-.27A6.52 6.52 0 0 1 9.5 16 6.5 6.5 0 0 1 3 9.5 6.5 6.5 0 0 1 9.5 3m0 2C7 5 5 7 5 9.5S7 14 9.5 14 14 12 14 9.5 12 5 9.5 5"/></svg>
|
||||
</label>
|
||||
<div class="md-search" data-md-component="search" role="dialog">
|
||||
<label class="md-search__overlay" for="__search"></label>
|
||||
<div class="md-search__inner" role="search">
|
||||
<form class="md-search__form" name="search">
|
||||
<input type="text" class="md-search__input" name="query" aria-label="Search" placeholder="Search" autocapitalize="off" autocorrect="off" autocomplete="off" spellcheck="false" data-md-component="search-query" required>
|
||||
<label class="md-search__icon md-icon" for="__search">
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M9.5 3A6.5 6.5 0 0 1 16 9.5c0 1.61-.59 3.09-1.56 4.23l.27.27h.79l5 5-1.5 1.5-5-5v-.79l-.27-.27A6.52 6.52 0 0 1 9.5 16 6.5 6.5 0 0 1 3 9.5 6.5 6.5 0 0 1 9.5 3m0 2C7 5 5 7 5 9.5S7 14 9.5 14 14 12 14 9.5 12 5 9.5 5"/></svg>
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M20 11v2H8l5.5 5.5-1.42 1.42L4.16 12l7.92-7.92L13.5 5.5 8 11z"/></svg>
|
||||
</label>
|
||||
<nav class="md-search__options" aria-label="Search">
|
||||
|
||||
<a href="javascript:void(0)" class="md-search__icon md-icon" title="Share" aria-label="Share" data-clipboard data-clipboard-text="" data-md-component="search-share" tabindex="-1">
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7c.05-.23.09-.46.09-.7s-.04-.47-.09-.7l7.05-4.11c.54.5 1.25.81 2.04.81a3 3 0 0 0 3-3 3 3 0 0 0-3-3 3 3 0 0 0-3 3c0 .24.04.47.09.7L8.04 9.81C7.5 9.31 6.79 9 6 9a3 3 0 0 0-3 3 3 3 0 0 0 3 3c.79 0 1.5-.31 2.04-.81l7.12 4.15c-.05.21-.08.43-.08.66 0 1.61 1.31 2.91 2.92 2.91s2.92-1.3 2.92-2.91A2.92 2.92 0 0 0 18 16.08"/></svg>
|
||||
</a>
|
||||
|
||||
<button type="reset" class="md-search__icon md-icon" title="Clear" aria-label="Clear" tabindex="-1">
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<div class="md-search__suggest" data-md-component="search-suggest"></div>
|
||||
|
||||
</form>
|
||||
<div class="md-search__output">
|
||||
<div class="md-search__scrollwrap" tabindex="0" data-md-scrollfix>
|
||||
<div class="md-search-result" data-md-component="search-result">
|
||||
<div class="md-search-result__meta">
|
||||
Initializing search
|
||||
</div>
|
||||
<ol class="md-search-result__list" role="presentation"></ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</nav>
|
||||
|
||||
</header>
|
||||
|
||||
<div class="md-container" data-md-component="container">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<main class="md-main" data-md-component="main">
|
||||
<div class="md-main__inner md-grid">
|
||||
|
||||
|
||||
|
||||
<div class="md-sidebar md-sidebar--primary" data-md-component="sidebar" data-md-type="navigation" >
|
||||
<div class="md-sidebar__scrollwrap">
|
||||
<div class="md-sidebar__inner">
|
||||
|
||||
|
||||
|
||||
|
||||
<nav class="md-nav md-nav--primary" aria-label="Navigation" data-md-level="0">
|
||||
<label class="md-nav__title" for="__drawer">
|
||||
<a href=".." title="Aetos Standards — Wiki" class="md-nav__button md-logo" aria-label="Aetos Standards — Wiki" data-md-component="logo">
|
||||
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 8a3 3 0 0 0 3-3 3 3 0 0 0-3-3 3 3 0 0 0-3 3 3 3 0 0 0 3 3m0 3.54C9.64 9.35 6.5 8 3 8v11c3.5 0 6.64 1.35 9 3.54 2.36-2.19 5.5-3.54 9-3.54V8c-3.5 0-6.64 1.35-9 3.54"/></svg>
|
||||
|
||||
</a>
|
||||
Aetos Standards — Wiki
|
||||
</label>
|
||||
|
||||
<ul class="md-nav__list" data-md-scrollfix>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href=".." class="md-nav__link">
|
||||
|
||||
|
||||
|
||||
<span class="md-ellipsis">
|
||||
Home
|
||||
|
||||
</span>
|
||||
|
||||
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="../01_overview/" class="md-nav__link">
|
||||
|
||||
|
||||
|
||||
<span class="md-ellipsis">
|
||||
Overview
|
||||
|
||||
</span>
|
||||
|
||||
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="../02_components/" class="md-nav__link">
|
||||
|
||||
|
||||
|
||||
<span class="md-ellipsis">
|
||||
App Components
|
||||
|
||||
</span>
|
||||
|
||||
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="md-nav__item md-nav__item--active">
|
||||
|
||||
<input class="md-nav__toggle md-toggle" type="checkbox" id="__toc">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<label class="md-nav__link md-nav__link--active" for="__toc">
|
||||
|
||||
|
||||
|
||||
<span class="md-ellipsis">
|
||||
Build Recipe
|
||||
|
||||
</span>
|
||||
|
||||
|
||||
<span class="md-nav__icon md-icon"></span>
|
||||
</label>
|
||||
|
||||
<a href="./" class="md-nav__link md-nav__link--active">
|
||||
|
||||
|
||||
|
||||
<span class="md-ellipsis">
|
||||
Build Recipe
|
||||
|
||||
</span>
|
||||
|
||||
|
||||
</a>
|
||||
|
||||
|
||||
|
||||
<nav class="md-nav md-nav--secondary" aria-label="Table of contents">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<label class="md-nav__title" for="__toc">
|
||||
<span class="md-nav__icon md-icon"></span>
|
||||
Table of contents
|
||||
</label>
|
||||
<ul class="md-nav__list" data-md-component="toc" data-md-scrollfix>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#quick-start" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
🚀 Quick start
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#agent-prompt-copyable" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
📋 Agent prompt (copyable)
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#where-the-steps-land" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
🧭 Where the steps land
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#definition-of-done" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
⚠️ Definition of done
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#related" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
Related
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
|
||||
</nav>
|
||||
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="../04_development/01_testing/" class="md-nav__link">
|
||||
|
||||
|
||||
|
||||
<span class="md-ellipsis">
|
||||
Testing & Quality
|
||||
|
||||
</span>
|
||||
|
||||
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="md-sidebar md-sidebar--secondary" data-md-component="sidebar" data-md-type="toc" >
|
||||
<div class="md-sidebar__scrollwrap">
|
||||
<div class="md-sidebar__inner">
|
||||
|
||||
|
||||
<nav class="md-nav md-nav--secondary" aria-label="Table of contents">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<label class="md-nav__title" for="__toc">
|
||||
<span class="md-nav__icon md-icon"></span>
|
||||
Table of contents
|
||||
</label>
|
||||
<ul class="md-nav__list" data-md-component="toc" data-md-scrollfix>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#quick-start" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
🚀 Quick start
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#agent-prompt-copyable" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
📋 Agent prompt (copyable)
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#where-the-steps-land" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
🧭 Where the steps land
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#definition-of-done" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
⚠️ Definition of done
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#related" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
Related
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="md-content" data-md-component="content">
|
||||
<article class="md-content__inner md-typeset">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<h1 id="build-recipe">Build Recipe</h1>
|
||||
<p>The ordered, deterministic checklist for generating a new application of the
|
||||
standard shape. Every step is fixed; only steps 2 and 3 need app-specific
|
||||
authoring. Give an AI agent the copyable prompt below (or read it from the
|
||||
<code>standards</code> MCP bundle as <code>doc://modules/build_recipe</code>) and it can produce a
|
||||
complete app in one pass.</p>
|
||||
<hr />
|
||||
<h2 id="quick-start">🚀 Quick start</h2>
|
||||
<div class="language-bash highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal"><a href="#__codelineno-0-1">1</a></span>
|
||||
<span class="normal"><a href="#__codelineno-0-2">2</a></span>
|
||||
<span class="normal"><a href="#__codelineno-0-3">3</a></span></pre></div></td><td class="code"><div><pre><span></span><code><span id="__span-0-1"><a id="__codelineno-0-1" name="__codelineno-0-1"></a><span class="c1"># Copy the runnable skeleton (from the standards docs checkout)</span>
|
||||
</span><span id="__span-0-2"><a id="__codelineno-0-2" name="__codelineno-0-2"></a>cp<span class="w"> </span>-r<span class="w"> </span><docs>/standards/src/templates/app<span class="w"> </span>apps/<your-app>
|
||||
</span><span id="__span-0-3"><a id="__codelineno-0-3" name="__codelineno-0-3"></a><span class="nb">cd</span><span class="w"> </span>apps/<your-app>
|
||||
</span></code></pre></div></td></tr></table></div>
|
||||
<p>Then follow the numbered steps in the prompt.</p>
|
||||
<hr />
|
||||
<h2 id="agent-prompt-copyable">📋 Agent prompt (copyable)</h2>
|
||||
<div class="language-markdown highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal"><a href="#__codelineno-1-1"> 1</a></span>
|
||||
<span class="normal"><a href="#__codelineno-1-2"> 2</a></span>
|
||||
<span class="normal"><a href="#__codelineno-1-3"> 3</a></span>
|
||||
<span class="normal"><a href="#__codelineno-1-4"> 4</a></span>
|
||||
<span class="normal"><a href="#__codelineno-1-5"> 5</a></span>
|
||||
<span class="normal"><a href="#__codelineno-1-6"> 6</a></span>
|
||||
<span class="normal"><a href="#__codelineno-1-7"> 7</a></span>
|
||||
<span class="normal"><a href="#__codelineno-1-8"> 8</a></span>
|
||||
<span class="normal"><a href="#__codelineno-1-9"> 9</a></span>
|
||||
<span class="normal"><a href="#__codelineno-1-10">10</a></span>
|
||||
<span class="normal"><a href="#__codelineno-1-11">11</a></span>
|
||||
<span class="normal"><a href="#__codelineno-1-12">12</a></span>
|
||||
<span class="normal"><a href="#__codelineno-1-13">13</a></span>
|
||||
<span class="normal"><a href="#__codelineno-1-14">14</a></span>
|
||||
<span class="normal"><a href="#__codelineno-1-15">15</a></span>
|
||||
<span class="normal"><a href="#__codelineno-1-16">16</a></span>
|
||||
<span class="normal"><a href="#__codelineno-1-17">17</a></span>
|
||||
<span class="normal"><a href="#__codelineno-1-18">18</a></span>
|
||||
<span class="normal"><a href="#__codelineno-1-19">19</a></span>
|
||||
<span class="normal"><a href="#__codelineno-1-20">20</a></span>
|
||||
<span class="normal"><a href="#__codelineno-1-21">21</a></span>
|
||||
<span class="normal"><a href="#__codelineno-1-22">22</a></span>
|
||||
<span class="normal"><a href="#__codelineno-1-23">23</a></span>
|
||||
<span class="normal"><a href="#__codelineno-1-24">24</a></span>
|
||||
<span class="normal"><a href="#__codelineno-1-25">25</a></span>
|
||||
<span class="normal"><a href="#__codelineno-1-26">26</a></span>
|
||||
<span class="normal"><a href="#__codelineno-1-27">27</a></span>
|
||||
<span class="normal"><a href="#__codelineno-1-28">28</a></span>
|
||||
<span class="normal"><a href="#__codelineno-1-29">29</a></span>
|
||||
<span class="normal"><a href="#__codelineno-1-30">30</a></span>
|
||||
<span class="normal"><a href="#__codelineno-1-31">31</a></span>
|
||||
<span class="normal"><a href="#__codelineno-1-32">32</a></span>
|
||||
<span class="normal"><a href="#__codelineno-1-33">33</a></span>
|
||||
<span class="normal"><a href="#__codelineno-1-34">34</a></span>
|
||||
<span class="normal"><a href="#__codelineno-1-35">35</a></span>
|
||||
<span class="normal"><a href="#__codelineno-1-36">36</a></span>
|
||||
<span class="normal"><a href="#__codelineno-1-37">37</a></span>
|
||||
<span class="normal"><a href="#__codelineno-1-38">38</a></span>
|
||||
<span class="normal"><a href="#__codelineno-1-39">39</a></span>
|
||||
<span class="normal"><a href="#__codelineno-1-40">40</a></span>
|
||||
<span class="normal"><a href="#__codelineno-1-41">41</a></span>
|
||||
<span class="normal"><a href="#__codelineno-1-42">42</a></span>
|
||||
<span class="normal"><a href="#__codelineno-1-43">43</a></span>
|
||||
<span class="normal"><a href="#__codelineno-1-44">44</a></span>
|
||||
<span class="normal"><a href="#__codelineno-1-45">45</a></span>
|
||||
<span class="normal"><a href="#__codelineno-1-46">46</a></span>
|
||||
<span class="normal"><a href="#__codelineno-1-47">47</a></span>
|
||||
<span class="normal"><a href="#__codelineno-1-48">48</a></span>
|
||||
<span class="normal"><a href="#__codelineno-1-49">49</a></span>
|
||||
<span class="normal"><a href="#__codelineno-1-50">50</a></span>
|
||||
<span class="normal"><a href="#__codelineno-1-51">51</a></span>
|
||||
<span class="normal"><a href="#__codelineno-1-52">52</a></span>
|
||||
<span class="normal"><a href="#__codelineno-1-53">53</a></span>
|
||||
<span class="normal"><a href="#__codelineno-1-54">54</a></span>
|
||||
<span class="normal"><a href="#__codelineno-1-55">55</a></span>
|
||||
<span class="normal"><a href="#__codelineno-1-56">56</a></span>
|
||||
<span class="normal"><a href="#__codelineno-1-57">57</a></span>
|
||||
<span class="normal"><a href="#__codelineno-1-58">58</a></span>
|
||||
<span class="normal"><a href="#__codelineno-1-59">59</a></span>
|
||||
<span class="normal"><a href="#__codelineno-1-60">60</a></span>
|
||||
<span class="normal"><a href="#__codelineno-1-61">61</a></span>
|
||||
<span class="normal"><a href="#__codelineno-1-62">62</a></span></pre></div></td><td class="code"><div><pre><span></span><code><span id="__span-1-1"><a id="__codelineno-1-1" name="__codelineno-1-1"></a><span class="gh"># Build a new application from the Aetos Standards blueprint</span>
|
||||
</span><span id="__span-1-2"><a id="__codelineno-1-2" name="__codelineno-1-2"></a>
|
||||
</span><span id="__span-1-3"><a id="__codelineno-1-3" name="__codelineno-1-3"></a>You are coding a complete new backend application conforming to the Aetos
|
||||
</span><span id="__span-1-4"><a id="__codelineno-1-4" name="__codelineno-1-4"></a>standard app blueprint. Execute every step in order. Only steps 2 and 3 use the
|
||||
</span><span id="__span-1-5"><a id="__codelineno-1-5" name="__codelineno-1-5"></a>app-specific details provided by the user; everything else is deterministic.
|
||||
</span><span id="__span-1-6"><a id="__codelineno-1-6" name="__codelineno-1-6"></a>
|
||||
</span><span id="__span-1-7"><a id="__codelineno-1-7" name="__codelineno-1-7"></a>Before writing any code, connect to the <span class="sb">`standards`</span> MCP server and read:
|
||||
</span><span id="__span-1-8"><a id="__codelineno-1-8" name="__codelineno-1-8"></a><span class="k">-</span><span class="w"> </span><span class="sb">`doc://nav`</span> (the module list)
|
||||
</span><span id="__span-1-9"><a id="__codelineno-1-9" name="__codelineno-1-9"></a><span class="k">-</span><span class="w"> </span><span class="sb">`doc://modules/layering_rules`</span> (the dependency rules)
|
||||
</span><span id="__span-1-10"><a id="__codelineno-1-10" name="__codelineno-1-10"></a><span class="k">-</span><span class="w"> </span><span class="sb">`doc://modules/genericity_matrix`</span> (what is generic vs app-specific)
|
||||
</span><span id="__span-1-11"><a id="__codelineno-1-11" name="__codelineno-1-11"></a>Then call <span class="sb">`find_library`</span> with capabilities like <span class="sb">`db`</span>, <span class="sb">`di`</span>, <span class="sb">`routing`</span>,
|
||||
</span><span id="__span-1-12"><a id="__codelineno-1-12" name="__codelineno-1-12"></a><span class="sb">`ingestion`</span>, <span class="sb">`auth`</span>, <span class="sb">`docs`</span>, <span class="sb">`pipeline`</span>, and note the libraries that already
|
||||
</span><span id="__span-1-13"><a id="__codelineno-1-13" name="__codelineno-1-13"></a>exist. Never reimplement one.
|
||||
</span><span id="__span-1-14"><a id="__codelineno-1-14" name="__codelineno-1-14"></a>
|
||||
</span><span id="__span-1-15"><a id="__codelineno-1-15" name="__codelineno-1-15"></a><span class="k">1.</span> <span class="gs">**Scaffold**</span> — copy <span class="sb">`src/templates/app/`</span> into <span class="sb">`apps/<your-app>/`</span>. Keep the
|
||||
</span><span id="__span-1-16"><a id="__codelineno-1-16" name="__codelineno-1-16"></a> package tree: <span class="sb">`domain/`</span>, <span class="sb">`infra/`</span>, <span class="sb">`interfaces/`</span>, <span class="sb">`pipelines/`</span>, <span class="sb">`tests/`</span>,
|
||||
</span><span id="__span-1-17"><a id="__codelineno-1-17" name="__codelineno-1-17"></a> <span class="sb">`scripts/`</span>, plus <span class="sb">`pyproject.toml`</span>, <span class="sb">`openapi.yaml`</span>, <span class="sb">`config.py`</span>, <span class="sb">`init.py`</span>,
|
||||
</span><span id="__span-1-18"><a id="__codelineno-1-18" name="__codelineno-1-18"></a> <span class="sb">`main.py`</span>, <span class="sb">`worker.py`</span>, <span class="sb">`Dockerfile`</span>, <span class="sb">`docker-compose.yaml`</span>, <span class="sb">`.env.example`</span>.
|
||||
</span><span id="__span-1-19"><a id="__codelineno-1-19" name="__codelineno-1-19"></a>
|
||||
</span><span id="__span-1-20"><a id="__codelineno-1-20" name="__codelineno-1-20"></a><span class="k">2.</span> <span class="gs">**Author the spec (app-specific)**</span> — write <span class="sb">`openapi.yaml`</span>: resources,
|
||||
</span><span id="__span-1-21"><a id="__codelineno-1-21" name="__codelineno-1-21"></a> schemas, <span class="sb">`operationId`</span>s, security, and extension metadata. This is the
|
||||
</span><span id="__span-1-22"><a id="__codelineno-1-22" name="__codelineno-1-22"></a> contract and drives all routing.
|
||||
</span><span id="__span-1-23"><a id="__codelineno-1-23" name="__codelineno-1-23"></a>
|
||||
</span><span id="__span-1-24"><a id="__codelineno-1-24" name="__codelineno-1-24"></a><span class="k">3.</span> <span class="gs">**Write each CRUD slice (app-specific)**</span> — per resource create the
|
||||
</span><span id="__span-1-25"><a id="__codelineno-1-25" name="__codelineno-1-25"></a> four-file slice: <span class="sb">`models.py`</span>, <span class="sb">`ports/db.py`</span>, <span class="sb">`ports/service.py`</span>, and
|
||||
</span><span id="__span-1-26"><a id="__codelineno-1-26" name="__codelineno-1-26"></a> optional <span class="sb">`converters.py`</span>. Follow the CRUD template.
|
||||
</span><span id="__span-1-27"><a id="__codelineno-1-27" name="__codelineno-1-27"></a>
|
||||
</span><span id="__span-1-28"><a id="__codelineno-1-28" name="__codelineno-1-28"></a><span class="k">4.</span> <span class="gs">**Wire the composition root**</span> — in <span class="sb">`init.py`</span>, build leaf repositories, then
|
||||
</span><span id="__span-1-29"><a id="__codelineno-1-29" name="__codelineno-1-29"></a> dependent repositories, then services; add infra adapters (db, cache,
|
||||
</span><span id="__span-1-30"><a id="__codelineno-1-30" name="__codelineno-1-30"></a> stores, progress bus, fetchers). Expose <span class="sb">`get_svc`</span> overloads.
|
||||
</span><span id="__span-1-31"><a id="__codelineno-1-31" name="__codelineno-1-31"></a>
|
||||
</span><span id="__span-1-32"><a id="__codelineno-1-32" name="__codelineno-1-32"></a><span class="k">5.</span> <span class="gs">**Add routes**</span> — create <span class="sb">`interfaces/<resource>/routes.py`</span> thin handlers
|
||||
</span><span id="__span-1-33"><a id="__codelineno-1-33" name="__codelineno-1-33"></a> bound to <span class="sb">`operationId`</span>s and re-export them in <span class="sb">`interfaces/openapi.py`</span>. Add
|
||||
</span><span id="__span-1-34"><a id="__codelineno-1-34" name="__codelineno-1-34"></a> the cache decorator to read endpoints.
|
||||
</span><span id="__span-1-35"><a id="__codelineno-1-35" name="__codelineno-1-35"></a>
|
||||
</span><span id="__span-1-36"><a id="__codelineno-1-36" name="__codelineno-1-36"></a><span class="k">6.</span> <span class="gs">**Bootstrap app and worker**</span> — confirm <span class="sb">`main.py`</span> runs the OpenAPI-first app
|
||||
</span><span id="__span-1-37"><a id="__codelineno-1-37" name="__codelineno-1-37"></a> with its lifespan. If the app has long-running jobs, adapt <span class="sb">`worker.py`</span> to
|
||||
</span><span id="__span-1-38"><a id="__codelineno-1-38" name="__codelineno-1-38"></a> poll the job collection, run the pipeline, and publish progress.
|
||||
</span><span id="__span-1-39"><a id="__codelineno-1-39" name="__codelineno-1-39"></a>
|
||||
</span><span id="__span-1-40"><a id="__codelineno-1-40" name="__codelineno-1-40"></a><span class="k">7.</span> <span class="gs">**Add pipeline families (only if data-driven)**</span> — create
|
||||
</span><span id="__span-1-41"><a id="__codelineno-1-41" name="__codelineno-1-41"></a> <span class="sb">`pipelines/<family>/`</span> with <span class="sb">`abc.py`</span>, <span class="sb">`impl.py`</span>, <span class="sb">`pipeline.yaml`</span>,
|
||||
</span><span id="__span-1-42"><a id="__codelineno-1-42" name="__codelineno-1-42"></a> <span class="sb">`resources/`</span>, and register the root. Add fixtures and a parity test.
|
||||
</span><span id="__span-1-43"><a id="__codelineno-1-43" name="__codelineno-1-43"></a>
|
||||
</span><span id="__span-1-44"><a id="__codelineno-1-44" name="__codelineno-1-44"></a><span class="k">8.</span> <span class="gs">**Add infrastructure only where needed**</span> — prefer a library-backed adapter.
|
||||
</span><span id="__span-1-45"><a id="__codelineno-1-45" name="__codelineno-1-45"></a> Never hand-write what an internal library already provides.
|
||||
</span><span id="__span-1-46"><a id="__codelineno-1-46" name="__codelineno-1-46"></a>
|
||||
</span><span id="__span-1-47"><a id="__codelineno-1-47" name="__codelineno-1-47"></a><span class="k">9.</span> <span class="gs">**Tests**</span> — integration tests with fakes at the HTTP edge; unit tests for
|
||||
</span><span id="__span-1-48"><a id="__codelineno-1-48" name="__codelineno-1-48"></a> domain and pipeline logic; a config-consistency test for rule-driven
|
||||
</span><span id="__span-1-49"><a id="__codelineno-1-49" name="__codelineno-1-49"></a> pipelines.
|
||||
</span><span id="__span-1-50"><a id="__codelineno-1-50" name="__codelineno-1-50"></a>
|
||||
</span><span id="__span-1-51"><a id="__codelineno-1-51" name="__codelineno-1-51"></a><span class="k">10.</span> <span class="gs">**Scripts and seeds**</span> — operator scripts under <span class="sb">`scripts/`</span> using
|
||||
</span><span id="__span-1-52"><a id="__codelineno-1-52" name="__codelineno-1-52"></a> <span class="sb">`init()`</span> / <span class="sb">`get_svc()`</span> / <span class="sb">`shutdown()`</span>.
|
||||
</span><span id="__span-1-53"><a id="__codelineno-1-53" name="__codelineno-1-53"></a>
|
||||
</span><span id="__span-1-54"><a id="__codelineno-1-54" name="__codelineno-1-54"></a><span class="k">11.</span> <span class="gs">**Docs**</span> — author GSDFC docstrings, generate <span class="sb">`.pyi`</span> stubs, mark the package
|
||||
</span><span id="__span-1-55"><a id="__codelineno-1-55" name="__codelineno-1-55"></a> <span class="sb">`py.typed`</span>, and build the docs.
|
||||
</span><span id="__span-1-56"><a id="__codelineno-1-56" name="__codelineno-1-56"></a>
|
||||
</span><span id="__span-1-57"><a id="__codelineno-1-57" name="__codelineno-1-57"></a><span class="gs">**Definition of done:**</span>
|
||||
</span><span id="__span-1-58"><a id="__codelineno-1-58" name="__codelineno-1-58"></a><span class="k">-</span><span class="w"> </span><span class="sb">`python -c "import main"`</span> succeeds.
|
||||
</span><span id="__span-1-59"><a id="__codelineno-1-59" name="__codelineno-1-59"></a><span class="k">-</span><span class="w"> </span><span class="sb">`pytest`</span> passes.
|
||||
</span><span id="__span-1-60"><a id="__codelineno-1-60" name="__codelineno-1-60"></a><span class="k">-</span><span class="w"> </span>The app serves every operation in the spec; the worker drains its queue.
|
||||
</span><span id="__span-1-61"><a id="__codelineno-1-61" name="__codelineno-1-61"></a><span class="k">-</span><span class="w"> </span>No domain module imports a web framework.
|
||||
</span><span id="__span-1-62"><a id="__codelineno-1-62" name="__codelineno-1-62"></a><span class="k">-</span><span class="w"> </span>No infrastructure was reimplemented that a library already provides.
|
||||
</span></code></pre></div></td></tr></table></div>
|
||||
<hr />
|
||||
<h2 id="where-the-steps-land">🧭 Where the steps land</h2>
|
||||
<p>The steps map onto the layers as follows:</p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Step</th>
|
||||
<th>Package / file</th>
|
||||
<th>Depends on</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>1 · Scaffold</td>
|
||||
<td>whole <code>templates/app/</code></td>
|
||||
<td>—</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>2 · Spec</td>
|
||||
<td><code>openapi.yaml</code></td>
|
||||
<td>—</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>3 · CRUD slices</td>
|
||||
<td><code>domain/<resource>/</code></td>
|
||||
<td>spec, CRUD template</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>4 · Wiring</td>
|
||||
<td><code>init.py</code></td>
|
||||
<td>every slice</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>5 · Routes</td>
|
||||
<td><code>interfaces/<resource>/</code></td>
|
||||
<td>services, openapi route registry</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>6 · Bootstrap</td>
|
||||
<td><code>main.py</code>, <code>worker.py</code></td>
|
||||
<td>composition root</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>7 · Pipelines</td>
|
||||
<td><code>pipelines/<family>/</code></td>
|
||||
<td>library pipeline engine</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>8 · Infra</td>
|
||||
<td><code>infra/</code></td>
|
||||
<td>domain ports</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>9 · Tests</td>
|
||||
<td><code>tests/</code></td>
|
||||
<td>app + fixtures</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>10 · Scripts</td>
|
||||
<td><code>scripts/</code></td>
|
||||
<td>composition root</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>11 · Docs</td>
|
||||
<td>docstrings + docs build</td>
|
||||
<td>code, Doc Forge</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr />
|
||||
<h2 id="definition-of-done">⚠️ Definition of done</h2>
|
||||
<ul>
|
||||
<li><code>python -c "import main"</code> succeeds.</li>
|
||||
<li><code>pytest</code> passes.</li>
|
||||
<li>The app serves the spec's operations; the worker drains its queue.</li>
|
||||
<li>No domain module imports a web framework.</li>
|
||||
<li>No infrastructure was reimplemented that a library already provides.</li>
|
||||
</ul>
|
||||
<hr />
|
||||
<h2 id="related">Related</h2>
|
||||
<ul>
|
||||
<li><a href="../01_overview/">01 – Overview</a> · <a href="../02_components/">02 – App Components</a> · <a href="../04_development/01_testing/">04 – Testing & Quality</a></li>
|
||||
</ul>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</article>
|
||||
</div>
|
||||
|
||||
|
||||
<script>var tabs=__md_get("__tabs");if(Array.isArray(tabs))e:for(var set of document.querySelectorAll(".tabbed-set")){var labels=set.querySelector(".tabbed-labels");for(var tab of tabs)for(var label of labels.getElementsByTagName("label"))if(label.innerText.trim()===tab){var input=document.getElementById(label.htmlFor);input.checked=!0;continue e}}</script>
|
||||
|
||||
<script>var target=document.getElementById(location.hash.slice(1));target&&target.name&&(target.checked=target.name.startsWith("__tabbed_"))</script>
|
||||
</div>
|
||||
|
||||
<button type="button" class="md-top md-icon" data-md-component="top" hidden>
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M13 20h-2V8l-5.5 5.5-1.42-1.42L12 4.16l7.92 7.92-1.42 1.42L13 8z"/></svg>
|
||||
Back to top
|
||||
</button>
|
||||
|
||||
</main>
|
||||
|
||||
<footer class="md-footer">
|
||||
|
||||
<div class="md-footer-meta md-typeset">
|
||||
<div class="md-footer-meta__inner md-grid">
|
||||
<div class="md-copyright">
|
||||
|
||||
|
||||
Made with
|
||||
<a href="https://squidfunk.github.io/mkdocs-material/" target="_blank" rel="noopener">
|
||||
Material for MkDocs
|
||||
</a>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
<div class="md-dialog" data-md-component="dialog">
|
||||
<div class="md-dialog__inner md-typeset"></div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
<script id="__config" type="application/json">{"base": "..", "features": ["navigation.sections", "navigation.expand", "navigation.top", "navigation.instant", "navigation.tracking", "navigation.indexes", "content.code.copy", "content.code.annotate", "content.tabs.link", "content.action.edit", "search.highlight", "search.share", "search.suggest"], "search": "../assets/javascripts/workers/search.973d3a69.min.js", "tags": null, "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}, "version": null}</script>
|
||||
|
||||
|
||||
<script src="../assets/javascripts/bundle.f55a23d4.min.js"></script>
|
||||
|
||||
<script src="https://unpkg.com/mermaid@10.9.1/dist/mermaid.min.js"></script>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -11,25 +11,23 @@
|
||||
|
||||
|
||||
|
||||
<link rel="prev" href="../01_overview/">
|
||||
<link rel="prev" href="../../03_build_recipe/">
|
||||
|
||||
|
||||
<link rel="next" href="../11_build_recipe/">
|
||||
|
||||
|
||||
<link rel="icon" href="../assets/images/favicon.png">
|
||||
<link rel="icon" href="../../assets/images/favicon.png">
|
||||
<meta name="generator" content="mkdocs-1.6.1, mkdocs-material-9.6.23">
|
||||
|
||||
|
||||
|
||||
<title>Testing - Aetos Standards — Wiki</title>
|
||||
<title>Testing & Quality - Aetos Standards — Wiki</title>
|
||||
|
||||
|
||||
|
||||
<link rel="stylesheet" href="../assets/stylesheets/main.84d31ad4.min.css">
|
||||
<link rel="stylesheet" href="../../assets/stylesheets/main.84d31ad4.min.css">
|
||||
|
||||
|
||||
<link rel="stylesheet" href="../assets/stylesheets/palette.06af60db.min.css">
|
||||
<link rel="stylesheet" href="../../assets/stylesheets/palette.06af60db.min.css">
|
||||
|
||||
|
||||
|
||||
@@ -48,7 +46,7 @@
|
||||
|
||||
|
||||
|
||||
<script>__md_scope=new URL("..",location),__md_hash=e=>[...e].reduce(((e,_)=>(e<<5)-e+_.charCodeAt(0)),0),__md_get=(e,_=localStorage,t=__md_scope)=>JSON.parse(_.getItem(t.pathname+"."+e)),__md_set=(e,_,t=localStorage,a=__md_scope)=>{try{t.setItem(a.pathname+"."+e,JSON.stringify(_))}catch(e){}}</script>
|
||||
<script>__md_scope=new URL("../..",location),__md_hash=e=>[...e].reduce(((e,_)=>(e<<5)-e+_.charCodeAt(0)),0),__md_get=(e,_=localStorage,t=__md_scope)=>JSON.parse(_.getItem(t.pathname+"."+e)),__md_set=(e,_,t=localStorage,a=__md_scope)=>{try{t.setItem(a.pathname+"."+e,JSON.stringify(_))}catch(e){}}</script>
|
||||
|
||||
|
||||
|
||||
@@ -74,7 +72,7 @@
|
||||
<div data-md-component="skip">
|
||||
|
||||
|
||||
<a href="#testing" class="md-skip">
|
||||
<a href="#testing-quality-gates" class="md-skip">
|
||||
Skip to content
|
||||
</a>
|
||||
|
||||
@@ -90,7 +88,7 @@
|
||||
|
||||
<header class="md-header md-header--shadow" data-md-component="header">
|
||||
<nav class="md-header__inner md-grid" aria-label="Header">
|
||||
<a href=".." title="Aetos Standards — Wiki" class="md-header__button md-logo" aria-label="Aetos Standards — Wiki" data-md-component="logo">
|
||||
<a href="../.." title="Aetos Standards — Wiki" class="md-header__button md-logo" aria-label="Aetos Standards — Wiki" data-md-component="logo">
|
||||
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 8a3 3 0 0 0 3-3 3 3 0 0 0-3-3 3 3 0 0 0-3 3 3 3 0 0 0 3 3m0 3.54C9.64 9.35 6.5 8 3 8v11c3.5 0 6.64 1.35 9 3.54 2.36-2.19 5.5-3.54 9-3.54V8c-3.5 0-6.64 1.35-9 3.54"/></svg>
|
||||
@@ -110,7 +108,7 @@
|
||||
<div class="md-header__topic" data-md-component="header-topic">
|
||||
<span class="md-ellipsis">
|
||||
|
||||
Testing
|
||||
Testing & Quality
|
||||
|
||||
</span>
|
||||
</div>
|
||||
@@ -207,7 +205,7 @@
|
||||
|
||||
<nav class="md-nav md-nav--primary" aria-label="Navigation" data-md-level="0">
|
||||
<label class="md-nav__title" for="__drawer">
|
||||
<a href=".." title="Aetos Standards — Wiki" class="md-nav__button md-logo" aria-label="Aetos Standards — Wiki" data-md-component="logo">
|
||||
<a href="../.." title="Aetos Standards — Wiki" class="md-nav__button md-logo" aria-label="Aetos Standards — Wiki" data-md-component="logo">
|
||||
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 8a3 3 0 0 0 3-3 3 3 0 0 0-3-3 3 3 0 0 0-3 3 3 3 0 0 0 3 3m0 3.54C9.64 9.35 6.5 8 3 8v11c3.5 0 6.64 1.35 9 3.54 2.36-2.19 5.5-3.54 9-3.54V8c-3.5 0-6.64 1.35-9 3.54"/></svg>
|
||||
@@ -225,7 +223,7 @@
|
||||
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href=".." class="md-nav__link">
|
||||
<a href="../.." class="md-nav__link">
|
||||
|
||||
|
||||
|
||||
@@ -247,7 +245,7 @@
|
||||
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="../01_overview/" class="md-nav__link">
|
||||
<a href="../../01_overview/" class="md-nav__link">
|
||||
|
||||
|
||||
|
||||
@@ -266,6 +264,50 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="../../02_components/" class="md-nav__link">
|
||||
|
||||
|
||||
|
||||
<span class="md-ellipsis">
|
||||
App Components
|
||||
|
||||
</span>
|
||||
|
||||
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="../../03_build_recipe/" class="md-nav__link">
|
||||
|
||||
|
||||
|
||||
<span class="md-ellipsis">
|
||||
Build Recipe
|
||||
|
||||
</span>
|
||||
|
||||
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -283,7 +325,7 @@
|
||||
|
||||
|
||||
<span class="md-ellipsis">
|
||||
Testing
|
||||
Testing & Quality
|
||||
|
||||
</span>
|
||||
|
||||
@@ -296,7 +338,7 @@
|
||||
|
||||
|
||||
<span class="md-ellipsis">
|
||||
Testing
|
||||
Testing & Quality
|
||||
|
||||
</span>
|
||||
|
||||
@@ -318,19 +360,10 @@
|
||||
</label>
|
||||
<ul class="md-nav__list" data-md-component="toc" data-md-scrollfix>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#summary" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
Summary
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#layout" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
Layout
|
||||
🗂️ Layout
|
||||
</span>
|
||||
</a>
|
||||
|
||||
@@ -339,7 +372,7 @@
|
||||
<li class="md-nav__item">
|
||||
<a href="#integration-tests" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
Integration tests
|
||||
🚀 Integration tests
|
||||
</span>
|
||||
</a>
|
||||
|
||||
@@ -348,7 +381,7 @@
|
||||
<li class="md-nav__item">
|
||||
<a href="#unit-tests" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
Unit tests
|
||||
🧪 Unit tests
|
||||
</span>
|
||||
</a>
|
||||
|
||||
@@ -357,7 +390,16 @@
|
||||
<li class="md-nav__item">
|
||||
<a href="#config-consistency-tests" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
Config-consistency tests
|
||||
🔐 Config-consistency tests
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#quality-gates" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
✅ Quality gates
|
||||
</span>
|
||||
</a>
|
||||
|
||||
@@ -366,7 +408,16 @@
|
||||
<li class="md-nav__item">
|
||||
<a href="#conventions" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
Conventions
|
||||
💡 Conventions
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#related" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
Related
|
||||
</span>
|
||||
</a>
|
||||
|
||||
@@ -380,28 +431,6 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="../11_build_recipe/" class="md-nav__link">
|
||||
|
||||
|
||||
|
||||
<span class="md-ellipsis">
|
||||
Build Recipe
|
||||
|
||||
</span>
|
||||
|
||||
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
@@ -428,19 +457,10 @@
|
||||
</label>
|
||||
<ul class="md-nav__list" data-md-component="toc" data-md-scrollfix>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#summary" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
Summary
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#layout" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
Layout
|
||||
🗂️ Layout
|
||||
</span>
|
||||
</a>
|
||||
|
||||
@@ -449,7 +469,7 @@
|
||||
<li class="md-nav__item">
|
||||
<a href="#integration-tests" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
Integration tests
|
||||
🚀 Integration tests
|
||||
</span>
|
||||
</a>
|
||||
|
||||
@@ -458,7 +478,7 @@
|
||||
<li class="md-nav__item">
|
||||
<a href="#unit-tests" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
Unit tests
|
||||
🧪 Unit tests
|
||||
</span>
|
||||
</a>
|
||||
|
||||
@@ -467,7 +487,16 @@
|
||||
<li class="md-nav__item">
|
||||
<a href="#config-consistency-tests" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
Config-consistency tests
|
||||
🔐 Config-consistency tests
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#quality-gates" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
✅ Quality gates
|
||||
</span>
|
||||
</a>
|
||||
|
||||
@@ -476,7 +505,16 @@
|
||||
<li class="md-nav__item">
|
||||
<a href="#conventions" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
Conventions
|
||||
💡 Conventions
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#related" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
Related
|
||||
</span>
|
||||
</a>
|
||||
|
||||
@@ -498,12 +536,12 @@
|
||||
|
||||
|
||||
|
||||
<h1 id="testing">Testing</h1>
|
||||
<h2 id="summary">Summary</h2>
|
||||
<p>Two test suites: <strong>integration</strong> (the HTTP edge with fakes) and <strong>unit</strong> (pure
|
||||
domain and pipeline logic). Both run under pytest with asyncio auto mode.</p>
|
||||
<h1 id="testing-quality-gates">Testing & Quality Gates</h1>
|
||||
<p>Two suites run under pytest with asyncio auto mode: <strong>integration</strong> (the HTTP
|
||||
edge against fakes) and <strong>unit</strong> (pure domain and pipeline logic). Finish a
|
||||
blueprint app only when every gate below passes.</p>
|
||||
<hr />
|
||||
<h2 id="layout">Layout</h2>
|
||||
<h2 id="layout">🗂️ Layout</h2>
|
||||
<div class="language-text highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal"><a href="#__codelineno-0-1">1</a></span>
|
||||
<span class="normal"><a href="#__codelineno-0-2">2</a></span>
|
||||
<span class="normal"><a href="#__codelineno-0-3">3</a></span>
|
||||
@@ -512,7 +550,8 @@ domain and pipeline logic). Both run under pytest with asyncio auto mode.</p>
|
||||
</span><span id="__span-0-3"><a id="__codelineno-0-3" name="__codelineno-0-3"></a> unit/ # models, converters, pipeline stages/roots
|
||||
</span><span id="__span-0-4"><a id="__codelineno-0-4" name="__codelineno-0-4"></a> conftest.py # fixtures
|
||||
</span></code></pre></div></td></tr></table></div>
|
||||
<h2 id="integration-tests">Integration tests</h2>
|
||||
<hr />
|
||||
<h2 id="integration-tests">🚀 Integration tests</h2>
|
||||
<ul>
|
||||
<li>Build the app with <code>TestClient</code>.</li>
|
||||
<li>Replace <code>app.state.resources</code> with fake repositories/services so no database
|
||||
@@ -534,24 +573,42 @@ domain and pipeline logic). Both run under pytest with asyncio auto mode.</p>
|
||||
</span><span id="__span-1-5"><a id="__codelineno-1-5" name="__codelineno-1-5"></a> <span class="n">r</span> <span class="o">=</span> <span class="n">client</span><span class="o">.</span><span class="n">post</span><span class="p">(</span><span class="s2">"/items"</span><span class="p">,</span> <span class="n">json</span><span class="o">=</span><span class="p">{</span><span class="s2">"name"</span><span class="p">:</span> <span class="s2">"x"</span><span class="p">})</span>
|
||||
</span><span id="__span-1-6"><a id="__codelineno-1-6" name="__codelineno-1-6"></a> <span class="k">assert</span> <span class="n">r</span><span class="o">.</span><span class="n">status_code</span> <span class="o">==</span> <span class="mi">200</span>
|
||||
</span></code></pre></div></td></tr></table></div>
|
||||
<h2 id="unit-tests">Unit tests</h2>
|
||||
<hr />
|
||||
<h2 id="unit-tests">🧪 Unit tests</h2>
|
||||
<ul>
|
||||
<li>Domain models and converters: pure, no I/O.</li>
|
||||
<li>Pipeline stages: feed a fixture state, assert the transformed state.</li>
|
||||
<li>Assembled pipeline roots: <strong>fixture parity</strong> — replay a stored fixture input
|
||||
and compare against a golden output file.</li>
|
||||
</ul>
|
||||
<h2 id="config-consistency-tests">Config-consistency tests</h2>
|
||||
<hr />
|
||||
<h2 id="config-consistency-tests">🔐 Config-consistency tests</h2>
|
||||
<p>For rule-driven pipelines, add a test that cross-references rules against the
|
||||
declared pattern registry: every flag emitted must be defined; every defined
|
||||
flag must be reachable. This catches drift without running the pipeline.</p>
|
||||
<h2 id="conventions">Conventions</h2>
|
||||
<hr />
|
||||
<h2 id="quality-gates">✅ Quality gates</h2>
|
||||
<p>A <strong>definition of done</strong> for any standard app:</p>
|
||||
<ul>
|
||||
<li><code>python -c "import main"</code> succeeds.</li>
|
||||
<li><code>pytest</code> passes.</li>
|
||||
<li>The app serves the spec's operations; the worker drains its queue.</li>
|
||||
<li>No domain module imports a web framework.</li>
|
||||
<li>No infrastructure was reimplemented that a library already provides.</li>
|
||||
</ul>
|
||||
<hr />
|
||||
<h2 id="conventions">💡 Conventions</h2>
|
||||
<ul>
|
||||
<li><code>asyncio_mode = "auto"</code>; no manual event loops.</li>
|
||||
<li>Fixtures are checked into the repo (inputs + golden outputs).</li>
|
||||
<li>Tests skip (not fail) when a required fixture is absent.</li>
|
||||
<li>Do not mock the domain; mock only the infrastructure edge.</li>
|
||||
</ul>
|
||||
<hr />
|
||||
<h2 id="related">Related</h2>
|
||||
<ul>
|
||||
<li><a href="02_components.md">02 – App Components</a> · <a href="03_build_recipe.md">03 – Build Recipe</a> · <a href="../../01_overview/">Overview</a></li>
|
||||
</ul>
|
||||
|
||||
|
||||
|
||||
@@ -608,10 +665,12 @@ flag must be reachable. This catches drift without running the pipeline.</p>
|
||||
|
||||
|
||||
|
||||
<script id="__config" type="application/json">{"base": "..", "features": ["navigation.sections", "navigation.expand", "navigation.top", "navigation.instant", "navigation.tracking", "navigation.indexes", "content.code.copy", "content.code.annotate", "content.tabs.link", "content.action.edit", "search.highlight", "search.share", "search.suggest"], "search": "../assets/javascripts/workers/search.973d3a69.min.js", "tags": null, "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}, "version": null}</script>
|
||||
<script id="__config" type="application/json">{"base": "../..", "features": ["navigation.sections", "navigation.expand", "navigation.top", "navigation.instant", "navigation.tracking", "navigation.indexes", "content.code.copy", "content.code.annotate", "content.tabs.link", "content.action.edit", "search.highlight", "search.share", "search.suggest"], "search": "../../assets/javascripts/workers/search.973d3a69.min.js", "tags": null, "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}, "version": null}</script>
|
||||
|
||||
|
||||
<script src="../assets/javascripts/bundle.f55a23d4.min.js"></script>
|
||||
<script src="../../assets/javascripts/bundle.f55a23d4.min.js"></script>
|
||||
|
||||
<script src="https://unpkg.com/mermaid@10.9.1/dist/mermaid.min.js"></script>
|
||||
|
||||
|
||||
</body>
|
||||
@@ -260,12 +260,12 @@
|
||||
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="/10_testing/" class="md-nav__link">
|
||||
<a href="/02_components/" class="md-nav__link">
|
||||
|
||||
|
||||
|
||||
<span class="md-ellipsis">
|
||||
Testing
|
||||
App Components
|
||||
|
||||
</span>
|
||||
|
||||
@@ -282,7 +282,7 @@
|
||||
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="/11_build_recipe/" class="md-nav__link">
|
||||
<a href="/03_build_recipe/" class="md-nav__link">
|
||||
|
||||
|
||||
|
||||
@@ -297,6 +297,28 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="/04_development/01_testing/" class="md-nav__link">
|
||||
|
||||
|
||||
|
||||
<span class="md-ellipsis">
|
||||
Testing & Quality
|
||||
|
||||
</span>
|
||||
|
||||
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
@@ -375,6 +397,8 @@
|
||||
|
||||
<script src="/assets/javascripts/bundle.f55a23d4.min.js"></script>
|
||||
|
||||
<script src="https://unpkg.com/mermaid@10.9.1/dist/mermaid.min.js"></script>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -72,7 +72,7 @@
|
||||
<div data-md-component="skip">
|
||||
|
||||
|
||||
<a href="#the-aetos-standard-app-blueprint" class="md-skip">
|
||||
<a href="#standards-the-aetos-app-blueprint" class="md-skip">
|
||||
Skip to content
|
||||
</a>
|
||||
|
||||
@@ -273,18 +273,27 @@
|
||||
<ul class="md-nav__list" data-md-component="toc" data-md-scrollfix>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#how-to-read-this" class="md-nav__link">
|
||||
<a href="#key-features" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
How to read this
|
||||
🚀 Key Features
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#for-ai-agents" class="md-nav__link">
|
||||
<a href="#documentation-structure" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
For AI agents
|
||||
📁 Documentation Structure
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#related-resources" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
🔗 Related Resources
|
||||
</span>
|
||||
</a>
|
||||
|
||||
@@ -327,12 +336,12 @@
|
||||
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="10_testing/" class="md-nav__link">
|
||||
<a href="02_components/" class="md-nav__link">
|
||||
|
||||
|
||||
|
||||
<span class="md-ellipsis">
|
||||
Testing
|
||||
App Components
|
||||
|
||||
</span>
|
||||
|
||||
@@ -349,7 +358,7 @@
|
||||
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="11_build_recipe/" class="md-nav__link">
|
||||
<a href="03_build_recipe/" class="md-nav__link">
|
||||
|
||||
|
||||
|
||||
@@ -364,6 +373,28 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="04_development/01_testing/" class="md-nav__link">
|
||||
|
||||
|
||||
|
||||
<span class="md-ellipsis">
|
||||
Testing & Quality
|
||||
|
||||
</span>
|
||||
|
||||
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
@@ -391,18 +422,27 @@
|
||||
<ul class="md-nav__list" data-md-component="toc" data-md-scrollfix>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#how-to-read-this" class="md-nav__link">
|
||||
<a href="#key-features" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
How to read this
|
||||
🚀 Key Features
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#for-ai-agents" class="md-nav__link">
|
||||
<a href="#documentation-structure" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
For AI agents
|
||||
📁 Documentation Structure
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#related-resources" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
🔗 Related Resources
|
||||
</span>
|
||||
</a>
|
||||
|
||||
@@ -424,92 +464,85 @@
|
||||
|
||||
|
||||
|
||||
<h1 id="the-aetos-standard-app-blueprint">The Aetos Standard App Blueprint</h1>
|
||||
<p>A single, reusable architecture for backend applications in the Aetos ecosystem.</p>
|
||||
<p>Every application built with this blueprint has the same shape:</p>
|
||||
<ul>
|
||||
<li>a <strong>spec-first HTTP layer</strong> driven by an OpenAPI document;</li>
|
||||
<li>a <strong>domain layer</strong> of pure models, ports, and services;</li>
|
||||
<li>an <strong>infrastructure layer</strong> of adapters (MongoDB, cache, workers, LLM, mail);</li>
|
||||
<li>a <strong>composition root</strong> that wires everything in exactly one place;</li>
|
||||
<li>an optional <strong>pipeline layer</strong> for data-driven extraction/enrichment;</li>
|
||||
<li>an optional <strong>background worker</strong> that runs long-running jobs and reports progress.</li>
|
||||
</ul>
|
||||
<h1 id="standards-the-aetos-app-blueprint">🧩 Standards — the Aetos App Blueprint</h1>
|
||||
<p>One reusable architecture for every backend application in the Aetos ecosystem:
|
||||
a spec-first HTTP edge, a pure domain core, a pluggable infrastructure layer,
|
||||
and a single composition root.</p>
|
||||
<p>Fill in <strong>two axes only</strong> and a new app is done: your <strong>resource model</strong>, and
|
||||
(if needed) your <strong>pipeline family</strong>. The blueprint itself never names a
|
||||
business domain — expenses, health metrics, inventory, and analytics are all
|
||||
the same shape underneath.</p>
|
||||
<blockquote>
|
||||
<p>This blueprints is deliberately <strong>app-agnostic</strong>. It never names a concrete
|
||||
business domain. Any application — expenses, health metrics, inventory,
|
||||
analytics — is produced by filling in <strong>two axes only</strong>: the resource model
|
||||
and (if needed) the pipeline family.</p>
|
||||
<p><strong>Doc model:</strong> this wiki is written for humans — how-to guides, the build
|
||||
recipe, and quality gates. The enforceable rules (layering, composition root,
|
||||
CRUD template, pipeline scaffold) and the library registry live in the
|
||||
<strong>reference</strong> pages, mirrored by the machine-readable <code>standards</code> MCP bundle.
|
||||
Query <code>find_library</code> before writing any infrastructure.</p>
|
||||
</blockquote>
|
||||
<h2 id="how-to-read-this">How to read this</h2>
|
||||
<hr />
|
||||
<h2 id="key-features">🚀 Key Features</h2>
|
||||
<ul>
|
||||
<li>🧭 <strong>Spec-first routing</strong> — the OpenAPI document, not decorator routers, is
|
||||
the source of truth (operationId lookup)</li>
|
||||
<li>🏗️ <strong>One-way layering</strong> — <code>interfaces → domain → infra</code>, enforced</li>
|
||||
<li>🔌 <strong>Single composition root</strong> — every dependency is wired in exactly one place</li>
|
||||
<li>🧩 <strong>Generic, not app-specific</strong> — reusable rules versus two authoring axes</li>
|
||||
<li>📚 <strong>Library-backed</strong> — DB, DI, pipelines, ingestion, auth, and docs come from
|
||||
internal libraries, never hand-written</li>
|
||||
<li>🚦 <strong>Explicit state machines</strong> — durable job statuses, progress, and
|
||||
human-in-the-loop pauses</li>
|
||||
</ul>
|
||||
<hr />
|
||||
<h2 id="documentation-structure">📁 Documentation Structure</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Page</th>
|
||||
<th>What it covers</th>
|
||||
<th>Section</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Overview</td>
|
||||
<td>The philosophy and the one-way dependency rule</td>
|
||||
<td><a href="01_overview/">Overview</a></td>
|
||||
<td>Philosophy, the one-way dependency rule, architecture</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Layering Rules</td>
|
||||
<td>Which package may import which</td>
|
||||
<td><a href="02_components/">App Components</a></td>
|
||||
<td>The component model and how the pieces wire together</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Composition Root</td>
|
||||
<td>The single wiring point</td>
|
||||
<td><a href="03_build_recipe/">Build Recipe</a></td>
|
||||
<td>The ordered, agentic checklist for a new app</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Spec-First Routing</td>
|
||||
<td>OpenAPI as the source of truth</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>CRUD Template</td>
|
||||
<td>The canonical resource slice</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Pipeline Scaffold</td>
|
||||
<td>Data-driven processing families</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Worker & Progress</td>
|
||||
<td>Async jobs, status machine, streaming progress</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Human-in-the-Loop</td>
|
||||
<td>Pausing a job to ask a question</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Infra Adapters</td>
|
||||
<td>MongoDB, cache, stores, LLM, mail</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Testing</td>
|
||||
<td>Integration + unit conventions</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Build Recipe</td>
|
||||
<td>The ordered checklist to generate a new app</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Genericity Matrix</td>
|
||||
<td>What is reusable vs what you must write</td>
|
||||
<td><a href="04_development/01_testing/">Testing & Quality</a></td>
|
||||
<td>Test layout and acceptance gates</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h2 id="for-ai-agents">For AI agents</h2>
|
||||
<p>This content is served as MCP resources under <code>docs://index</code>, <code>docs://nav</code>,
|
||||
and <code>docs://modules/{module}</code>. To build a new app:</p>
|
||||
<ol>
|
||||
<li>Read <code>build_recipe</code> first.</li>
|
||||
<li>Read <code>genericity_matrix</code> to know what you must author.</li>
|
||||
<li>Query the <strong>library registry</strong> (<code>find_library</code>) before writing any
|
||||
infrastructure yourself.</li>
|
||||
<li>Copy the <code>templates/</code> skeleton and fill the two app-specific axes.</li>
|
||||
</ol>
|
||||
<p><strong>Reference</strong> (separate site, mirrors the MCP bundle): the enforceable
|
||||
rules — <a href="../lib/02_layering_rules/">Layering</a>, <a href="../lib/03_composition_root/">Composition
|
||||
Root</a>, <a href="../lib/04_spec_first_routing/">Spec-First
|
||||
Routing</a>, <a href="../lib/05_crud_template/">CRUD
|
||||
Template</a>, <a href="../lib/06_pipeline_scaffold/">Pipeline
|
||||
Scaffold</a>, <a href="../lib/07_worker_and_progress/">Worker &
|
||||
Progress</a>, <a href="../lib/08_human_in_loop/">Human-in-the-Loop</a>,
|
||||
<a href="../lib/09_infra_adaptors/">Infra Adapters</a>, <a href="../lib/12_genericity_matrix/">Genericity
|
||||
Matrix</a>, <a href="../lib/13_library_registry/">Library
|
||||
Registry</a>.</p>
|
||||
<hr />
|
||||
<h2 id="related-resources">🔗 Related Resources</h2>
|
||||
<ul>
|
||||
<li><strong>MCP bundle</strong> — <code>standards</code> server on <code>:8009</code>: read <code>doc://index</code>,
|
||||
<code>doc://nav</code>, <code>doc://modules/{module}</code>; call <code>find_library(capability)</code></li>
|
||||
<li><strong>Library registry</strong> — the catalog of internal libraries behind every app
|
||||
(<code>libraries.json</code>, served as <code>registry://libraries</code>)</li>
|
||||
<li><strong>Template</strong> — the runnable skeleton at <code>standards/src/templates/app/</code></li>
|
||||
<li><strong>Copy the repo's <code>AGENTS.md</code>-style build prompt</strong> from
|
||||
<a href="03_build_recipe/">Build Recipe</a></li>
|
||||
</ul>
|
||||
<hr />
|
||||
<p>© Aetoskia Internal — <code>standards</code></p>
|
||||
|
||||
|
||||
|
||||
@@ -571,6 +604,8 @@ and <code>docs://modules/{module}</code>. To build a new app:</p>
|
||||
|
||||
<script src="assets/javascripts/bundle.f55a23d4.min.js"></script>
|
||||
|
||||
<script src="https://unpkg.com/mermaid@10.9.1/dist/mermaid.min.js"></script>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user