diff --git a/standards/mcp/modules/wiki_reference_app_client_admin.json b/standards/mcp/modules/wiki_reference_app_client_admin.json index b1b48ab..2f1b9ed 100644 --- a/standards/mcp/modules/wiki_reference_app_client_admin.json +++ b/standards/mcp/modules/wiki_reference_app_client_admin.json @@ -2,7 +2,7 @@ "module": "wiki_reference_app_client_admin", "content": { "path": "wiki_reference_app_client_admin", - "docstring": "# Client Admin — the spec-driven frontend\n\nThe client sample (`standards/src/templates/client/`) renders a full admin —\nauth pages, resource lists, forms, detail views — **from the server's\n`openapi.yaml`**, driven entirely by the x- extension fields. There are no\nhand-written screens per resource.\n\n## What ships\n\nA Vite + React + MUI app wiring the two Aetos packages:\n\n- **`@aetoskia/react-openapi`** — fetches `${VITE_API_BASE_URL}/openapi.json`,\n builds the router and the resource admin from the spec's schemas and x-\n fields (list columns, autocomplete, fk pickers, enum selects, hidden fields…)\n- **`@aetoskia/react-auth`** — login/register/logout against the auth-server\n paths declared in the spec's `securitySchemes.bearerAuth`, and attaches the\n JWT to every API call\n- a shared MUI theme (`src/shared-theme/`) and the shell\n (`Header` / `Footer` / `Home`)\n\n`src/openapi-config.ts` is the only configuration:\n\n```ts\nexport const specConfiguration: SpecConfiguration = {\n specUrl: `${apiBase}/openapi.json`,\n baseApiUrl: apiBase,\n title: \"Vet Admin\",\n getToken: () => tokenStore.get(),\n};\n```\n\n## Pair it with the sample server\n\n```bash\ncd apps/vet-sample-client\nprintf 'VITE_API_BASE_URL=http://localhost:8020\\nVITE_AUTH_BASE_URL=http://localhost:8020\\n' > .env.local\nnpm install\nnpm run dev\n```\n\nOpen the dev-server URL; the admin lists Parents, Pets, Treatments, Vets,\nAppointments and Jobs straight from the vet spec. Edit `openapi.yaml` on the\nserver and the client follows on reload — the spec is the contract on both\nsides.\n\n## Where the x- extensions come from\n\nThe canonical catalog lives in the react-openapi package\n(`spec-extensions.ts`, served to agents as the `specExtensions` module of the\nreact-openapi MCP bundle). The worked examples on the server side are the\n`Job` schema (enum selects via `x-edit-as: select` + `x-ui-type: enum`,\n`x-hidden` field masks, `x-list-columns`) and the FK fields on `Appointment`\n(`x-fk: {resource: pets, prefetch: true}`).\n\n## CI\n\nThe client ships a `drone-npm` shape (Type B in\n[Drone Pipelines](../lib/90_drone_pipelines/)) and follows the\n[Versioning](../lib/91_versioning/) standard (`package.json` +\n`CHANGELOG.md`).\n", + "docstring": "# Client Admin — the spec-driven frontend\n\nThe client sample (`standards/src/templates/client/`) renders a full admin —\nauth pages, resource lists, forms, detail views — **from the server's\n`openapi.yaml`**, driven entirely by the x- extension fields. There are no\nhand-written screens per resource.\n\n## What ships\n\nA Vite + React + MUI app wiring the two Aetos packages:\n\n- **`@aetoskia/react-openapi`** — fetches `${VITE_API_BASE_URL}/openapi.json`,\n builds the router and the resource admin from the spec's schemas and x-\n fields (list columns, autocomplete, fk pickers, enum selects, hidden fields…)\n- **`@aetoskia/react-auth`** — login/register/logout against the auth-server\n paths declared in the spec's `securitySchemes.bearerAuth`, and attaches the\n JWT to every API call\n- a shared MUI theme (`src/shared-theme/`) and the shell\n (`Header` / `Footer` / `Home`)\n\n`src/openapi-config.ts` is the only configuration:\n\n```ts\nexport const specConfiguration: SpecConfiguration = {\n specUrl: `${apiBase}/openapi.json`,\n baseApiUrl: apiBase,\n title: \"Vet Admin\",\n getToken: () => tokenStore.get(),\n};\n```\n\n## Pair it with the sample server\n\n```bash\ncd apps/vet-sample-client\nprintf 'VITE_API_BASE_URL=http://localhost:8020\\nVITE_AUTH_BASE_URL=http://localhost:8020\\n' > .env.local\nnpm install\nnpm run dev\n```\n\nOpen the dev-server URL; the admin lists Parents, Pets, Treatments, Vets,\nAppointments and Jobs straight from the vet spec. Edit `openapi.yaml` on the\nserver and the client follows on reload — the spec is the contract on both\nsides.\n\n## Where the x- extensions come from\n\nThe canonical catalog lives in the react-openapi package\n(`spec-extensions.ts`, served to agents as the `specExtensions` module of the\nreact-openapi MCP bundle). The worked examples on the server side are the\n`Job` schema (enum selects via `x-edit-as: select` + `x-ui-type: enum`,\n`x-hidden` field masks, `x-list-columns`) and the FK fields on `Appointment`\n(`x-fk: {resource: pets, prefetch: true}`).\n\n## CI\n\nThe client ships a `drone-npm` shape (Type B in\n[Drone Pipelines](../lib/90_drone_pipelines/)) and follows the\n[Versioning](../lib/91_versioning/) standard (`package.json` +\n`CHANGELOG.md`).\n", "objects": {} } } \ No newline at end of file diff --git a/standards/mcp/modules/wiki_reference_app_domain_and_api.json b/standards/mcp/modules/wiki_reference_app_domain_and_api.json index 154464d..9980c10 100644 --- a/standards/mcp/modules/wiki_reference_app_domain_and_api.json +++ b/standards/mcp/modules/wiki_reference_app_domain_and_api.json @@ -2,7 +2,7 @@ "module": "wiki_reference_app_domain_and_api", "content": { "path": "wiki_reference_app_domain_and_api", - "docstring": "# Domain & API — the resource axis\n\nEvery aggregate follows the same four-file slice\n([CRUD Template](../lib/21_crud_template/)); walk `parents` first, then\n`appointments` when you need foreign keys.\n\n## The slice, end to end (parents)\n\n```\ndomain/parents/\n models.py # framework-free Pydantic model (id optional)\n ports/\n db.py # ParentDocument(Parent, BaseDocument) + create_parent_repo()\n service.py # ParentCrudsService(CrudsServiceAdaptor[...]) + get_parent_service()\ninterfaces/parents/\n routes.py # list/create/get/update/delete — names == operationIds\n```\n\nWiring happens in exactly one place — the composition root\n`init.py`:\n\n```python\nparent_repo = create_parent_repo()\n_resources[\"parent_svc\"] = get_parent_service(parent_repo)\n```\n\nRoutes resolve the service from app state through the shared helper\n(`interfaces/_common.py`):\n\n```python\nasync def list_parents(request: Request, limit: int = 20, offset: int = 0) -> dict:\n docs = await svc(request, \"parent\").search({})\n return paginate([dump(d) for d in docs], limit, offset)\n```\n\n## The spec is the contract\n\n`openapi.yaml` is OpenAPI 3.1 and the\nsingle source of truth: `main.py` builds an `OpenAPIFirstApp` that binds each\n`operationId` to a function of the same name in `interfaces/openapi.py` (a flat\nre-export of every routes module). There are no decorator routers.\n\nThe x- extension fields on schemas and properties are what make the admin\nclient render itself — see [Auto-Rendered Admin](../lib/40_ui_admin/) and\nthe worked `Job` schema (enum selects, hidden fields, filterable columns).\n\n## Foreign keys (appointments)\n\n`Appointment` declares `pet`, `vet`, `treatment` as `dict | int | None`. The\nwrite path normalizes bare ids through `expand_ref`; the read path returns\nfully expanded objects:\n\n```python\npet = await expand_ref(svc(request, \"pet\"), body.get(\"pet\"), \"pet\")\n```\n\nThe list endpoint additionally filters by date prefix and embedded fk ids —\nclient-side over the repository result, the pattern for small reference data.\n\n## Ids on the wire\n\nThe domain models declare `id: int | None = None` and documents use the\n**integer-id** repository variants (`infra/db/repos/mongo_int.py`,\n`create_repo()` in `domain/*/ports/db.py`, `REPO_BACKEND=memory|mongo`). This\nkeeps the OpenAPI `id: integer` contract, the in-memory demo store, and the\nMongo persistence layer consistent.\n\n## Tests\n\n- `tests/integration/test_openapi_app_builds.py` — generic gate: every\n `operationId` resolves and the app builds. Copy it verbatim.\n- `tests/unit/test_jobs_routes.py` — the pattern for route unit tests with a\n service double (no HTTP client needed).\n", + "docstring": "# Domain & API — the resource axis\n\nEvery aggregate follows the same four-file slice\n([CRUD Template](../lib/21_crud_template/)); walk `parents` first, then\n`appointments` when you need foreign keys.\n\n## The slice, end to end (parents)\n\n```\ndomain/parents/\n models.py # framework-free Pydantic model (id optional)\n ports/\n db.py # ParentDocument(Parent, BaseDocument) + create_parent_repo()\n service.py # ParentCrudsService(CrudsServiceAdaptor[...]) + get_parent_service()\ninterfaces/parents/\n routes.py # list/create/get/update/delete — names == operationIds\n```\n\nWiring happens in exactly one place — the composition root\n`init.py`:\n\n```python\nparent_repo = create_parent_repo()\n_resources[\"parent_svc\"] = get_parent_service(parent_repo)\n```\n\nRoutes resolve the service from app state through the shared helper\n(`interfaces/_common.py`):\n\n```python\nasync def list_parents(request: Request, limit: int = 20, offset: int = 0) -> dict:\n docs = await svc(request, \"parent\").search({})\n return paginate([dump(d) for d in docs], limit, offset)\n```\n\n## The spec is the contract\n\n`openapi.yaml` is OpenAPI 3.1 and the\nsingle source of truth: `main.py` builds an `OpenAPIFirstApp` that binds each\n`operationId` to a function of the same name in `interfaces/openapi.py` (a flat\nre-export of every routes module). There are no decorator routers.\n\nThe x- extension fields on schemas and properties are what make the admin\nclient render itself — see [Auto-Rendered Admin](../lib/40_ui_admin/) and\nthe worked `Job` schema (enum selects, hidden fields, filterable columns).\n\n## Foreign keys (appointments)\n\n`Appointment` declares `pet`, `vet`, `treatment` as `dict | int | None`. The\nwrite path normalizes bare ids through `expand_ref`; the read path returns\nfully expanded objects:\n\n```python\npet = await expand_ref(svc(request, \"pet\"), body.get(\"pet\"), \"pet\")\n```\n\nThe list endpoint additionally filters by date prefix and embedded fk ids —\nclient-side over the repository result, the pattern for small reference data.\n\n## Ids on the wire\n\nThe domain models declare `id: int | None = None` and documents use the\n**integer-id** repository variants (`infra/db/repos/mongo_int.py`,\n`create_repo()` in `domain/*/ports/db.py`, `REPO_BACKEND=memory|mongo`). This\nkeeps the OpenAPI `id: integer` contract, the in-memory demo store, and the\nMongo persistence layer consistent.\n\n## Tests\n\n- `tests/integration/test_openapi_app_builds.py` — generic gate: every\n `operationId` resolves and the app builds. Copy it verbatim.\n- `tests/unit/test_jobs_routes.py` — the pattern for route unit tests with a\n service double (no HTTP client needed).\n", "objects": {} } } \ No newline at end of file diff --git a/standards/mcp/modules/wiki_reference_app_overview.json b/standards/mcp/modules/wiki_reference_app_overview.json index 2e85eab..19cb41a 100644 --- a/standards/mcp/modules/wiki_reference_app_overview.json +++ b/standards/mcp/modules/wiki_reference_app_overview.json @@ -2,7 +2,7 @@ "module": "wiki_reference_app_overview", "content": { "path": "wiki_reference_app_overview", - "docstring": "# Reference App — Overview\n\nThe standards ship a **runnable sample** of the whole blueprint: the **vet**\nveterinary-clinic application. It exists to be copied — it is the reference\nfor how a real domain sits on the architecture, and the code an agent pulls\nthrough the MCP tools (`get_app_template`, `get_app_component`).\n\n!!! note \"One sample, two templates\"\n The **server** sample lives at `standards/src/templates/app/`\n (FastAPI + openapi-first + mongo-ops + hexa). The **client** sample lives\n at `standards/src/templates/client/` (Vite + MUI + `@aetoskia/react-openapi`\n + `@aetoskia/react-auth`). Together they are the full-stack reference.\n\n---\n\n## The domain at a glance\n\n| Aggregate | What it demonstrates |\n|---------------|---------------------------------------------------------------|\n| **parents** | The simplest CRUD slice (no foreign keys) — copy this first |\n| **pets** | Embedded list-of-refs (`parents[]`), file upload, enum fields |\n| **treatments**| Plain resource with pricing fields |\n| **vets** | Plain resource; target of prefetch FKs |\n| **appointments** | Single FKs (`pet`, `vet`, `treatment`) expanded on read, filtered list |\n| **jobs** | The worker's queue — durable status machine |\n\nThe pipeline axis ships one worked family, **`appointment_reminder`**, plus the\nreusable `pipelines/shared/` scaffold and a `worker.py` that dispatches jobs by\n`job.kind`.\n\n## Run it\n\n```bash\ncp -r /standards/src/templates/app apps/vet-sample\ncd apps/vet-sample\npip install -e \".[dev]\"\n\nuvicorn main:app --reload # API on :8020 (memory backend, zero services)\npython worker.py # background worker (polls jobs)\npython -m scripts.seed # optional: seed demo data (mongo backend)\n```\n\nThe default `REPO_BACKEND=memory` means the API boots with **no external\nservices** — repositories are process-local dictionaries. Switch to\n`REPO_BACKEND=mongo` for persistence (see `.env.example`).\n\nThe client pairs with it:\n\n```bash\ncp -r /standards/src/templates/client apps/vet-sample-client\ncd apps/vet-sample-client\necho \"VITE_API_BASE_URL=http://localhost:8020\" > .env.local\necho \"VITE_AUTH_BASE_URL=http://localhost:8020\" >> .env.local\nnpm install\nnpm run dev # Vite dev server\n```\n\n!!! warning \"Auth\"\n The server ships with the `bearerAuth` security scheme declared\n (`{AUTH_SERVER}` introspection). Without a reachable auth-server the API\n answers `401` on resource routes; `/health` stays public. For a local\n no-auth demo, remove the `security` block from `openapi.yaml` and the\n global security entry — see\n [Auth Integration](../lib/22_auth_integration/).\n\n## Next\n\n- [Domain & API](02_domain_and_api.md) — walking the resource axis\n- [Pipelines & Worker](03_pipelines_and_worker.md) — the pipeline axis\n- [Client Admin](04_client_admin.md) — the spec-driven frontend\n", + "docstring": "# Reference App — Overview\n\nThe standards ship a **runnable sample** of the whole blueprint: the **vet**\nveterinary-clinic application. It exists to be copied — it is the reference\nfor how a real domain sits on the architecture, and the code an agent pulls\nthrough the MCP tools (`get_app_template`, `get_app_component`).\n\n!!! note \"One sample, two templates\"\n The **server** sample lives at `standards/src/templates/app/`\n (FastAPI + openapi-first + mongo-ops + hexa). The **client** sample lives\n at `standards/src/templates/client/` (Vite + MUI + `@aetoskia/react-openapi`\n + `@aetoskia/react-auth`). Together they are the full-stack reference.\n\n---\n\n## The domain at a glance\n\n| Aggregate | What it demonstrates |\n|---------------|---------------------------------------------------------------|\n| **parents** | The simplest CRUD slice (no foreign keys) — copy this first |\n| **pets** | Embedded list-of-refs (`parents[]`), file upload, enum fields |\n| **treatments**| Plain resource with pricing fields |\n| **vets** | Plain resource; target of prefetch FKs |\n| **appointments** | Single FKs (`pet`, `vet`, `treatment`) expanded on read, filtered list |\n| **jobs** | The worker's queue — durable status machine |\n\nThe pipeline axis ships one worked family, **`appointment_reminder`**, plus the\nreusable `pipelines/shared/` scaffold and a `worker.py` that dispatches jobs by\n`job.kind`.\n\n## Run it\n\n```bash\ncp -r /standards/src/templates/app apps/vet-sample\ncd apps/vet-sample\npip install -e \".[dev]\"\n\nuvicorn main:app --reload # API on :8020 (memory backend, zero services)\npython worker.py # background worker (polls jobs)\npython -m scripts.seed # optional: seed demo data (mongo backend)\n```\n\nThe default `REPO_BACKEND=memory` means the API boots with **no external\nservices** — repositories are process-local dictionaries. Switch to\n`REPO_BACKEND=mongo` for persistence (see `.env.example`).\n\nThe client pairs with it:\n\n```bash\ncp -r /standards/src/templates/client apps/vet-sample-client\ncd apps/vet-sample-client\necho \"VITE_API_BASE_URL=http://localhost:8020\" > .env.local\necho \"VITE_AUTH_BASE_URL=http://localhost:8020\" >> .env.local\nnpm install\nnpm run dev # Vite dev server\n```\n\n!!! warning \"Auth\"\n The server ships with the `bearerAuth` security scheme declared\n (`{AUTH_SERVER}` introspection). Without a reachable auth-server the API\n answers `401` on resource routes; `/health` stays public. For a local\n no-auth demo, remove the `security` block from `openapi.yaml` and the\n global security entry — see\n [Auth Integration](../lib/22_auth_integration/).\n\n## Next\n\n- [Domain & API](02_domain_and_api.md) — walking the resource axis\n- [Pipelines & Worker](03_pipelines_and_worker.md) — the pipeline axis\n- [Client Admin](04_client_admin.md) — the spec-driven frontend\n", "objects": {} } } \ No newline at end of file diff --git a/standards/mcp/modules/wiki_reference_app_pipelines_and_worker.json b/standards/mcp/modules/wiki_reference_app_pipelines_and_worker.json index e62c4cc..53c3668 100644 --- a/standards/mcp/modules/wiki_reference_app_pipelines_and_worker.json +++ b/standards/mcp/modules/wiki_reference_app_pipelines_and_worker.json @@ -2,7 +2,7 @@ "module": "wiki_reference_app_pipelines_and_worker", "content": { "path": "wiki_reference_app_pipelines_and_worker", - "docstring": "# Pipelines & Worker — the pipeline axis\n\nThe sample proves the full loop: **API creates a job document → worker polls →\npipeline family runs → job reaches a terminal status.**\n\n## The jobs slice\n\n`domain/jobs/` is an ordinary CRUD slice; its model is the durable state\nmachine the worker drives:\n\n```python\nclass Job(BaseModel):\n name: str\n kind: str # dispatch key\n status: Literal[\"pending\", \"processing\", \"done\", \"failed\"] = \"pending\"\n payload: dict | None = None\n retry_count: int = 0\n error_message: str | None = None\n```\n\n`POST /jobs` with `{\"name\": \"...\", \"kind\": \"appointment_reminder\",\n\"payload\": {\"appointment_id\": 1}}` enqueues work; `GET /jobs?status=...`\ninspects the queue.\n\n## The appointment_reminder family\n\n```\npipelines/appointment_reminder/\n abc.py # LoadPort / ComposePort / DeliverPort + root interface\n impl.py # concrete stages, wired root, register(\"appointment_reminder\", ...)\n pipeline.yaml # graph spec (nodes, edges, roots)\n resources/\n pipeline.yaml # runtime spec consumed by the loader\n patterns/reminder.yaml # message template fragments\npipelines/shared/\n ports.py loader.py registry.py # reused by every family\n```\n\nEach stage is a small, independently testable unit. The loader\n(`pipelines/shared/loader.py`) wires the root through `hexa.build` and merges\n`resources/patterns/*.yaml` into the stage-injected registry:\n\n```python\npipeline = get_appointment_reminder_pipeline(appointment_svc)\nreminder = await pipeline.run(appointment_id)\n```\n\nTo add a family: copy `appointment_reminder/`, replace the stages, call\n`register(kind, variant, root_cls)` — see\n[Pipeline Scaffold](../lib/50_pipeline_scaffold/).\n\n## The worker\n\n`worker.py` is the process that turns\ndocuments into execution:\n\n1. poll `job_svc.search({\"status\": \"pending\"})` on `WORKER_POLL_SECONDS`\n2. claim a job: `status → processing`\n3. dispatch through the `HANDLERS` table — `HANDLERS[job.kind](job, resources)`\n4. on success `status → done`; on exception `status → failed`,\n `retry_count += 1`, `error_message` captured; jobs at\n `WORKER_MAX_RETRIES` are skipped\n\n```python\nHANDLERS: dict[str, Callable] = {\n \"appointment_reminder\": run_appointment_reminder,\n # add yours here\n}\n```\n\nThe worker shares the composition root: `from init import init` gives it the\nsame wired services the API uses — one place to add a dependency.\n\n!!! note \"Same process model in production\"\n API and worker are separate processes (see `docker-compose.yaml`: `api`,\n `worker`, `mongo`). Any in-memory progress bus is per-process; to surface\n live progress through the API, swap the bus adaptor for a broker-backed\n one — see [Worker & Progress](../lib/51_worker_and_progress/).\n\n## Tests\n\n- `tests/unit/test_appointment_reminder_pipeline.py` — stage units + the wired\n root against a fake service.\n- `tests/unit/test_worker.py` — dispatch, done/failed transitions, retry\n exhaustion, using an in-memory job service double and a cancelled poll task.\n", + "docstring": "# Pipelines & Worker — the pipeline axis\n\nThe sample proves the full loop: **API creates a job document → worker polls →\npipeline family runs → job reaches a terminal status.**\n\n## The jobs slice\n\n`domain/jobs/` is an ordinary CRUD slice; its model is the durable state\nmachine the worker drives:\n\n```python\nclass Job(BaseModel):\n name: str\n kind: str # dispatch key\n status: Literal[\"pending\", \"processing\", \"done\", \"failed\"] = \"pending\"\n payload: dict | None = None\n retry_count: int = 0\n error_message: str | None = None\n```\n\n`POST /jobs` with `{\"name\": \"...\", \"kind\": \"appointment_reminder\",\n\"payload\": {\"appointment_id\": 1}}` enqueues work; `GET /jobs?status=...`\ninspects the queue.\n\n## The appointment_reminder family\n\n```\npipelines/appointment_reminder/\n abc.py # LoadPort / ComposePort / DeliverPort + root interface\n impl.py # concrete stages, wired root, register(\"appointment_reminder\", ...)\n pipeline.yaml # graph spec (nodes, edges, roots)\n resources/\n pipeline.yaml # runtime spec consumed by the loader\n patterns/reminder.yaml # message template fragments\npipelines/shared/\n ports.py loader.py registry.py # reused by every family\n```\n\nEach stage is a small, independently testable unit. The loader\n(`pipelines/shared/loader.py`) wires the root through `hexa.build` and merges\n`resources/patterns/*.yaml` into the stage-injected registry:\n\n```python\npipeline = get_appointment_reminder_pipeline(appointment_svc)\nreminder = await pipeline.run(appointment_id)\n```\n\nTo add a family: copy `appointment_reminder/`, replace the stages, call\n`register(kind, variant, root_cls)` — see\n[Pipeline Scaffold](../lib/50_pipeline_scaffold/).\n\n## The worker\n\n`worker.py` is the process that turns\ndocuments into execution:\n\n1. poll `job_svc.search({\"status\": \"pending\"})` on `WORKER_POLL_SECONDS`\n2. claim a job: `status → processing`\n3. dispatch through the `HANDLERS` table — `HANDLERS[job.kind](job, resources)`\n4. on success `status → done`; on exception `status → failed`,\n `retry_count += 1`, `error_message` captured; jobs at\n `WORKER_MAX_RETRIES` are skipped\n\n```python\nHANDLERS: dict[str, Callable] = {\n \"appointment_reminder\": run_appointment_reminder,\n # add yours here\n}\n```\n\nThe worker shares the composition root: `from init import init` gives it the\nsame wired services the API uses — one place to add a dependency.\n\n!!! note \"Same process model in production\"\n API and worker are separate processes (see `docker-compose.yaml`: `api`,\n `worker`, `mongo`). Any in-memory progress bus is per-process; to surface\n live progress through the API, swap the bus adaptor for a broker-backed\n one — see [Worker & Progress](../lib/51_worker_and_progress/).\n\n## Tests\n\n- `tests/unit/test_appointment_reminder_pipeline.py` — stage units + the wired\n root against a fake service.\n- `tests/unit/test_worker.py` — dispatch, done/failed transitions, retry\n exhaustion, using an in-memory job service double and a cancelled poll task.\n", "objects": {} } } \ No newline at end of file diff --git a/standards/src/wiki/06_reference_app/01_overview.md b/standards/src/wiki/06_reference_app/01_overview.md index 2aa055b..d667c48 100644 --- a/standards/src/wiki/06_reference_app/01_overview.md +++ b/standards/src/wiki/06_reference_app/01_overview.md @@ -1,7 +1,7 @@ -# Reference App — Overview +# Reference App — Overview The standards ship a **runnable sample** of the whole blueprint: the **vet** -veterinary-clinic application. It exists to be copied — it is the reference +veterinary-clinic application. It exists to be copied — it is the reference for how a real domain sits on the architecture, and the code an agent pulls through the MCP tools (`get_app_template`, `get_app_component`). @@ -17,12 +17,12 @@ through the MCP tools (`get_app_template`, `get_app_component`). | Aggregate | What it demonstrates | |---------------|---------------------------------------------------------------| -| **parents** | The simplest CRUD slice (no foreign keys) — copy this first | +| **parents** | The simplest CRUD slice (no foreign keys) — copy this first | | **pets** | Embedded list-of-refs (`parents[]`), file upload, enum fields | | **treatments**| Plain resource with pricing fields | | **vets** | Plain resource; target of prefetch FKs | | **appointments** | Single FKs (`pet`, `vet`, `treatment`) expanded on read, filtered list | -| **jobs** | The worker's queue — durable status machine | +| **jobs** | The worker's queue — durable status machine | The pipeline axis ships one worked family, **`appointment_reminder`**, plus the reusable `pipelines/shared/` scaffold and a `worker.py` that dispatches jobs by @@ -41,7 +41,7 @@ python -m scripts.seed # optional: seed demo data (mongo backend) ``` The default `REPO_BACKEND=memory` means the API boots with **no external -services** — repositories are process-local dictionaries. Switch to +services** — repositories are process-local dictionaries. Switch to `REPO_BACKEND=mongo` for persistence (see `.env.example`). The client pairs with it: @@ -60,11 +60,11 @@ npm run dev # Vite dev server (`{AUTH_SERVER}` introspection). Without a reachable auth-server the API answers `401` on resource routes; `/health` stays public. For a local no-auth demo, remove the `security` block from `openapi.yaml` and the - global security entry — see + global security entry — see [Auth Integration](../lib/22_auth_integration/). ## Next -- [Domain & API](02_domain_and_api.md) — walking the resource axis -- [Pipelines & Worker](03_pipelines_and_worker.md) — the pipeline axis -- [Client Admin](04_client_admin.md) — the spec-driven frontend +- [Domain & API](02_domain_and_api.md) — walking the resource axis +- [Pipelines & Worker](03_pipelines_and_worker.md) — the pipeline axis +- [Client Admin](04_client_admin.md) — the spec-driven frontend diff --git a/standards/src/wiki/06_reference_app/02_domain_and_api.md b/standards/src/wiki/06_reference_app/02_domain_and_api.md index 2720118..74c043e 100644 --- a/standards/src/wiki/06_reference_app/02_domain_and_api.md +++ b/standards/src/wiki/06_reference_app/02_domain_and_api.md @@ -1,4 +1,4 @@ -# Domain & API — the resource axis +# Domain & API — the resource axis Every aggregate follows the same four-file slice ([CRUD Template](../lib/21_crud_template/)); walk `parents` first, then @@ -13,10 +13,10 @@ domain/parents/ db.py # ParentDocument(Parent, BaseDocument) + create_parent_repo() service.py # ParentCrudsService(CrudsServiceAdaptor[...]) + get_parent_service() interfaces/parents/ - routes.py # list/create/get/update/delete — names == operationIds + routes.py # list/create/get/update/delete — names == operationIds ``` -Wiring happens in exactly one place — the composition root +Wiring happens in exactly one place — the composition root `init.py`: ```python @@ -41,7 +41,7 @@ single source of truth: `main.py` builds an `OpenAPIFirstApp` that binds each re-export of every routes module). There are no decorator routers. The x- extension fields on schemas and properties are what make the admin -client render itself — see [Auto-Rendered Admin](../lib/40_ui_admin/) and +client render itself — see [Auto-Rendered Admin](../lib/40_ui_admin/) and the worked `Job` schema (enum selects, hidden fields, filterable columns). ## Foreign keys (appointments) @@ -54,7 +54,7 @@ fully expanded objects: pet = await expand_ref(svc(request, "pet"), body.get("pet"), "pet") ``` -The list endpoint additionally filters by date prefix and embedded fk ids — +The list endpoint additionally filters by date prefix and embedded fk ids — client-side over the repository result, the pattern for small reference data. ## Ids on the wire @@ -67,7 +67,7 @@ Mongo persistence layer consistent. ## Tests -- `tests/integration/test_openapi_app_builds.py` — generic gate: every +- `tests/integration/test_openapi_app_builds.py` — generic gate: every `operationId` resolves and the app builds. Copy it verbatim. -- `tests/unit/test_jobs_routes.py` — the pattern for route unit tests with a +- `tests/unit/test_jobs_routes.py` — the pattern for route unit tests with a service double (no HTTP client needed). diff --git a/standards/src/wiki/06_reference_app/03_pipelines_and_worker.md b/standards/src/wiki/06_reference_app/03_pipelines_and_worker.md index ece562e..6d79efb 100644 --- a/standards/src/wiki/06_reference_app/03_pipelines_and_worker.md +++ b/standards/src/wiki/06_reference_app/03_pipelines_and_worker.md @@ -1,4 +1,4 @@ -# Pipelines & Worker — the pipeline axis +# Pipelines & Worker — the pipeline axis The sample proves the full loop: **API creates a job document → worker polls → pipeline family runs → job reaches a terminal status.** @@ -46,7 +46,7 @@ reminder = await pipeline.run(appointment_id) ``` To add a family: copy `appointment_reminder/`, replace the stages, call -`register(kind, variant, root_cls)` — see +`register(kind, variant, root_cls)` — see [Pipeline Scaffold](../lib/50_pipeline_scaffold/). ## The worker @@ -56,7 +56,7 @@ documents into execution: 1. poll `job_svc.search({"status": "pending"})` on `WORKER_POLL_SECONDS` 2. claim a job: `status → processing` -3. dispatch through the `HANDLERS` table — `HANDLERS[job.kind](job, resources)` +3. dispatch through the `HANDLERS` table — `HANDLERS[job.kind](job, resources)` 4. on success `status → done`; on exception `status → failed`, `retry_count += 1`, `error_message` captured; jobs at `WORKER_MAX_RETRIES` are skipped @@ -69,17 +69,17 @@ HANDLERS: dict[str, Callable] = { ``` The worker shares the composition root: `from init import init` gives it the -same wired services the API uses — one place to add a dependency. +same wired services the API uses — one place to add a dependency. !!! note "Same process model in production" API and worker are separate processes (see `docker-compose.yaml`: `api`, `worker`, `mongo`). Any in-memory progress bus is per-process; to surface live progress through the API, swap the bus adaptor for a broker-backed - one — see [Worker & Progress](../lib/51_worker_and_progress/). + one — see [Worker & Progress](../lib/51_worker_and_progress/). ## Tests -- `tests/unit/test_appointment_reminder_pipeline.py` — stage units + the wired +- `tests/unit/test_appointment_reminder_pipeline.py` — stage units + the wired root against a fake service. -- `tests/unit/test_worker.py` — dispatch, done/failed transitions, retry +- `tests/unit/test_worker.py` — dispatch, done/failed transitions, retry exhaustion, using an in-memory job service double and a cancelled poll task. diff --git a/standards/src/wiki/06_reference_app/04_client_admin.md b/standards/src/wiki/06_reference_app/04_client_admin.md index 66b5750..2edc48f 100644 --- a/standards/src/wiki/06_reference_app/04_client_admin.md +++ b/standards/src/wiki/06_reference_app/04_client_admin.md @@ -1,7 +1,7 @@ -# Client Admin — the spec-driven frontend +# Client Admin — the spec-driven frontend -The client sample (`standards/src/templates/client/`) renders a full admin — -auth pages, resource lists, forms, detail views — **from the server's +The client sample (`standards/src/templates/client/`) renders a full admin — +auth pages, resource lists, forms, detail views — **from the server's `openapi.yaml`**, driven entirely by the x- extension fields. There are no hand-written screens per resource. @@ -9,10 +9,10 @@ hand-written screens per resource. A Vite + React + MUI app wiring the two Aetos packages: -- **`@aetoskia/react-openapi`** — fetches `${VITE_API_BASE_URL}/openapi.json`, +- **`@aetoskia/react-openapi`** — fetches `${VITE_API_BASE_URL}/openapi.json`, builds the router and the resource admin from the spec's schemas and x- - fields (list columns, autocomplete, fk pickers, enum selects, hidden fields…) -- **`@aetoskia/react-auth`** — login/register/logout against the auth-server + fields (list columns, autocomplete, fk pickers, enum selects, hidden fields…) +- **`@aetoskia/react-auth`** — login/register/logout against the auth-server paths declared in the spec's `securitySchemes.bearerAuth`, and attaches the JWT to every API call - a shared MUI theme (`src/shared-theme/`) and the shell @@ -40,7 +40,7 @@ npm run dev Open the dev-server URL; the admin lists Parents, Pets, Treatments, Vets, Appointments and Jobs straight from the vet spec. Edit `openapi.yaml` on the -server and the client follows on reload — the spec is the contract on both +server and the client follows on reload — the spec is the contract on both sides. ## Where the x- extensions come from diff --git a/standards/wiki/06_reference_app/01_overview/index.html b/standards/wiki/06_reference_app/01_overview/index.html index 9a49b5e..acf4b0b 100644 --- a/standards/wiki/06_reference_app/01_overview/index.html +++ b/standards/wiki/06_reference_app/01_overview/index.html @@ -74,7 +74,7 @@
- + Skip to content @@ -842,9 +842,9 @@ -

Reference App — Overview

+

Reference App — Overview

The standards ship a runnable sample of the whole blueprint: the vet -veterinary-clinic application. It exists to be copied — it is the reference +veterinary-clinic application. It exists to be copied — it is the reference for how a real domain sits on the architecture, and the code an agent pulls through the MCP tools (get_app_template, get_app_component).

@@ -866,7 +866,7 @@ at standards/src/templates/client/ (Vite + MUI + @aetoskia/re parents -The simplest CRUD slice (no foreign keys) — copy this first +The simplest CRUD slice (no foreign keys) — copy this first pets @@ -886,7 +886,7 @@ at standards/src/templates/client/ (Vite + MUI + @aetoskia/re jobs -The worker's queue — durable status machine +The worker's queue — durable status machine @@ -909,7 +909,7 @@ reusable pipelines/shared/ scaffold and a worker.py th python -m scripts.seed # optional: seed demo data (mongo backend)

The default REPO_BACKEND=memory means the API boots with no external -services — repositories are process-local dictionaries. Switch to +services — repositories are process-local dictionaries. Switch to REPO_BACKEND=mongo for persistence (see .env.example).

The client pairs with it:

1
@@ -930,14 +930,14 @@ services — repositories are process-local dictionaries. Switch t
 ({AUTH_SERVER} introspection). Without a reachable auth-server the API
 answers 401 on resource routes; /health stays public. For a local
 no-auth demo, remove the security block from openapi.yaml and the
-global security entry — see
+global security entry — see
 Auth Integration.

Next

diff --git a/standards/wiki/06_reference_app/02_domain_and_api/index.html b/standards/wiki/06_reference_app/02_domain_and_api/index.html index 355b939..21a343b 100644 --- a/standards/wiki/06_reference_app/02_domain_and_api/index.html +++ b/standards/wiki/06_reference_app/02_domain_and_api/index.html @@ -74,7 +74,7 @@
- + Skip to content @@ -860,7 +860,7 @@ -

Domain & API — the resource axis

+

Domain & API — the resource axis

Every aggregate follows the same four-file slice (CRUD Template); walk parents first, then appointments when you need foreign keys.

@@ -877,9 +877,9 @@ db.py # ParentDocument(Parent, BaseDocument) + create_parent_repo() service.py # ParentCrudsService(CrudsServiceAdaptor[...]) + get_parent_service() interfaces/parents/ - routes.py # list/create/get/update/delete — names == operationIds + routes.py # list/create/get/update/delete — names == operationIds
-

Wiring happens in exactly one place — the composition root +

Wiring happens in exactly one place — the composition root init.py:

parent_repo = create_parent_repo()
@@ -899,7 +899,7 @@ single source of truth: main.py builds an OpenAPIFirstAppoperationId to a function of the same name in interfaces/openapi.py (a flat
 re-export of every routes module). There are no decorator routers.

The x- extension fields on schemas and properties are what make the admin -client render itself — see Auto-Rendered Admin and +client render itself — see Auto-Rendered Admin and the worked Job schema (enum selects, hidden fields, filterable columns).

Foreign keys (appointments)

Appointment declares pet, vet, treatment as dict | int | None. The @@ -907,7 +907,7 @@ write path normalizes bare ids through expand_ref; the read path re fully expanded objects:

pet = await expand_ref(svc(request, "pet"), body.get("pet"), "pet")
 
-

The list endpoint additionally filters by date prefix and embedded fk ids — +

The list endpoint additionally filters by date prefix and embedded fk ids — client-side over the repository result, the pattern for small reference data.

Ids on the wire

The domain models declare id: int | None = None and documents use the @@ -917,9 +917,9 @@ keeps the OpenAPI id: integer contract, the in-memory demo store, a Mongo persistence layer consistent.

Tests

    -
  • tests/integration/test_openapi_app_builds.py — generic gate: every +
  • tests/integration/test_openapi_app_builds.py — generic gate: every operationId resolves and the app builds. Copy it verbatim.
  • -
  • tests/unit/test_jobs_routes.py — the pattern for route unit tests with a +
  • tests/unit/test_jobs_routes.py — the pattern for route unit tests with a service double (no HTTP client needed).
diff --git a/standards/wiki/06_reference_app/03_pipelines_and_worker/index.html b/standards/wiki/06_reference_app/03_pipelines_and_worker/index.html index a068741..406fa43 100644 --- a/standards/wiki/06_reference_app/03_pipelines_and_worker/index.html +++ b/standards/wiki/06_reference_app/03_pipelines_and_worker/index.html @@ -74,7 +74,7 @@
- + Skip to content @@ -851,7 +851,7 @@ -

Pipelines & Worker — the pipeline axis

+

Pipelines & Worker — the pipeline axis

The sample proves the full loop: API creates a job document → worker polls → pipeline family runs → job reaches a terminal status.

The jobs slice

@@ -901,7 +901,7 @@ inspects the queue.

reminder = await pipeline.run(appointment_id)

To add a family: copy appointment_reminder/, replace the stages, call -register(kind, variant, root_cls) — see +register(kind, variant, root_cls) — see Pipeline Scaffold.

The worker

worker.py is the process that turns @@ -909,7 +909,7 @@ documents into execution:

  1. poll job_svc.search({"status": "pending"}) on WORKER_POLL_SECONDS
  2. claim a job: status → processing
  3. -
  4. dispatch through the HANDLERS table — HANDLERS[job.kind](job, resources)
  5. +
  6. dispatch through the HANDLERS table — HANDLERS[job.kind](job, resources)
  7. on success status → done; on exception status → failed, retry_count += 1, error_message captured; jobs at WORKER_MAX_RETRIES are skipped
  8. @@ -923,19 +923,19 @@ documents into execution:

    }

    The worker shares the composition root: from init import init gives it the -same wired services the API uses — one place to add a dependency.

    +same wired services the API uses — one place to add a dependency.

    Same process model in production

    API and worker are separate processes (see docker-compose.yaml: api, worker, mongo). Any in-memory progress bus is per-process; to surface live progress through the API, swap the bus adaptor for a broker-backed -one — see Worker & Progress.

    +one — see Worker & Progress.

    Tests

      -
    • tests/unit/test_appointment_reminder_pipeline.py — stage units + the wired +
    • tests/unit/test_appointment_reminder_pipeline.py — stage units + the wired root against a fake service.
    • -
    • tests/unit/test_worker.py — dispatch, done/failed transitions, retry +
    • tests/unit/test_worker.py — dispatch, done/failed transitions, retry exhaustion, using an in-memory job service double and a cancelled poll task.
    diff --git a/standards/wiki/06_reference_app/04_client_admin/index.html b/standards/wiki/06_reference_app/04_client_admin/index.html index 2bc268d..6c72a86 100644 --- a/standards/wiki/06_reference_app/04_client_admin/index.html +++ b/standards/wiki/06_reference_app/04_client_admin/index.html @@ -74,7 +74,7 @@
    - + Skip to content @@ -851,18 +851,18 @@ -

    Client Admin — the spec-driven frontend

    -

    The client sample (standards/src/templates/client/) renders a full admin — -auth pages, resource lists, forms, detail views — from the server's +

    Client Admin — the spec-driven frontend

    +

    The client sample (standards/src/templates/client/) renders a full admin — +auth pages, resource lists, forms, detail views — from the server's openapi.yaml, driven entirely by the x- extension fields. There are no hand-written screens per resource.

    What ships

    A Vite + React + MUI app wiring the two Aetos packages:

      -
    • @aetoskia/react-openapi — fetches ${VITE_API_BASE_URL}/openapi.json, +
    • @aetoskia/react-openapi — fetches ${VITE_API_BASE_URL}/openapi.json, builds the router and the resource admin from the spec's schemas and x- - fields (list columns, autocomplete, fk pickers, enum selects, hidden fields…)
    • -
    • @aetoskia/react-auth — login/register/logout against the auth-server + fields (list columns, autocomplete, fk pickers, enum selects, hidden fields…)
    • +
    • @aetoskia/react-auth — login/register/logout against the auth-server paths declared in the spec's securitySchemes.bearerAuth, and attaches the JWT to every API call
    • a shared MUI theme (src/shared-theme/) and the shell @@ -892,7 +892,7 @@ hand-written screens per resource.

    Open the dev-server URL; the admin lists Parents, Pets, Treatments, Vets, Appointments and Jobs straight from the vet spec. Edit openapi.yaml on the -server and the client follows on reload — the spec is the contract on both +server and the client follows on reload — the spec is the contract on both sides.

    Where the x- extensions come from

    The canonical catalog lives in the react-openapi package diff --git a/standards/wiki/search/search_index.json b/standards/wiki/search/search_index.json index e27a258..363ae6e 100644 --- a/standards/wiki/search/search_index.json +++ b/standards/wiki/search/search_index.json @@ -1 +1 @@ -{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"\ud83e\udde9 Standards \u2014 the Aetos App Blueprint","text":"

    One reusable architecture for every backend application in the Aetos ecosystem: a spec-first HTTP edge, a framework-free domain core, a pluggable infrastructure layer, and a single composition root.

    Fill in two axes only and a new app is done: your resource model, and (if needed) your pipeline family. The blueprint itself never names a business domain \u2014 expenses, health metrics, inventory, and analytics are all the same shape underneath.

    Doc model: this wiki is written for humans \u2014 how-to guides, the build recipe, and quality gates. The enforceable rules (layering, composition root, CRUD template, pipeline scaffold) and the library registry live in the reference pages, mirrored by the machine-readable standards MCP bundle. Query find_library before writing any infrastructure.

    "},{"location":"#key-features","title":"\ud83d\ude80 Key Features","text":"
    • \ud83e\udded Spec-first routing \u2014 the OpenAPI document, not decorator routers, is the source of truth (operationId lookup)
    • \ud83c\udfd7\ufe0f One-way layering \u2014 interfaces \u2192 domain \u2192 infra, enforced
    • \ud83d\udd0c Single composition root \u2014 every dependency is wired in exactly one place
    • \ud83e\udde9 Generic, not app-specific \u2014 reusable rules versus two authoring axes
    • \ud83d\udcda Library-backed \u2014 DB, DI, pipelines, ingestion, auth, and docs come from internal libraries, never hand-written
    • \ud83d\udea6 Explicit state machines \u2014 durable job statuses, progress, and human-in-the-loop pauses
    "},{"location":"#documentation-structure","title":"\ud83d\udcc1 Documentation Structure","text":"Section Description Overview Philosophy, the one-way dependency rule, architecture App Components The component model and how the pieces wire together Start an App The entry point: gather the two axes, then build Reference App The runnable vet sample \u2014 server, pipelines, client Testing & Quality Test layout and acceptance gates

    Reference (separate site, mirrors the MCP bundle): the enforceable anatomy \u2014 start at the Build Recipe, then the pillars: domains (Layering), the OpenAPI contract (Composition Root, Spec-First Routing, CRUD Template, Auth Integration), interfaces & infra (Infra Adapters), UI (Auto-Rendered Admin), the optional pipelines layer (Pipeline Scaffold, Worker & Progress, Human-in-the-Loop), and delivery/meta (Genericity Matrix, Library Registry).

    "},{"location":"#related-resources","title":"\ud83d\udd17 Related Resources","text":"
    • MCP bundle \u2014 standards server on :8009: read docs://index, docs://nav, docs://modules/{module}; call find_library(capability)
    • Library registry \u2014 the catalog of internal libraries behind every app (libraries.json, served as registry://libraries)
    • Template \u2014 the runnable sample at standards/src/templates/app/ (server) and standards/src/templates/client/ (React admin); walkthrough in the Reference App
    • Start a build \u2014 invoke the start_app prompt on the standards server, or read the Start an App guide

    \u00a9 Aetoskia Internal \u2014 standards

    "},{"location":"01_overview/","title":"Overview","text":"

    The blueprint is a hexagonal (ports & adapters) application with a spec-first web edge and a single composition root. It favors explicit wiring over implicit magic and existing internal libraries over hand-written infrastructure. Start here to build the mental model; the enforceable details live in the reference pages.

    "},{"location":"01_overview/#architecture","title":"\ud83c\udfd7\ufe0f Architecture","text":"
    flowchart LR\n    subgraph EDGE[\"interfaces/ \u2014 HTTP edge\"]\n        OAS[\"openapi.yaml<br/>operationId contract\"]\n        RT[\"<resource>/routes.py<br/>thin handlers\"]\n    end\n\n    subgraph CORE[\"domain/ \u2014 business core\"]\n        MD[\"models\"]\n        PT[\"ports (adapter binding seam)\"]\n        SV[\"services\"]\n    end\n\n    subgraph INFRA[\"infra/ \u2014 adapters\"]\n        RP[\"mongo repos (mongo-ops)\"]\n        AD[\"cache \u00b7 stores \u00b7 llm \u00b7 mail\"]\n        DB[(\"MongoDB\")]\n    end\n\n    CR[\"init.py<br/>composition root\"]\n\n    OAS --> RT\n    RT --> SV\n    SV --> PT\n    SV --> RP\n    PT -. depends on .- RP\n    CR --- RT\n    CR --- SV\n    CR --- RP\n    RP --> DB\n    AD --- CR

    The rule holds at every edge: code in an outer ring may depend on a ring closer to the core, never the reverse. domain/ never imports a web framework; only its ports/ seam reaches into infra/ to bind adapters.

    Legend \u2014 solid arrows are compile-time calls; dotted arrows are the port/adapter binding, performed by the composition root at startup.

    "},{"location":"01_overview/#the-one-way-dependency-rule","title":"\ud83d\udd04 The one-way dependency rule","text":"Ring May import Never imports interfaces/ domain, framework glue (glue only) business logic, infra directly domain/ its own models + its ports/ seam (which binds infra/libraries) web frameworks, interfaces/ infra/ domain ports, drivers, libraries \u2014 init.py everything (the wiring point) \u2014"},{"location":"01_overview/#core-principles","title":"\ud83e\udded Core principles","text":"
    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 a port; the port's factory binds the concrete adapter, and the composition root calls those factories.
    3. One composition root. Nothing constructs its own dependencies. All wiring lives in one module (init.py) exposed as a flat resource registry.
    4. Spec-first. The HTTP contract is declared in openapi.yaml; route handlers are looked up by operationId. There are no decorator routers.
    5. Reuse the libraries. Orchestration, DI, Mongo repositories, ingestion, auth, and docs come from internal libraries \u2014 see the Library Registry.
    6. Explicit state machines. Long-running work moves through explicit, durable statuses and can pause for human input.
    "},{"location":"01_overview/#runtime-processes","title":"\u2699\ufe0f Runtime processes","text":"

    A standard app may run as up to three processes that share the same composition root:

    Process Entry point Responsibility API main.py serve HTTP; start/stop resources via lifespan Worker worker.py poll durable jobs, run pipelines, publish progress Scripts python -m scripts.<name> one-shot operator tasks"},{"location":"01_overview/#read-next","title":"\u27a1\ufe0f Read Next","text":"
    • 02 \u2013 App Components \u00b7 03 \u2013 Start an App
    "},{"location":"02_components/","title":"App Components","text":"

    What ships in every standard app, and how the pieces relate. This is the blueprint's component model \u2014 templates/app/ is a runnable instance of it.

    "},{"location":"02_components/#component-model","title":"\ud83d\uddc2\ufe0f Component model","text":"

    The blueprint has two orthogonal views: runtime flow (who calls whom) and composition-root wiring (who constructs whom). Keeping them separate makes the layering visible instead of hiding it behind a hub.

    "},{"location":"02_components/#runtime-flow","title":"\ud83d\udd00 Runtime flow","text":"

    Who calls whom at request and job time:

    %%{init: {\"flowchart\": {\"nodeSpacing\": 50, \"rankSpacing\": 80}}}%%\nflowchart TB\n    subgraph REQ[\"Request path \u2014 API process\"]\n        direction LR\n        CL([\"Client<br/>operationId\"]) --> RT[\"interfaces/&lt;resource&gt;/routes.py\"]\n        RT --> SVC[\"&lt;resource&gt;_svc\"]\n        SVC --> REPO[\"&lt;resource&gt;_repo\"]\n    end\n\n    subgraph WRK[\"Background path \u2014 worker process\"]\n        direction LR\n        W[\"worker.py\"] --> JSVC[\"job_svc\"]\n        W --> PIPE[\"pipelines/&lt;family&gt;\"]\n        PIPE --> ISVC[\"item_svc\"]\n        JSVC --> JREPO[\"job_repo\"]\n        ISVC --> IREPO[\"item_repo\"]\n    end\n\n    subgraph INFRA[\"infra/ \u2014 adapters\"]\n        MONGO[(\"MongoDB / mongo_ops\")]\n    end\n\n    REPO --> MONGO\n    JREPO --> MONGO\n    IREPO --> MONGO

    Legend \u2014 solid arrows are runtime calls. Nothing is self-constructed.

    "},{"location":"02_components/#composition-root-wiring","title":"\ud83e\uddf5 Composition-root wiring","text":"

    Who constructs what at startup:

    %%{init: {\"flowchart\": {\"nodeSpacing\": 50, \"rankSpacing\": 80}}}%%\nflowchart TB\n    INIT[\"init.py \u2014 composition root\"]\n    SETUP[\"infra/db/setup.py\"]\n    FACT[\"domain/*/ports/*<br/>factory seams\"]\n    REPOS[\"repos<br/>infra/db/repos/mongo.py\"]\n    SVCES[\"services<br/>CrudsServiceAdaptor\"]\n    STATE[\"app.state.db_resources\"]\n    HEXA[\"hexa.build\"]\n    RT[\"routes.py\"]\n    WK[\"worker.py\"]\n\n    INIT --> SETUP\n    INIT --> FACT\n    FACT --> REPOS\n    FACT --> SVCES\n    REPOS --> SVCES\n    HEXA -.-> SVCES\n    INIT --> STATE\n    STATE --> RT\n    INIT --> WK

    Legend \u2014 init.py is the only place that imports concrete adapters. The ports/ factory seams bind infra; the API reads resources from app.state.db_resources, the worker calls init() directly.

    "},{"location":"02_components/#component-roles","title":"\ud83d\udce6 Component roles","text":""},{"location":"02_components/#1-interfaces-the-edge","title":"1. interfaces/ \u2014 the edge","text":"

    Thin handlers bound to openapi.yaml operationIds. They parse the request, call a service, and shape the response. No business rules, no I/O.

    • interfaces/openapi.py re-exports every route module so the OpenAPI-first app can resolve handlers.
    • Routes may mount the cache decorator on read endpoints.
    "},{"location":"02_components/#2-domain-the-core","title":"2. domain/ \u2014 the core","text":"

    Models, ports, and services (use cases). No framework imports. The ports/ package is the one seam allowed to reach into infra/: each port module declares the document shape and a factory that binds the adapter.

    • <aggregate>/models.py \u2014 dataclasses, enums, document shapes.
    • <aggregate>/ports/db.py \u2014 document model + create_<aggregate>_repo().
    • <aggregate>/ports/service.py \u2014 service + get_<aggregate>_service(repo).
    • Services compose ports and enforce the business rules; the port factories bind the implementations, and the composition root calls them.
    "},{"location":"02_components/#3-infra-the-adapters","title":"3. infra/ \u2014 the adapters","text":"

    Concrete implementations of the domain ports plus cross-cutting infrastructure:

    • db/ \u2014 mongo-ops repositories and the connection lifecycle.
    • adapters/ \u2014 cache store, progress bus, storages, fetchers (library-backed).
    • Anything else the domain needs, implemented against its ports.
    "},{"location":"02_components/#4-pipelines-data-driven-families-optional","title":"4. pipelines/ \u2014 data-driven families (optional)","text":"

    Extraction and enrichment families, each a deterministic step chain (abc.py / impl.py / pipeline.yaml / resources/). Roots are registered in the pattern registry and driven either by a worker job or a CLI.

    "},{"location":"02_components/#5-workerpy-the-job-worker-optional","title":"5. worker.py \u2014 the job worker (optional)","text":"

    A separate process that polls durable job collections, runs pipeline roots, and publishes progress to the progress bus. It shares the same composition root as the API.

    "},{"location":"02_components/#6-initpy-the-composition-root","title":"6. init.py \u2014 the composition root","text":"

    The only place anything is constructed: leaf repositories \u2192 dependent repositories \u2192 services \u2192 adapters. Exposes get_svc overloads and starts / stops resources on the app lifespan.

    "},{"location":"02_components/#how-the-request-flows","title":"\ud83d\udd17 How the request flows","text":"
    sequenceDiagram\n    participant C as Client\n    participant R as route (interfaces)\n    participant S as service (port factory / adapter)\n    participant P as repo (infra)\n    participant DB as MongoDB\n\n    C->>R: POST /items (operationId)\n    R->>S: svc.create(payload)\n    S->>P: repo.insert(doc)\n    P->>DB: insert\n    DB-->>P: ack\n    P-->>S: doc\n    S-->>R: result\n    R-->>C: 200 JSON
    "},{"location":"02_components/#related","title":"Related","text":"
    • 01 \u2013 Overview \u00b7 03 \u2013 Start an App \u00b7 04 \u2013 Testing & Quality
    "},{"location":"03_start_an_app/","title":"\ud83d\ude80 Start an App","text":"

    This is the entry point for building a new application from the blueprint. The goal is one deterministic pass: you supply only the two axes, and the agent does the rest by reading the standards MCP, importing Tier A libraries, and copying Tier B template components.

    Doc model: this page is the human entry point. The deterministic, machine-facing checklist lives in the reference page Build Recipe, served as docs://modules/build_recipe.

    "},{"location":"03_start_an_app/#what-the-agent-needs-from-you","title":"\ud83e\uddfe What the agent needs from you","text":"

    Gather these before starting \u2014 the start_app MCP prompt asks for them:

    1. App name + one-line purpose.
    2. Axis 1 \u2014 resources. Each resource, its fields, and relationships. Paste openapi.yaml if you already have it.
    3. Axis 2 \u2014 pipelines. Only if an input needs a multi-step transformation: the families and their input formats (or \"none\").
    4. Auth needed now? Default no. Auth is deferred (py-jwt primitives only), and an authless app is just a spec with no security keys \u2014 see Spec-First Routing \u2192 Authless apps.
    5. Integrations. Mongo, ingestion formats, email, LLM provider.

    If you can state axes 1 and 2, the rest is mechanical.

    "},{"location":"03_start_an_app/#how-to-start","title":"\u25b6\ufe0f How to start","text":"

    From an MCP client connected to the standards server:

    1. Invoke the intake prompt (or start_app with no arguments) to get the intake questions.
    2. Answer them, then invoke build (build(app_name=..., resources=..., pipelines=..., auth=...)), or use start_app to intake-and-build in one call.
    3. The prompt hands off to docs://modules/build_recipe; the agent scaffolds with get_app_template() and pulls exact files with get_app_component(kind), authoring only the app-specific parts.
    4. To validate the standard without touching a real app, invoke trial (trial(app_name=..., build_path=...)) to build into a scratch directory, prove it builds, and report findings.

    Manual alternative \u2014 copy the skeleton and follow the reference recipe:

    cp -r <docs>/standards/src/templates/app apps/<your-app>\n
    "},{"location":"03_start_an_app/#reference","title":"\ud83d\udcda Reference","text":"
    • Build Recipe \u2014 the deterministic checklist
    • Genericity Matrix \u2014 Tier A / B / C
    • Library Registry \u2014 the Tier A catalog
    • App Components \u2014 the component model
    • Reference App \u2014 the runnable vet sample to copy
    "},{"location":"03_start_an_app/#related","title":"Related","text":"
    • 01 \u2013 Overview \u00b7 02 \u2013 App Components \u00b7 04 \u2013 Testing & Quality
    "},{"location":"05_mcp_setup/","title":"\ud83d\udd0c MCP Setup","text":"

    Every repository ships its documentation as a Model Context Protocol (MCP) server, and the standards registry server ties them together. The docs portal runs each one as a FastMCP streamable-http app bound to 0.0.0.0:<port>, exposing the endpoint at /mcp (see docs/main.py and docs/config.yml).

    Connect your agent to the standards server first: it answers find_library, list_repos, and the start_app prompt, so the agent reaches for an internal library instead of reimplementing one (see the Library Registry).

    "},{"location":"05_mcp_setup/#run-the-servers-locally","title":"\u25b6\ufe0f Run the servers locally","text":"

    From the docs checkout:

    .venv\\Scripts\\python.exe main.py                      # every server\n.venv\\Scripts\\python.exe main.py --servers standards   # just the registry\n

    :8000 is the health endpoint (GET /healthz), not an MCP server.

    "},{"location":"05_mcp_setup/#servers","title":"\ud83e\udded Servers","text":"Slug MCP server Port localhost mcp.aetoskia.com standards standards 8009 http://localhost:8009/mcp https://mcp.aetoskia.com/standards/mcp doc-forge docforge 8001 http://localhost:8001/mcp https://mcp.aetoskia.com/doc-forge/mcp mail-intake mail_intake 8002 http://localhost:8002/mcp https://mcp.aetoskia.com/mail-intake/mcp omniread omniread 8003 http://localhost:8003/mcp https://mcp.aetoskia.com/omniread/mcp openapi-first openapi_first 8004 http://localhost:8004/mcp https://mcp.aetoskia.com/openapi-first/mcp py-jwt jwtlib 8005 http://localhost:8005/mcp https://mcp.aetoskia.com/py-jwt/mcp dagpipe dagpipe 8006 http://localhost:8006/mcp https://mcp.aetoskia.com/dagpipe/mcp mongo-ops mongo_ops 8007 http://localhost:8007/mcp https://mcp.aetoskia.com/mongo-ops/mcp hexa hexa 8008 http://localhost:8008/mcp https://mcp.aetoskia.com/hexa/mcp doc-weaver doc-weaver 8010 http://localhost:8010/mcp https://mcp.aetoskia.com/doc-weaver/mcp react-auth react-auth 8011 http://localhost:8011/mcp https://mcp.aetoskia.com/react-auth/mcp react-openapi react-openapi 8012 http://localhost:8012/mcp https://mcp.aetoskia.com/react-openapi/mcp

    The hosted endpoints are served over HTTPS at https://mcp.aetoskia.com/<slug>/mcp. The tabs below are linked (content.tabs.link): switching one switches all of them. Every snippet registers all twelve servers, so a single paste is enough.

    "},{"location":"05_mcp_setup/#claude-code","title":"\ud83e\udd16 Claude Code","text":"

    Project-scoped .mcp.json at the repo root. The type field is required \u2014 an entry with a url but no type is read as a stdio server and skipped.

    localhostmcp.aetoskia.com
    {\n  \"mcpServers\": {\n    \"standards\": { \"type\": \"http\", \"url\": \"http://localhost:8009/mcp\" },\n    \"doc-forge\": { \"type\": \"http\", \"url\": \"http://localhost:8001/mcp\" },\n    \"mail-intake\": { \"type\": \"http\", \"url\": \"http://localhost:8002/mcp\" },\n    \"omniread\": { \"type\": \"http\", \"url\": \"http://localhost:8003/mcp\" },\n    \"openapi-first\": { \"type\": \"http\", \"url\": \"http://localhost:8004/mcp\" },\n    \"py-jwt\": { \"type\": \"http\", \"url\": \"http://localhost:8005/mcp\" },\n    \"dagpipe\": { \"type\": \"http\", \"url\": \"http://localhost:8006/mcp\" },\n    \"mongo-ops\": { \"type\": \"http\", \"url\": \"http://localhost:8007/mcp\" },\n    \"hexa\": { \"type\": \"http\", \"url\": \"http://localhost:8008/mcp\" },\n    \"doc-weaver\": { \"type\": \"http\", \"url\": \"http://localhost:8010/mcp\" },\n    \"react-auth\": { \"type\": \"http\", \"url\": \"http://localhost:8011/mcp\" },\n    \"react-openapi\": { \"type\": \"http\", \"url\": \"http://localhost:8012/mcp\" }\n  }\n}\n
    {\n  \"mcpServers\": {\n    \"standards\": { \"type\": \"http\", \"url\": \"https://mcp.aetoskia.com/standards/mcp\" },\n    \"doc-forge\": { \"type\": \"http\", \"url\": \"https://mcp.aetoskia.com/doc-forge/mcp\" },\n    \"mail-intake\": { \"type\": \"http\", \"url\": \"https://mcp.aetoskia.com/mail-intake/mcp\" },\n    \"omniread\": { \"type\": \"http\", \"url\": \"https://mcp.aetoskia.com/omniread/mcp\" },\n    \"openapi-first\": { \"type\": \"http\", \"url\": \"https://mcp.aetoskia.com/openapi-first/mcp\" },\n    \"py-jwt\": { \"type\": \"http\", \"url\": \"https://mcp.aetoskia.com/py-jwt/mcp\" },\n    \"dagpipe\": { \"type\": \"http\", \"url\": \"https://mcp.aetoskia.com/dagpipe/mcp\" },\n    \"mongo-ops\": { \"type\": \"http\", \"url\": \"https://mcp.aetoskia.com/mongo-ops/mcp\" },\n    \"hexa\": { \"type\": \"http\", \"url\": \"https://mcp.aetoskia.com/hexa/mcp\" },\n    \"doc-weaver\": { \"type\": \"http\", \"url\": \"https://mcp.aetoskia.com/doc-weaver/mcp\" },\n    \"react-auth\": { \"type\": \"http\", \"url\": \"https://mcp.aetoskia.com/react-auth/mcp\" },\n    \"react-openapi\": { \"type\": \"http\", \"url\": \"https://mcp.aetoskia.com/react-openapi/mcp\" }\n  }\n}\n

    Or add them with the CLI: claude mcp add --transport http standards http://localhost:8009/mcp.

    "},{"location":"05_mcp_setup/#codex","title":"\ud83e\udd16 Codex","text":"

    ~/.codex/config.toml (or a project-scoped .codex/config.toml).

    localhostmcp.aetoskia.com
    [mcp_servers.standards]\nurl = \"http://localhost:8009/mcp\"\n\n[mcp_servers.doc-forge]\nurl = \"http://localhost:8001/mcp\"\n\n[mcp_servers.mail-intake]\nurl = \"http://localhost:8002/mcp\"\n\n[mcp_servers.omniread]\nurl = \"http://localhost:8003/mcp\"\n\n[mcp_servers.openapi-first]\nurl = \"http://localhost:8004/mcp\"\n\n[mcp_servers.py-jwt]\nurl = \"http://localhost:8005/mcp\"\n\n[mcp_servers.dagpipe]\nurl = \"http://localhost:8006/mcp\"\n\n[mcp_servers.mongo-ops]\nurl = \"http://localhost:8007/mcp\"\n\n[mcp_servers.hexa]\nurl = \"http://localhost:8008/mcp\"\n\n[mcp_servers.doc-weaver]\nurl = \"http://localhost:8010/mcp\"\n\n[mcp_servers.react-auth]\nurl = \"http://localhost:8011/mcp\"\n\n[mcp_servers.react-openapi]\nurl = \"http://localhost:8012/mcp\"\n
    [mcp_servers.standards]\nurl = \"https://mcp.aetoskia.com/standards/mcp\"\n\n[mcp_servers.doc-forge]\nurl = \"https://mcp.aetoskia.com/doc-forge/mcp\"\n\n[mcp_servers.mail-intake]\nurl = \"https://mcp.aetoskia.com/mail-intake/mcp\"\n\n[mcp_servers.omniread]\nurl = \"https://mcp.aetoskia.com/omniread/mcp\"\n\n[mcp_servers.openapi-first]\nurl = \"https://mcp.aetoskia.com/openapi-first/mcp\"\n\n[mcp_servers.py-jwt]\nurl = \"https://mcp.aetoskia.com/py-jwt/mcp\"\n\n[mcp_servers.dagpipe]\nurl = \"https://mcp.aetoskia.com/dagpipe/mcp\"\n\n[mcp_servers.mongo-ops]\nurl = \"https://mcp.aetoskia.com/mongo-ops/mcp\"\n\n[mcp_servers.hexa]\nurl = \"https://mcp.aetoskia.com/hexa/mcp\"\n\n[mcp_servers.doc-weaver]\nurl = \"https://mcp.aetoskia.com/doc-weaver/mcp\"\n\n[mcp_servers.react-auth]\nurl = \"https://mcp.aetoskia.com/react-auth/mcp\"\n\n[mcp_servers.react-openapi]\nurl = \"https://mcp.aetoskia.com/react-openapi/mcp\"\n

    Or add them with the CLI: codex mcp add standards --url http://localhost:8009/mcp.

    "},{"location":"05_mcp_setup/#antigravity","title":"\ud83e\udd16 Antigravity","text":"

    ~/.gemini/antigravity/mcp_config.json (IDE) or ~/.gemini/config/mcp_config.json (CLI). Antigravity uses serverUrl, not url, for HTTP endpoints.

    localhostmcp.aetoskia.com
    {\n  \"mcpServers\": {\n    \"standards\": { \"serverUrl\": \"http://localhost:8009/mcp\" },\n    \"doc-forge\": { \"serverUrl\": \"http://localhost:8001/mcp\" },\n    \"mail-intake\": { \"serverUrl\": \"http://localhost:8002/mcp\" },\n    \"omniread\": { \"serverUrl\": \"http://localhost:8003/mcp\" },\n    \"openapi-first\": { \"serverUrl\": \"http://localhost:8004/mcp\" },\n    \"py-jwt\": { \"serverUrl\": \"http://localhost:8005/mcp\" },\n    \"dagpipe\": { \"serverUrl\": \"http://localhost:8006/mcp\" },\n    \"mongo-ops\": { \"serverUrl\": \"http://localhost:8007/mcp\" },\n    \"hexa\": { \"serverUrl\": \"http://localhost:8008/mcp\" },\n    \"doc-weaver\": { \"serverUrl\": \"http://localhost:8010/mcp\" },\n    \"react-auth\": { \"serverUrl\": \"http://localhost:8011/mcp\" },\n    \"react-openapi\": { \"serverUrl\": \"http://localhost:8012/mcp\" }\n  }\n}\n
    {\n  \"mcpServers\": {\n    \"standards\": { \"serverUrl\": \"https://mcp.aetoskia.com/standards/mcp\" },\n    \"doc-forge\": { \"serverUrl\": \"https://mcp.aetoskia.com/doc-forge/mcp\" },\n    \"mail-intake\": { \"serverUrl\": \"https://mcp.aetoskia.com/mail-intake/mcp\" },\n    \"omniread\": { \"serverUrl\": \"https://mcp.aetoskia.com/omniread/mcp\" },\n    \"openapi-first\": { \"serverUrl\": \"https://mcp.aetoskia.com/openapi-first/mcp\" },\n    \"py-jwt\": { \"serverUrl\": \"https://mcp.aetoskia.com/py-jwt/mcp\" },\n    \"dagpipe\": { \"serverUrl\": \"https://mcp.aetoskia.com/dagpipe/mcp\" },\n    \"mongo-ops\": { \"serverUrl\": \"https://mcp.aetoskia.com/mongo-ops/mcp\" },\n    \"hexa\": { \"serverUrl\": \"https://mcp.aetoskia.com/hexa/mcp\" },\n    \"doc-weaver\": { \"serverUrl\": \"https://mcp.aetoskia.com/doc-weaver/mcp\" },\n    \"react-auth\": { \"serverUrl\": \"https://mcp.aetoskia.com/react-auth/mcp\" },\n    \"react-openapi\": { \"serverUrl\": \"https://mcp.aetoskia.com/react-openapi/mcp\" }\n  }\n}\n
    "},{"location":"05_mcp_setup/#opencode","title":"\ud83e\udd16 OpenCode","text":"

    opencode.json (project) or ~/.config/opencode/opencode.json (global).

    localhostmcp.aetoskia.com
    {\n  \"$schema\": \"https://opencode.ai/config.json\",\n  \"mcp\": {\n    \"standards\": { \"type\": \"remote\", \"url\": \"http://localhost:8009/mcp\", \"enabled\": true },\n    \"doc-forge\": { \"type\": \"remote\", \"url\": \"http://localhost:8001/mcp\", \"enabled\": true },\n    \"mail-intake\": { \"type\": \"remote\", \"url\": \"http://localhost:8002/mcp\", \"enabled\": true },\n    \"omniread\": { \"type\": \"remote\", \"url\": \"http://localhost:8003/mcp\", \"enabled\": true },\n    \"openapi-first\": { \"type\": \"remote\", \"url\": \"http://localhost:8004/mcp\", \"enabled\": true },\n    \"py-jwt\": { \"type\": \"remote\", \"url\": \"http://localhost:8005/mcp\", \"enabled\": true },\n    \"dagpipe\": { \"type\": \"remote\", \"url\": \"http://localhost:8006/mcp\", \"enabled\": true },\n    \"mongo-ops\": { \"type\": \"remote\", \"url\": \"http://localhost:8007/mcp\", \"enabled\": true },\n    \"hexa\": { \"type\": \"remote\", \"url\": \"http://localhost:8008/mcp\", \"enabled\": true },\n    \"doc-weaver\": { \"type\": \"remote\", \"url\": \"http://localhost:8010/mcp\", \"enabled\": true },\n    \"react-auth\": { \"type\": \"remote\", \"url\": \"http://localhost:8011/mcp\", \"enabled\": true },\n    \"react-openapi\": { \"type\": \"remote\", \"url\": \"http://localhost:8012/mcp\", \"enabled\": true }\n  }\n}\n
    {\n  \"$schema\": \"https://opencode.ai/config.json\",\n  \"mcp\": {\n    \"standards\": { \"type\": \"remote\", \"url\": \"https://mcp.aetoskia.com/standards/mcp\", \"enabled\": true },\n    \"doc-forge\": { \"type\": \"remote\", \"url\": \"https://mcp.aetoskia.com/doc-forge/mcp\", \"enabled\": true },\n    \"mail-intake\": { \"type\": \"remote\", \"url\": \"https://mcp.aetoskia.com/mail-intake/mcp\", \"enabled\": true },\n    \"omniread\": { \"type\": \"remote\", \"url\": \"https://mcp.aetoskia.com/omniread/mcp\", \"enabled\": true },\n    \"openapi-first\": { \"type\": \"remote\", \"url\": \"https://mcp.aetoskia.com/openapi-first/mcp\", \"enabled\": true },\n    \"py-jwt\": { \"type\": \"remote\", \"url\": \"https://mcp.aetoskia.com/py-jwt/mcp\", \"enabled\": true },\n    \"dagpipe\": { \"type\": \"remote\", \"url\": \"https://mcp.aetoskia.com/dagpipe/mcp\", \"enabled\": true },\n    \"mongo-ops\": { \"type\": \"remote\", \"url\": \"https://mcp.aetoskia.com/mongo-ops/mcp\", \"enabled\": true },\n    \"hexa\": { \"type\": \"remote\", \"url\": \"https://mcp.aetoskia.com/hexa/mcp\", \"enabled\": true },\n    \"doc-weaver\": { \"type\": \"remote\", \"url\": \"https://mcp.aetoskia.com/doc-weaver/mcp\", \"enabled\": true },\n    \"react-auth\": { \"type\": \"remote\", \"url\": \"https://mcp.aetoskia.com/react-auth/mcp\", \"enabled\": true },\n    \"react-openapi\": { \"type\": \"remote\", \"url\": \"https://mcp.aetoskia.com/react-openapi/mcp\", \"enabled\": true }\n  }\n}\n

    Or add them with the CLI: opencode mcp add standards --url http://localhost:8009/mcp --global.

    "},{"location":"05_mcp_setup/#pycharm-junie-ai-assistant","title":"\ud83e\udd16 PyCharm (Junie / AI Assistant)","text":"

    ~/.junie/mcp/mcp.json (user scope) or .junie/mcp/mcp.json (project scope). The same config is reachable from Settings | Tools | MCP Server \u2192 Manual Client Configuration \u2192 Copy HTTP Stream Config.

    localhostmcp.aetoskia.com
    {\n  \"mcpServers\": {\n    \"standards\": { \"url\": \"http://localhost:8009/mcp\" },\n    \"doc-forge\": { \"url\": \"http://localhost:8001/mcp\" },\n    \"mail-intake\": { \"url\": \"http://localhost:8002/mcp\" },\n    \"omniread\": { \"url\": \"http://localhost:8003/mcp\" },\n    \"openapi-first\": { \"url\": \"http://localhost:8004/mcp\" },\n    \"py-jwt\": { \"url\": \"http://localhost:8005/mcp\" },\n    \"dagpipe\": { \"url\": \"http://localhost:8006/mcp\" },\n    \"mongo-ops\": { \"url\": \"http://localhost:8007/mcp\" },\n    \"hexa\": { \"url\": \"http://localhost:8008/mcp\" },\n    \"doc-weaver\": { \"url\": \"http://localhost:8010/mcp\" },\n    \"react-auth\": { \"url\": \"http://localhost:8011/mcp\" },\n    \"react-openapi\": { \"url\": \"http://localhost:8012/mcp\" }\n  }\n}\n
    {\n  \"mcpServers\": {\n    \"standards\": { \"url\": \"https://mcp.aetoskia.com/standards/mcp\" },\n    \"doc-forge\": { \"url\": \"https://mcp.aetoskia.com/doc-forge/mcp\" },\n    \"mail-intake\": { \"url\": \"https://mcp.aetoskia.com/mail-intake/mcp\" },\n    \"omniread\": { \"url\": \"https://mcp.aetoskia.com/omniread/mcp\" },\n    \"openapi-first\": { \"url\": \"https://mcp.aetoskia.com/openapi-first/mcp\" },\n    \"py-jwt\": { \"url\": \"https://mcp.aetoskia.com/py-jwt/mcp\" },\n    \"dagpipe\": { \"url\": \"https://mcp.aetoskia.com/dagpipe/mcp\" },\n    \"mongo-ops\": { \"url\": \"https://mcp.aetoskia.com/mongo-ops/mcp\" },\n    \"hexa\": { \"url\": \"https://mcp.aetoskia.com/hexa/mcp\" },\n    \"doc-weaver\": { \"url\": \"https://mcp.aetoskia.com/doc-weaver/mcp\" },\n    \"react-auth\": { \"url\": \"https://mcp.aetoskia.com/react-auth/mcp\" },\n    \"react-openapi\": { \"url\": \"https://mcp.aetoskia.com/react-openapi/mcp\" }\n  }\n}\n
    "},{"location":"05_mcp_setup/#verify","title":"\u2705 Verify","text":"
    • Health check: curl http://localhost:8000/healthz \u2192 ok.
    • Claude Code: claude mcp list \u2192 each server \u2026 \u2713 Connected.
    • Codex: codex mcp list \u2192 each server \u2026 http.
    • OpenCode: opencode mcp list \u2192 each server \u2026 connected.
    • In-session: run /mcp to list the connected servers and their tools.
    "},{"location":"05_mcp_setup/#reference","title":"\ud83d\udcda Reference","text":"
    • Library Registry \u2014 the catalog each server mirrors, and where find_library resolves
    • Start an App \u2014 connect standards, then invoke start_app
    • App Components \u2014 the component model
    "},{"location":"05_mcp_setup/#related","title":"Related","text":"
    • Start an App \u00b7 Testing & Quality \u00b7 Library Registry
    "},{"location":"04_development/01_testing/","title":"Testing & Quality Gates","text":"

    Two suites run under pytest with asyncio auto mode: integration (the HTTP edge against fakes) and unit (pure domain and pipeline logic). Finish a blueprint app only when every gate below passes.

    "},{"location":"04_development/01_testing/#layout","title":"\ud83d\uddc2\ufe0f Layout","text":"
    tests/\n    integration/     # TestClient(app) + mocked resources\n    unit/            # models, converters, pipeline stages/roots\n    conftest.py      # fixtures\n
    "},{"location":"04_development/01_testing/#integration-tests","title":"\ud83d\ude80 Integration tests","text":"
    • Build the app with TestClient.
    • Replace app.state.db_resources with fake repositories/services so no database is required.
    • Patch the auth introspection call (httpx.AsyncClient.post) to return a canned identity.
    • Assert status codes and bodies for list/get/create/update/delete, plus 400, 404, 204, and the streaming endpoint's fallback.
    client = TestClient(app)\napp.state.db_resources = {\"item_svc\": FakeItemService(), ...}\n\ndef test_create_item():\n    r = client.post(\"/items\", json={\"name\": \"x\"})\n    assert r.status_code == 200\n
    "},{"location":"04_development/01_testing/#unit-tests","title":"\ud83e\uddea Unit tests","text":"
    • Domain models and converters: pure, no I/O.
    • Pipeline stages: feed a fixture state, assert the transformed state.
    • Assembled pipeline roots: fixture parity \u2014 replay a stored fixture input and compare against a golden output file.
    "},{"location":"04_development/01_testing/#config-consistency-tests","title":"\ud83d\udd10 Config-consistency tests","text":"

    For rule-driven pipelines, add a test that cross-references rules against the declared pattern registry: every flag emitted must be defined; every defined flag must be reachable. This catches drift without running the pipeline.

    "},{"location":"04_development/01_testing/#quality-gates","title":"\u2705 Quality gates","text":"

    A definition of done for any standard app:

    • python -c \"import main\" succeeds.
    • pytest passes.
    • The app serves the spec's operations; the worker drains its queue.
    • No domain module imports a web framework.
    • No infrastructure was reimplemented that a library already provides.
    "},{"location":"04_development/01_testing/#conventions","title":"\ud83d\udca1 Conventions","text":"
    • asyncio_mode = \"auto\"; no manual event loops.
    • Fixtures are checked into the repo (inputs + golden outputs).
    • Tests skip (not fail) when a required fixture is absent.
    • Do not mock the domain; mock only the infrastructure edge.
    "},{"location":"04_development/01_testing/#related","title":"Related","text":"
    • 02 \u2013 App Components \u00b7 03 \u2013 Start an App \u00b7 Overview
    "},{"location":"04_development/02_releasing/","title":"Releasing & Versioning","text":"

    How to cut a release of any Aetos repository. The rule lives in the reference page Versioning & Changelog; this is the runbook you follow.

    Every repository is one of two Drone shapes \u2014 a PyPI package or a Docker deployment (Drone Pipelines). Both release the same way: the version bump and changelog promotion go in the merge request, and the tag is created on the merged commit.

    "},{"location":"04_development/02_releasing/#before-you-start","title":"\ud83e\uddfe Before you start","text":"
    • You have an open merge request holding the change, and main is green (the Drone quality gate passed on the last run).
    • You know whether this is a breaking, feature, or fix release.
    • The version file exists:
    • package \u2192 version in pyproject.toml
    • service \u2192 VERSION at the repo root
    "},{"location":"04_development/02_releasing/#pick-the-next-version","title":"\ud83d\udea6 Pick the next version","text":"

    The version file and the tag must be identical. For 0.y.z, 0.y is the major line (see the reference page); once at 1.0.0, normal SemVer applies.

    Change 0.y.z \u2192 1.0.0+ \u2192 Breaking 0.(y+1).0 (x+1).0.0 Feature 0.y.(z+1) x.(y+1).0 Fix 0.y.(z+1) x.y.(z+1)"},{"location":"04_development/02_releasing/#release-in-the-mr","title":"\ud83c\udf3f Release in the MR","text":"

    The version is known while the change is still a merge request, so the bump rides along with it. Do all edits in the MR:

    • Reconcile the changelog first. [Unreleased] is a draft, not the source of truth: diff the change against the last tag (git log <last-tag>..HEAD, git diff --stat <last-tag>..HEAD) and add an entry for every user-facing change that is missing. Always show the proposed additions to the user and wait for approval before promoting \u2014 ask rather than guess when an entry is ambiguous.

    • Promote the changelog in CHANGELOG.md: move the (now reconciled) [Unreleased] content under a new ## [x.y.z] - YYYY-MM-DD heading, leave a fresh empty [Unreleased] on top, and add the compare links \u2014 naming the version/tag, never a commit id:

    [Unreleased]: https://git.aetoskia.com/aetos/<repo>/compare/x.y.z...HEAD\n[x.y.z]: https://git.aetoskia.com/aetos/<repo>/compare/x.y.(z-1)...x.y.z\n
    • Bump the version file to the intended release version: version = \"x.y.z\" in pyproject.toml (packages) or the VERSION file (services).
    "},{"location":"04_development/02_releasing/#tag-the-merged-commit","title":"\ud83c\udff7\ufe0f Tag the merged commit","text":"
    1. Merge or squash the MR into main.
    2. Create the tag on the resulting commit: git tag x.y.z (lightweight).
    3. Push the tag (git push origin x.y.z) \u2014 Drone builds the release.
    "},{"location":"04_development/02_releasing/#agent-assisted-tagging","title":"\ud83e\udd16 Agent-assisted tagging","text":"

    The agent does not author or merge the MR and never tags before the merge. After you confirm the MR is merged/squashed and tell it to proceed, the agent:

    1. Waits for that explicit go-ahead.
    2. Finds the commit that introduced the bump:
    3. package: git log -S 'version = \"x.y.z\"' -n 1 --format='%H %s' -- pyproject.toml
    4. service: git log -S 'x.y.z' -n 1 --format='%H %s' -- VERSION
    5. Verifies that commit's version file equals x.y.z, then tags only that commit: git tag x.y.z <sha>.
    6. Does not push \u2014 it asks you to run git push origin x.y.z.
    "},{"location":"04_development/02_releasing/#what-drone-does","title":"\ud83e\udd16 What Drone does","text":"
    • Type A (package): asserts tag == pyproject version, then skips if that version already exists on the index, runs the quality gate, builds, and uploads the wheel + sdist.
    • Type B (service): asserts tag == VERSION, resolves the tag, skips if the image already exists, runs the quality gate, builds, pushes, and restarts the container.

    If the guard fails, nothing is published \u2014 fix the version file, amend the commit, and move the tag only if it was never pushed.

    "},{"location":"04_development/02_releasing/#backfill-recovery","title":"\ud83d\udedf Backfill / recovery","text":"

    If an older tag never published (a past version-file mismatch, an index outage):

    • Package: run the backfill-pypi-from-tags pipeline (custom event). It checks out every tag, verifies tag == pyproject version at that commit, and uploads the ones missing from the index. Already-fixed history is never rewritten.
    • Service: rebuild the image for the missing tag with a fresh commit and a new patch version; do not move a published tag.
    "},{"location":"04_development/02_releasing/#if-a-release-is-wrong","title":"\ud83d\udd01 If a release is wrong","text":"
    • Never reuse or force-move a pushed tag.
    • Fix forward: bump to the next patch, promote the changelog, tag again.
    "},{"location":"04_development/02_releasing/#related","title":"Related","text":"
    • Testing & Quality Gates \u00b7 Versioning & Changelog \u00b7 Drone Pipelines
    "},{"location":"06_reference_app/01_overview/","title":"Reference App \u00e2\u20ac\u201d Overview","text":"

    The standards ship a runnable sample of the whole blueprint: the vet veterinary-clinic application. It exists to be copied \u00e2\u20ac\u201d it is the reference for how a real domain sits on the architecture, and the code an agent pulls through the MCP tools (get_app_template, get_app_component).

    One sample, two templates

    The server sample lives at standards/src/templates/app/ (FastAPI + openapi-first + mongo-ops + hexa). The client sample lives at standards/src/templates/client/ (Vite + MUI + @aetoskia/react-openapi + @aetoskia/react-auth). Together they are the full-stack reference.

    "},{"location":"06_reference_app/01_overview/#the-domain-at-a-glance","title":"The domain at a glance","text":"Aggregate What it demonstrates parents The simplest CRUD slice (no foreign keys) \u00e2\u20ac\u201d copy this first pets Embedded list-of-refs (parents[]), file upload, enum fields treatments Plain resource with pricing fields vets Plain resource; target of prefetch FKs appointments Single FKs (pet, vet, treatment) expanded on read, filtered list jobs The worker's queue \u00e2\u20ac\u201d durable status machine

    The pipeline axis ships one worked family, appointment_reminder, plus the reusable pipelines/shared/ scaffold and a worker.py that dispatches jobs by job.kind.

    "},{"location":"06_reference_app/01_overview/#run-it","title":"Run it","text":"
    cp -r <docs>/standards/src/templates/app apps/vet-sample\ncd apps/vet-sample\npip install -e \".[dev]\"\n\nuvicorn main:app --reload     # API on :8020 (memory backend, zero services)\npython worker.py              # background worker (polls jobs)\npython -m scripts.seed        # optional: seed demo data (mongo backend)\n

    The default REPO_BACKEND=memory means the API boots with no external services \u00e2\u20ac\u201d repositories are process-local dictionaries. Switch to REPO_BACKEND=mongo for persistence (see .env.example).

    The client pairs with it:

    cp -r <docs>/standards/src/templates/client apps/vet-sample-client\ncd apps/vet-sample-client\necho \"VITE_API_BASE_URL=http://localhost:8020\" > .env.local\necho \"VITE_AUTH_BASE_URL=http://localhost:8020\" >> .env.local\nnpm install\nnpm run dev                   # Vite dev server\n

    Auth

    The server ships with the bearerAuth security scheme declared ({AUTH_SERVER} introspection). Without a reachable auth-server the API answers 401 on resource routes; /health stays public. For a local no-auth demo, remove the security block from openapi.yaml and the global security entry \u00e2\u20ac\u201d see Auth Integration.

    "},{"location":"06_reference_app/01_overview/#next","title":"Next","text":"
    • Domain & API \u00e2\u20ac\u201d walking the resource axis
    • Pipelines & Worker \u00e2\u20ac\u201d the pipeline axis
    • Client Admin \u00e2\u20ac\u201d the spec-driven frontend
    "},{"location":"06_reference_app/02_domain_and_api/","title":"Domain & API \u00e2\u20ac\u201d the resource axis","text":"

    Every aggregate follows the same four-file slice (CRUD Template); walk parents first, then appointments when you need foreign keys.

    "},{"location":"06_reference_app/02_domain_and_api/#the-slice-end-to-end-parents","title":"The slice, end to end (parents)","text":"
    domain/parents/\n    models.py               # framework-free Pydantic model (id optional)\n    ports/\n        db.py               # ParentDocument(Parent, BaseDocument) + create_parent_repo()\n        service.py          # ParentCrudsService(CrudsServiceAdaptor[...]) + get_parent_service()\ninterfaces/parents/\n    routes.py               # list/create/get/update/delete \u00e2\u20ac\u201d names == operationIds\n

    Wiring happens in exactly one place \u00e2\u20ac\u201d the composition root init.py:

    parent_repo = create_parent_repo()\n_resources[\"parent_svc\"] = get_parent_service(parent_repo)\n

    Routes resolve the service from app state through the shared helper (interfaces/_common.py):

    async def list_parents(request: Request, limit: int = 20, offset: int = 0) -> dict:\n    docs = await svc(request, \"parent\").search({})\n    return paginate([dump(d) for d in docs], limit, offset)\n
    "},{"location":"06_reference_app/02_domain_and_api/#the-spec-is-the-contract","title":"The spec is the contract","text":"

    openapi.yaml is OpenAPI 3.1 and the single source of truth: main.py builds an OpenAPIFirstApp that binds each operationId to a function of the same name in interfaces/openapi.py (a flat re-export of every routes module). There are no decorator routers.

    The x- extension fields on schemas and properties are what make the admin client render itself \u00e2\u20ac\u201d see Auto-Rendered Admin and the worked Job schema (enum selects, hidden fields, filterable columns).

    "},{"location":"06_reference_app/02_domain_and_api/#foreign-keys-appointments","title":"Foreign keys (appointments)","text":"

    Appointment declares pet, vet, treatment as dict | int | None. The write path normalizes bare ids through expand_ref; the read path returns fully expanded objects:

    pet = await expand_ref(svc(request, \"pet\"), body.get(\"pet\"), \"pet\")\n

    The list endpoint additionally filters by date prefix and embedded fk ids \u00e2\u20ac\u201d client-side over the repository result, the pattern for small reference data.

    "},{"location":"06_reference_app/02_domain_and_api/#ids-on-the-wire","title":"Ids on the wire","text":"

    The domain models declare id: int | None = None and documents use the integer-id repository variants (infra/db/repos/mongo_int.py, create_repo() in domain/*/ports/db.py, REPO_BACKEND=memory|mongo). This keeps the OpenAPI id: integer contract, the in-memory demo store, and the Mongo persistence layer consistent.

    "},{"location":"06_reference_app/02_domain_and_api/#tests","title":"Tests","text":"
    • tests/integration/test_openapi_app_builds.py \u00e2\u20ac\u201d generic gate: every operationId resolves and the app builds. Copy it verbatim.
    • tests/unit/test_jobs_routes.py \u00e2\u20ac\u201d the pattern for route unit tests with a service double (no HTTP client needed).
    "},{"location":"06_reference_app/03_pipelines_and_worker/","title":"Pipelines & Worker \u00e2\u20ac\u201d the pipeline axis","text":"

    The sample proves the full loop: API creates a job document \u2192 worker polls \u2192 pipeline family runs \u2192 job reaches a terminal status.

    "},{"location":"06_reference_app/03_pipelines_and_worker/#the-jobs-slice","title":"The jobs slice","text":"

    domain/jobs/ is an ordinary CRUD slice; its model is the durable state machine the worker drives:

    class Job(BaseModel):\n    name: str\n    kind: str                                        # dispatch key\n    status: Literal[\"pending\", \"processing\", \"done\", \"failed\"] = \"pending\"\n    payload: dict | None = None\n    retry_count: int = 0\n    error_message: str | None = None\n

    POST /jobs with {\"name\": \"...\", \"kind\": \"appointment_reminder\", \"payload\": {\"appointment_id\": 1}} enqueues work; GET /jobs?status=... inspects the queue.

    "},{"location":"06_reference_app/03_pipelines_and_worker/#the-appointment_reminder-family","title":"The appointment_reminder family","text":"
    pipelines/appointment_reminder/\n    abc.py            # LoadPort / ComposePort / DeliverPort + root interface\n    impl.py           # concrete stages, wired root, register(\"appointment_reminder\", ...)\n    pipeline.yaml     # graph spec (nodes, edges, roots)\n    resources/\n        pipeline.yaml # runtime spec consumed by the loader\n        patterns/reminder.yaml   # message template fragments\npipelines/shared/\n    ports.py loader.py registry.py   # reused by every family\n

    Each stage is a small, independently testable unit. The loader (pipelines/shared/loader.py) wires the root through hexa.build and merges resources/patterns/*.yaml into the stage-injected registry:

    pipeline = get_appointment_reminder_pipeline(appointment_svc)\nreminder = await pipeline.run(appointment_id)\n

    To add a family: copy appointment_reminder/, replace the stages, call register(kind, variant, root_cls) \u00e2\u20ac\u201d see Pipeline Scaffold.

    "},{"location":"06_reference_app/03_pipelines_and_worker/#the-worker","title":"The worker","text":"

    worker.py is the process that turns documents into execution:

    1. poll job_svc.search({\"status\": \"pending\"}) on WORKER_POLL_SECONDS
    2. claim a job: status \u2192 processing
    3. dispatch through the HANDLERS table \u00e2\u20ac\u201d HANDLERS[job.kind](job, resources)
    4. on success status \u2192 done; on exception status \u2192 failed, retry_count += 1, error_message captured; jobs at WORKER_MAX_RETRIES are skipped
    HANDLERS: dict[str, Callable] = {\n    \"appointment_reminder\": run_appointment_reminder,\n    # add yours here\n}\n

    The worker shares the composition root: from init import init gives it the same wired services the API uses \u00e2\u20ac\u201d one place to add a dependency.

    Same process model in production

    API and worker are separate processes (see docker-compose.yaml: api, worker, mongo). Any in-memory progress bus is per-process; to surface live progress through the API, swap the bus adaptor for a broker-backed one \u00e2\u20ac\u201d see Worker & Progress.

    "},{"location":"06_reference_app/03_pipelines_and_worker/#tests","title":"Tests","text":"
    • tests/unit/test_appointment_reminder_pipeline.py \u00e2\u20ac\u201d stage units + the wired root against a fake service.
    • tests/unit/test_worker.py \u00e2\u20ac\u201d dispatch, done/failed transitions, retry exhaustion, using an in-memory job service double and a cancelled poll task.
    "},{"location":"06_reference_app/04_client_admin/","title":"Client Admin \u00e2\u20ac\u201d the spec-driven frontend","text":"

    The client sample (standards/src/templates/client/) renders a full admin \u00e2\u20ac\u201d auth pages, resource lists, forms, detail views \u00e2\u20ac\u201d from the server's openapi.yaml, driven entirely by the x- extension fields. There are no hand-written screens per resource.

    "},{"location":"06_reference_app/04_client_admin/#what-ships","title":"What ships","text":"

    A Vite + React + MUI app wiring the two Aetos packages:

    • @aetoskia/react-openapi \u00e2\u20ac\u201d fetches ${VITE_API_BASE_URL}/openapi.json, builds the router and the resource admin from the spec's schemas and x- fields (list columns, autocomplete, fk pickers, enum selects, hidden fields\u00e2\u20ac\u00a6)
    • @aetoskia/react-auth \u00e2\u20ac\u201d login/register/logout against the auth-server paths declared in the spec's securitySchemes.bearerAuth, and attaches the JWT to every API call
    • a shared MUI theme (src/shared-theme/) and the shell (Header / Footer / Home)

    src/openapi-config.ts is the only configuration:

    export const specConfiguration: SpecConfiguration = {\n  specUrl: `${apiBase}/openapi.json`,\n  baseApiUrl: apiBase,\n  title: \"Vet Admin\",\n  getToken: () => tokenStore.get(),\n};\n
    "},{"location":"06_reference_app/04_client_admin/#pair-it-with-the-sample-server","title":"Pair it with the sample server","text":"
    cd apps/vet-sample-client\nprintf 'VITE_API_BASE_URL=http://localhost:8020\\nVITE_AUTH_BASE_URL=http://localhost:8020\\n' > .env.local\nnpm install\nnpm run dev\n

    Open the dev-server URL; the admin lists Parents, Pets, Treatments, Vets, Appointments and Jobs straight from the vet spec. Edit openapi.yaml on the server and the client follows on reload \u00e2\u20ac\u201d the spec is the contract on both sides.

    "},{"location":"06_reference_app/04_client_admin/#where-the-x-extensions-come-from","title":"Where the x- extensions come from","text":"

    The canonical catalog lives in the react-openapi package (spec-extensions.ts, served to agents as the specExtensions module of the react-openapi MCP bundle). The worked examples on the server side are the Job schema (enum selects via x-edit-as: select + x-ui-type: enum, x-hidden field masks, x-list-columns) and the FK fields on Appointment (x-fk: {resource: pets, prefetch: true}).

    "},{"location":"06_reference_app/04_client_admin/#ci","title":"CI","text":"

    The client ships a drone-npm shape (Type B in Drone Pipelines) and follows the Versioning standard (package.json + CHANGELOG.md).

    "}]} \ No newline at end of file +{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"\ud83e\udde9 Standards \u2014 the Aetos App Blueprint","text":"

    One reusable architecture for every backend application in the Aetos ecosystem: a spec-first HTTP edge, a framework-free domain core, a pluggable infrastructure layer, and a single composition root.

    Fill in two axes only and a new app is done: your resource model, and (if needed) your pipeline family. The blueprint itself never names a business domain \u2014 expenses, health metrics, inventory, and analytics are all the same shape underneath.

    Doc model: this wiki is written for humans \u2014 how-to guides, the build recipe, and quality gates. The enforceable rules (layering, composition root, CRUD template, pipeline scaffold) and the library registry live in the reference pages, mirrored by the machine-readable standards MCP bundle. Query find_library before writing any infrastructure.

    "},{"location":"#key-features","title":"\ud83d\ude80 Key Features","text":"
    • \ud83e\udded Spec-first routing \u2014 the OpenAPI document, not decorator routers, is the source of truth (operationId lookup)
    • \ud83c\udfd7\ufe0f One-way layering \u2014 interfaces \u2192 domain \u2192 infra, enforced
    • \ud83d\udd0c Single composition root \u2014 every dependency is wired in exactly one place
    • \ud83e\udde9 Generic, not app-specific \u2014 reusable rules versus two authoring axes
    • \ud83d\udcda Library-backed \u2014 DB, DI, pipelines, ingestion, auth, and docs come from internal libraries, never hand-written
    • \ud83d\udea6 Explicit state machines \u2014 durable job statuses, progress, and human-in-the-loop pauses
    "},{"location":"#documentation-structure","title":"\ud83d\udcc1 Documentation Structure","text":"Section Description Overview Philosophy, the one-way dependency rule, architecture App Components The component model and how the pieces wire together Start an App The entry point: gather the two axes, then build Reference App The runnable vet sample \u2014 server, pipelines, client Testing & Quality Test layout and acceptance gates

    Reference (separate site, mirrors the MCP bundle): the enforceable anatomy \u2014 start at the Build Recipe, then the pillars: domains (Layering), the OpenAPI contract (Composition Root, Spec-First Routing, CRUD Template, Auth Integration), interfaces & infra (Infra Adapters), UI (Auto-Rendered Admin), the optional pipelines layer (Pipeline Scaffold, Worker & Progress, Human-in-the-Loop), and delivery/meta (Genericity Matrix, Library Registry).

    "},{"location":"#related-resources","title":"\ud83d\udd17 Related Resources","text":"
    • MCP bundle \u2014 standards server on :8009: read docs://index, docs://nav, docs://modules/{module}; call find_library(capability)
    • Library registry \u2014 the catalog of internal libraries behind every app (libraries.json, served as registry://libraries)
    • Template \u2014 the runnable sample at standards/src/templates/app/ (server) and standards/src/templates/client/ (React admin); walkthrough in the Reference App
    • Start a build \u2014 invoke the start_app prompt on the standards server, or read the Start an App guide

    \u00a9 Aetoskia Internal \u2014 standards

    "},{"location":"01_overview/","title":"Overview","text":"

    The blueprint is a hexagonal (ports & adapters) application with a spec-first web edge and a single composition root. It favors explicit wiring over implicit magic and existing internal libraries over hand-written infrastructure. Start here to build the mental model; the enforceable details live in the reference pages.

    "},{"location":"01_overview/#architecture","title":"\ud83c\udfd7\ufe0f Architecture","text":"
    flowchart LR\n    subgraph EDGE[\"interfaces/ \u2014 HTTP edge\"]\n        OAS[\"openapi.yaml<br/>operationId contract\"]\n        RT[\"<resource>/routes.py<br/>thin handlers\"]\n    end\n\n    subgraph CORE[\"domain/ \u2014 business core\"]\n        MD[\"models\"]\n        PT[\"ports (adapter binding seam)\"]\n        SV[\"services\"]\n    end\n\n    subgraph INFRA[\"infra/ \u2014 adapters\"]\n        RP[\"mongo repos (mongo-ops)\"]\n        AD[\"cache \u00b7 stores \u00b7 llm \u00b7 mail\"]\n        DB[(\"MongoDB\")]\n    end\n\n    CR[\"init.py<br/>composition root\"]\n\n    OAS --> RT\n    RT --> SV\n    SV --> PT\n    SV --> RP\n    PT -. depends on .- RP\n    CR --- RT\n    CR --- SV\n    CR --- RP\n    RP --> DB\n    AD --- CR

    The rule holds at every edge: code in an outer ring may depend on a ring closer to the core, never the reverse. domain/ never imports a web framework; only its ports/ seam reaches into infra/ to bind adapters.

    Legend \u2014 solid arrows are compile-time calls; dotted arrows are the port/adapter binding, performed by the composition root at startup.

    "},{"location":"01_overview/#the-one-way-dependency-rule","title":"\ud83d\udd04 The one-way dependency rule","text":"Ring May import Never imports interfaces/ domain, framework glue (glue only) business logic, infra directly domain/ its own models + its ports/ seam (which binds infra/libraries) web frameworks, interfaces/ infra/ domain ports, drivers, libraries \u2014 init.py everything (the wiring point) \u2014"},{"location":"01_overview/#core-principles","title":"\ud83e\udded Core principles","text":"
    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 a port; the port's factory binds the concrete adapter, and the composition root calls those factories.
    3. One composition root. Nothing constructs its own dependencies. All wiring lives in one module (init.py) exposed as a flat resource registry.
    4. Spec-first. The HTTP contract is declared in openapi.yaml; route handlers are looked up by operationId. There are no decorator routers.
    5. Reuse the libraries. Orchestration, DI, Mongo repositories, ingestion, auth, and docs come from internal libraries \u2014 see the Library Registry.
    6. Explicit state machines. Long-running work moves through explicit, durable statuses and can pause for human input.
    "},{"location":"01_overview/#runtime-processes","title":"\u2699\ufe0f Runtime processes","text":"

    A standard app may run as up to three processes that share the same composition root:

    Process Entry point Responsibility API main.py serve HTTP; start/stop resources via lifespan Worker worker.py poll durable jobs, run pipelines, publish progress Scripts python -m scripts.<name> one-shot operator tasks"},{"location":"01_overview/#read-next","title":"\u27a1\ufe0f Read Next","text":"
    • 02 \u2013 App Components \u00b7 03 \u2013 Start an App
    "},{"location":"02_components/","title":"App Components","text":"

    What ships in every standard app, and how the pieces relate. This is the blueprint's component model \u2014 templates/app/ is a runnable instance of it.

    "},{"location":"02_components/#component-model","title":"\ud83d\uddc2\ufe0f Component model","text":"

    The blueprint has two orthogonal views: runtime flow (who calls whom) and composition-root wiring (who constructs whom). Keeping them separate makes the layering visible instead of hiding it behind a hub.

    "},{"location":"02_components/#runtime-flow","title":"\ud83d\udd00 Runtime flow","text":"

    Who calls whom at request and job time:

    %%{init: {\"flowchart\": {\"nodeSpacing\": 50, \"rankSpacing\": 80}}}%%\nflowchart TB\n    subgraph REQ[\"Request path \u2014 API process\"]\n        direction LR\n        CL([\"Client<br/>operationId\"]) --> RT[\"interfaces/&lt;resource&gt;/routes.py\"]\n        RT --> SVC[\"&lt;resource&gt;_svc\"]\n        SVC --> REPO[\"&lt;resource&gt;_repo\"]\n    end\n\n    subgraph WRK[\"Background path \u2014 worker process\"]\n        direction LR\n        W[\"worker.py\"] --> JSVC[\"job_svc\"]\n        W --> PIPE[\"pipelines/&lt;family&gt;\"]\n        PIPE --> ISVC[\"item_svc\"]\n        JSVC --> JREPO[\"job_repo\"]\n        ISVC --> IREPO[\"item_repo\"]\n    end\n\n    subgraph INFRA[\"infra/ \u2014 adapters\"]\n        MONGO[(\"MongoDB / mongo_ops\")]\n    end\n\n    REPO --> MONGO\n    JREPO --> MONGO\n    IREPO --> MONGO

    Legend \u2014 solid arrows are runtime calls. Nothing is self-constructed.

    "},{"location":"02_components/#composition-root-wiring","title":"\ud83e\uddf5 Composition-root wiring","text":"

    Who constructs what at startup:

    %%{init: {\"flowchart\": {\"nodeSpacing\": 50, \"rankSpacing\": 80}}}%%\nflowchart TB\n    INIT[\"init.py \u2014 composition root\"]\n    SETUP[\"infra/db/setup.py\"]\n    FACT[\"domain/*/ports/*<br/>factory seams\"]\n    REPOS[\"repos<br/>infra/db/repos/mongo.py\"]\n    SVCES[\"services<br/>CrudsServiceAdaptor\"]\n    STATE[\"app.state.db_resources\"]\n    HEXA[\"hexa.build\"]\n    RT[\"routes.py\"]\n    WK[\"worker.py\"]\n\n    INIT --> SETUP\n    INIT --> FACT\n    FACT --> REPOS\n    FACT --> SVCES\n    REPOS --> SVCES\n    HEXA -.-> SVCES\n    INIT --> STATE\n    STATE --> RT\n    INIT --> WK

    Legend \u2014 init.py is the only place that imports concrete adapters. The ports/ factory seams bind infra; the API reads resources from app.state.db_resources, the worker calls init() directly.

    "},{"location":"02_components/#component-roles","title":"\ud83d\udce6 Component roles","text":""},{"location":"02_components/#1-interfaces-the-edge","title":"1. interfaces/ \u2014 the edge","text":"

    Thin handlers bound to openapi.yaml operationIds. They parse the request, call a service, and shape the response. No business rules, no I/O.

    • interfaces/openapi.py re-exports every route module so the OpenAPI-first app can resolve handlers.
    • Routes may mount the cache decorator on read endpoints.
    "},{"location":"02_components/#2-domain-the-core","title":"2. domain/ \u2014 the core","text":"

    Models, ports, and services (use cases). No framework imports. The ports/ package is the one seam allowed to reach into infra/: each port module declares the document shape and a factory that binds the adapter.

    • <aggregate>/models.py \u2014 dataclasses, enums, document shapes.
    • <aggregate>/ports/db.py \u2014 document model + create_<aggregate>_repo().
    • <aggregate>/ports/service.py \u2014 service + get_<aggregate>_service(repo).
    • Services compose ports and enforce the business rules; the port factories bind the implementations, and the composition root calls them.
    "},{"location":"02_components/#3-infra-the-adapters","title":"3. infra/ \u2014 the adapters","text":"

    Concrete implementations of the domain ports plus cross-cutting infrastructure:

    • db/ \u2014 mongo-ops repositories and the connection lifecycle.
    • adapters/ \u2014 cache store, progress bus, storages, fetchers (library-backed).
    • Anything else the domain needs, implemented against its ports.
    "},{"location":"02_components/#4-pipelines-data-driven-families-optional","title":"4. pipelines/ \u2014 data-driven families (optional)","text":"

    Extraction and enrichment families, each a deterministic step chain (abc.py / impl.py / pipeline.yaml / resources/). Roots are registered in the pattern registry and driven either by a worker job or a CLI.

    "},{"location":"02_components/#5-workerpy-the-job-worker-optional","title":"5. worker.py \u2014 the job worker (optional)","text":"

    A separate process that polls durable job collections, runs pipeline roots, and publishes progress to the progress bus. It shares the same composition root as the API.

    "},{"location":"02_components/#6-initpy-the-composition-root","title":"6. init.py \u2014 the composition root","text":"

    The only place anything is constructed: leaf repositories \u2192 dependent repositories \u2192 services \u2192 adapters. Exposes get_svc overloads and starts / stops resources on the app lifespan.

    "},{"location":"02_components/#how-the-request-flows","title":"\ud83d\udd17 How the request flows","text":"
    sequenceDiagram\n    participant C as Client\n    participant R as route (interfaces)\n    participant S as service (port factory / adapter)\n    participant P as repo (infra)\n    participant DB as MongoDB\n\n    C->>R: POST /items (operationId)\n    R->>S: svc.create(payload)\n    S->>P: repo.insert(doc)\n    P->>DB: insert\n    DB-->>P: ack\n    P-->>S: doc\n    S-->>R: result\n    R-->>C: 200 JSON
    "},{"location":"02_components/#related","title":"Related","text":"
    • 01 \u2013 Overview \u00b7 03 \u2013 Start an App \u00b7 04 \u2013 Testing & Quality
    "},{"location":"03_start_an_app/","title":"\ud83d\ude80 Start an App","text":"

    This is the entry point for building a new application from the blueprint. The goal is one deterministic pass: you supply only the two axes, and the agent does the rest by reading the standards MCP, importing Tier A libraries, and copying Tier B template components.

    Doc model: this page is the human entry point. The deterministic, machine-facing checklist lives in the reference page Build Recipe, served as docs://modules/build_recipe.

    "},{"location":"03_start_an_app/#what-the-agent-needs-from-you","title":"\ud83e\uddfe What the agent needs from you","text":"

    Gather these before starting \u2014 the start_app MCP prompt asks for them:

    1. App name + one-line purpose.
    2. Axis 1 \u2014 resources. Each resource, its fields, and relationships. Paste openapi.yaml if you already have it.
    3. Axis 2 \u2014 pipelines. Only if an input needs a multi-step transformation: the families and their input formats (or \"none\").
    4. Auth needed now? Default no. Auth is deferred (py-jwt primitives only), and an authless app is just a spec with no security keys \u2014 see Spec-First Routing \u2192 Authless apps.
    5. Integrations. Mongo, ingestion formats, email, LLM provider.

    If you can state axes 1 and 2, the rest is mechanical.

    "},{"location":"03_start_an_app/#how-to-start","title":"\u25b6\ufe0f How to start","text":"

    From an MCP client connected to the standards server:

    1. Invoke the intake prompt (or start_app with no arguments) to get the intake questions.
    2. Answer them, then invoke build (build(app_name=..., resources=..., pipelines=..., auth=...)), or use start_app to intake-and-build in one call.
    3. The prompt hands off to docs://modules/build_recipe; the agent scaffolds with get_app_template() and pulls exact files with get_app_component(kind), authoring only the app-specific parts.
    4. To validate the standard without touching a real app, invoke trial (trial(app_name=..., build_path=...)) to build into a scratch directory, prove it builds, and report findings.

    Manual alternative \u2014 copy the skeleton and follow the reference recipe:

    cp -r <docs>/standards/src/templates/app apps/<your-app>\n
    "},{"location":"03_start_an_app/#reference","title":"\ud83d\udcda Reference","text":"
    • Build Recipe \u2014 the deterministic checklist
    • Genericity Matrix \u2014 Tier A / B / C
    • Library Registry \u2014 the Tier A catalog
    • App Components \u2014 the component model
    • Reference App \u2014 the runnable vet sample to copy
    "},{"location":"03_start_an_app/#related","title":"Related","text":"
    • 01 \u2013 Overview \u00b7 02 \u2013 App Components \u00b7 04 \u2013 Testing & Quality
    "},{"location":"05_mcp_setup/","title":"\ud83d\udd0c MCP Setup","text":"

    Every repository ships its documentation as a Model Context Protocol (MCP) server, and the standards registry server ties them together. The docs portal runs each one as a FastMCP streamable-http app bound to 0.0.0.0:<port>, exposing the endpoint at /mcp (see docs/main.py and docs/config.yml).

    Connect your agent to the standards server first: it answers find_library, list_repos, and the start_app prompt, so the agent reaches for an internal library instead of reimplementing one (see the Library Registry).

    "},{"location":"05_mcp_setup/#run-the-servers-locally","title":"\u25b6\ufe0f Run the servers locally","text":"

    From the docs checkout:

    .venv\\Scripts\\python.exe main.py                      # every server\n.venv\\Scripts\\python.exe main.py --servers standards   # just the registry\n

    :8000 is the health endpoint (GET /healthz), not an MCP server.

    "},{"location":"05_mcp_setup/#servers","title":"\ud83e\udded Servers","text":"Slug MCP server Port localhost mcp.aetoskia.com standards standards 8009 http://localhost:8009/mcp https://mcp.aetoskia.com/standards/mcp doc-forge docforge 8001 http://localhost:8001/mcp https://mcp.aetoskia.com/doc-forge/mcp mail-intake mail_intake 8002 http://localhost:8002/mcp https://mcp.aetoskia.com/mail-intake/mcp omniread omniread 8003 http://localhost:8003/mcp https://mcp.aetoskia.com/omniread/mcp openapi-first openapi_first 8004 http://localhost:8004/mcp https://mcp.aetoskia.com/openapi-first/mcp py-jwt jwtlib 8005 http://localhost:8005/mcp https://mcp.aetoskia.com/py-jwt/mcp dagpipe dagpipe 8006 http://localhost:8006/mcp https://mcp.aetoskia.com/dagpipe/mcp mongo-ops mongo_ops 8007 http://localhost:8007/mcp https://mcp.aetoskia.com/mongo-ops/mcp hexa hexa 8008 http://localhost:8008/mcp https://mcp.aetoskia.com/hexa/mcp doc-weaver doc-weaver 8010 http://localhost:8010/mcp https://mcp.aetoskia.com/doc-weaver/mcp react-auth react-auth 8011 http://localhost:8011/mcp https://mcp.aetoskia.com/react-auth/mcp react-openapi react-openapi 8012 http://localhost:8012/mcp https://mcp.aetoskia.com/react-openapi/mcp

    The hosted endpoints are served over HTTPS at https://mcp.aetoskia.com/<slug>/mcp. The tabs below are linked (content.tabs.link): switching one switches all of them. Every snippet registers all twelve servers, so a single paste is enough.

    "},{"location":"05_mcp_setup/#claude-code","title":"\ud83e\udd16 Claude Code","text":"

    Project-scoped .mcp.json at the repo root. The type field is required \u2014 an entry with a url but no type is read as a stdio server and skipped.

    localhostmcp.aetoskia.com
    {\n  \"mcpServers\": {\n    \"standards\": { \"type\": \"http\", \"url\": \"http://localhost:8009/mcp\" },\n    \"doc-forge\": { \"type\": \"http\", \"url\": \"http://localhost:8001/mcp\" },\n    \"mail-intake\": { \"type\": \"http\", \"url\": \"http://localhost:8002/mcp\" },\n    \"omniread\": { \"type\": \"http\", \"url\": \"http://localhost:8003/mcp\" },\n    \"openapi-first\": { \"type\": \"http\", \"url\": \"http://localhost:8004/mcp\" },\n    \"py-jwt\": { \"type\": \"http\", \"url\": \"http://localhost:8005/mcp\" },\n    \"dagpipe\": { \"type\": \"http\", \"url\": \"http://localhost:8006/mcp\" },\n    \"mongo-ops\": { \"type\": \"http\", \"url\": \"http://localhost:8007/mcp\" },\n    \"hexa\": { \"type\": \"http\", \"url\": \"http://localhost:8008/mcp\" },\n    \"doc-weaver\": { \"type\": \"http\", \"url\": \"http://localhost:8010/mcp\" },\n    \"react-auth\": { \"type\": \"http\", \"url\": \"http://localhost:8011/mcp\" },\n    \"react-openapi\": { \"type\": \"http\", \"url\": \"http://localhost:8012/mcp\" }\n  }\n}\n
    {\n  \"mcpServers\": {\n    \"standards\": { \"type\": \"http\", \"url\": \"https://mcp.aetoskia.com/standards/mcp\" },\n    \"doc-forge\": { \"type\": \"http\", \"url\": \"https://mcp.aetoskia.com/doc-forge/mcp\" },\n    \"mail-intake\": { \"type\": \"http\", \"url\": \"https://mcp.aetoskia.com/mail-intake/mcp\" },\n    \"omniread\": { \"type\": \"http\", \"url\": \"https://mcp.aetoskia.com/omniread/mcp\" },\n    \"openapi-first\": { \"type\": \"http\", \"url\": \"https://mcp.aetoskia.com/openapi-first/mcp\" },\n    \"py-jwt\": { \"type\": \"http\", \"url\": \"https://mcp.aetoskia.com/py-jwt/mcp\" },\n    \"dagpipe\": { \"type\": \"http\", \"url\": \"https://mcp.aetoskia.com/dagpipe/mcp\" },\n    \"mongo-ops\": { \"type\": \"http\", \"url\": \"https://mcp.aetoskia.com/mongo-ops/mcp\" },\n    \"hexa\": { \"type\": \"http\", \"url\": \"https://mcp.aetoskia.com/hexa/mcp\" },\n    \"doc-weaver\": { \"type\": \"http\", \"url\": \"https://mcp.aetoskia.com/doc-weaver/mcp\" },\n    \"react-auth\": { \"type\": \"http\", \"url\": \"https://mcp.aetoskia.com/react-auth/mcp\" },\n    \"react-openapi\": { \"type\": \"http\", \"url\": \"https://mcp.aetoskia.com/react-openapi/mcp\" }\n  }\n}\n

    Or add them with the CLI: claude mcp add --transport http standards http://localhost:8009/mcp.

    "},{"location":"05_mcp_setup/#codex","title":"\ud83e\udd16 Codex","text":"

    ~/.codex/config.toml (or a project-scoped .codex/config.toml).

    localhostmcp.aetoskia.com
    [mcp_servers.standards]\nurl = \"http://localhost:8009/mcp\"\n\n[mcp_servers.doc-forge]\nurl = \"http://localhost:8001/mcp\"\n\n[mcp_servers.mail-intake]\nurl = \"http://localhost:8002/mcp\"\n\n[mcp_servers.omniread]\nurl = \"http://localhost:8003/mcp\"\n\n[mcp_servers.openapi-first]\nurl = \"http://localhost:8004/mcp\"\n\n[mcp_servers.py-jwt]\nurl = \"http://localhost:8005/mcp\"\n\n[mcp_servers.dagpipe]\nurl = \"http://localhost:8006/mcp\"\n\n[mcp_servers.mongo-ops]\nurl = \"http://localhost:8007/mcp\"\n\n[mcp_servers.hexa]\nurl = \"http://localhost:8008/mcp\"\n\n[mcp_servers.doc-weaver]\nurl = \"http://localhost:8010/mcp\"\n\n[mcp_servers.react-auth]\nurl = \"http://localhost:8011/mcp\"\n\n[mcp_servers.react-openapi]\nurl = \"http://localhost:8012/mcp\"\n
    [mcp_servers.standards]\nurl = \"https://mcp.aetoskia.com/standards/mcp\"\n\n[mcp_servers.doc-forge]\nurl = \"https://mcp.aetoskia.com/doc-forge/mcp\"\n\n[mcp_servers.mail-intake]\nurl = \"https://mcp.aetoskia.com/mail-intake/mcp\"\n\n[mcp_servers.omniread]\nurl = \"https://mcp.aetoskia.com/omniread/mcp\"\n\n[mcp_servers.openapi-first]\nurl = \"https://mcp.aetoskia.com/openapi-first/mcp\"\n\n[mcp_servers.py-jwt]\nurl = \"https://mcp.aetoskia.com/py-jwt/mcp\"\n\n[mcp_servers.dagpipe]\nurl = \"https://mcp.aetoskia.com/dagpipe/mcp\"\n\n[mcp_servers.mongo-ops]\nurl = \"https://mcp.aetoskia.com/mongo-ops/mcp\"\n\n[mcp_servers.hexa]\nurl = \"https://mcp.aetoskia.com/hexa/mcp\"\n\n[mcp_servers.doc-weaver]\nurl = \"https://mcp.aetoskia.com/doc-weaver/mcp\"\n\n[mcp_servers.react-auth]\nurl = \"https://mcp.aetoskia.com/react-auth/mcp\"\n\n[mcp_servers.react-openapi]\nurl = \"https://mcp.aetoskia.com/react-openapi/mcp\"\n

    Or add them with the CLI: codex mcp add standards --url http://localhost:8009/mcp.

    "},{"location":"05_mcp_setup/#antigravity","title":"\ud83e\udd16 Antigravity","text":"

    ~/.gemini/antigravity/mcp_config.json (IDE) or ~/.gemini/config/mcp_config.json (CLI). Antigravity uses serverUrl, not url, for HTTP endpoints.

    localhostmcp.aetoskia.com
    {\n  \"mcpServers\": {\n    \"standards\": { \"serverUrl\": \"http://localhost:8009/mcp\" },\n    \"doc-forge\": { \"serverUrl\": \"http://localhost:8001/mcp\" },\n    \"mail-intake\": { \"serverUrl\": \"http://localhost:8002/mcp\" },\n    \"omniread\": { \"serverUrl\": \"http://localhost:8003/mcp\" },\n    \"openapi-first\": { \"serverUrl\": \"http://localhost:8004/mcp\" },\n    \"py-jwt\": { \"serverUrl\": \"http://localhost:8005/mcp\" },\n    \"dagpipe\": { \"serverUrl\": \"http://localhost:8006/mcp\" },\n    \"mongo-ops\": { \"serverUrl\": \"http://localhost:8007/mcp\" },\n    \"hexa\": { \"serverUrl\": \"http://localhost:8008/mcp\" },\n    \"doc-weaver\": { \"serverUrl\": \"http://localhost:8010/mcp\" },\n    \"react-auth\": { \"serverUrl\": \"http://localhost:8011/mcp\" },\n    \"react-openapi\": { \"serverUrl\": \"http://localhost:8012/mcp\" }\n  }\n}\n
    {\n  \"mcpServers\": {\n    \"standards\": { \"serverUrl\": \"https://mcp.aetoskia.com/standards/mcp\" },\n    \"doc-forge\": { \"serverUrl\": \"https://mcp.aetoskia.com/doc-forge/mcp\" },\n    \"mail-intake\": { \"serverUrl\": \"https://mcp.aetoskia.com/mail-intake/mcp\" },\n    \"omniread\": { \"serverUrl\": \"https://mcp.aetoskia.com/omniread/mcp\" },\n    \"openapi-first\": { \"serverUrl\": \"https://mcp.aetoskia.com/openapi-first/mcp\" },\n    \"py-jwt\": { \"serverUrl\": \"https://mcp.aetoskia.com/py-jwt/mcp\" },\n    \"dagpipe\": { \"serverUrl\": \"https://mcp.aetoskia.com/dagpipe/mcp\" },\n    \"mongo-ops\": { \"serverUrl\": \"https://mcp.aetoskia.com/mongo-ops/mcp\" },\n    \"hexa\": { \"serverUrl\": \"https://mcp.aetoskia.com/hexa/mcp\" },\n    \"doc-weaver\": { \"serverUrl\": \"https://mcp.aetoskia.com/doc-weaver/mcp\" },\n    \"react-auth\": { \"serverUrl\": \"https://mcp.aetoskia.com/react-auth/mcp\" },\n    \"react-openapi\": { \"serverUrl\": \"https://mcp.aetoskia.com/react-openapi/mcp\" }\n  }\n}\n
    "},{"location":"05_mcp_setup/#opencode","title":"\ud83e\udd16 OpenCode","text":"

    opencode.json (project) or ~/.config/opencode/opencode.json (global).

    localhostmcp.aetoskia.com
    {\n  \"$schema\": \"https://opencode.ai/config.json\",\n  \"mcp\": {\n    \"standards\": { \"type\": \"remote\", \"url\": \"http://localhost:8009/mcp\", \"enabled\": true },\n    \"doc-forge\": { \"type\": \"remote\", \"url\": \"http://localhost:8001/mcp\", \"enabled\": true },\n    \"mail-intake\": { \"type\": \"remote\", \"url\": \"http://localhost:8002/mcp\", \"enabled\": true },\n    \"omniread\": { \"type\": \"remote\", \"url\": \"http://localhost:8003/mcp\", \"enabled\": true },\n    \"openapi-first\": { \"type\": \"remote\", \"url\": \"http://localhost:8004/mcp\", \"enabled\": true },\n    \"py-jwt\": { \"type\": \"remote\", \"url\": \"http://localhost:8005/mcp\", \"enabled\": true },\n    \"dagpipe\": { \"type\": \"remote\", \"url\": \"http://localhost:8006/mcp\", \"enabled\": true },\n    \"mongo-ops\": { \"type\": \"remote\", \"url\": \"http://localhost:8007/mcp\", \"enabled\": true },\n    \"hexa\": { \"type\": \"remote\", \"url\": \"http://localhost:8008/mcp\", \"enabled\": true },\n    \"doc-weaver\": { \"type\": \"remote\", \"url\": \"http://localhost:8010/mcp\", \"enabled\": true },\n    \"react-auth\": { \"type\": \"remote\", \"url\": \"http://localhost:8011/mcp\", \"enabled\": true },\n    \"react-openapi\": { \"type\": \"remote\", \"url\": \"http://localhost:8012/mcp\", \"enabled\": true }\n  }\n}\n
    {\n  \"$schema\": \"https://opencode.ai/config.json\",\n  \"mcp\": {\n    \"standards\": { \"type\": \"remote\", \"url\": \"https://mcp.aetoskia.com/standards/mcp\", \"enabled\": true },\n    \"doc-forge\": { \"type\": \"remote\", \"url\": \"https://mcp.aetoskia.com/doc-forge/mcp\", \"enabled\": true },\n    \"mail-intake\": { \"type\": \"remote\", \"url\": \"https://mcp.aetoskia.com/mail-intake/mcp\", \"enabled\": true },\n    \"omniread\": { \"type\": \"remote\", \"url\": \"https://mcp.aetoskia.com/omniread/mcp\", \"enabled\": true },\n    \"openapi-first\": { \"type\": \"remote\", \"url\": \"https://mcp.aetoskia.com/openapi-first/mcp\", \"enabled\": true },\n    \"py-jwt\": { \"type\": \"remote\", \"url\": \"https://mcp.aetoskia.com/py-jwt/mcp\", \"enabled\": true },\n    \"dagpipe\": { \"type\": \"remote\", \"url\": \"https://mcp.aetoskia.com/dagpipe/mcp\", \"enabled\": true },\n    \"mongo-ops\": { \"type\": \"remote\", \"url\": \"https://mcp.aetoskia.com/mongo-ops/mcp\", \"enabled\": true },\n    \"hexa\": { \"type\": \"remote\", \"url\": \"https://mcp.aetoskia.com/hexa/mcp\", \"enabled\": true },\n    \"doc-weaver\": { \"type\": \"remote\", \"url\": \"https://mcp.aetoskia.com/doc-weaver/mcp\", \"enabled\": true },\n    \"react-auth\": { \"type\": \"remote\", \"url\": \"https://mcp.aetoskia.com/react-auth/mcp\", \"enabled\": true },\n    \"react-openapi\": { \"type\": \"remote\", \"url\": \"https://mcp.aetoskia.com/react-openapi/mcp\", \"enabled\": true }\n  }\n}\n

    Or add them with the CLI: opencode mcp add standards --url http://localhost:8009/mcp --global.

    "},{"location":"05_mcp_setup/#pycharm-junie-ai-assistant","title":"\ud83e\udd16 PyCharm (Junie / AI Assistant)","text":"

    ~/.junie/mcp/mcp.json (user scope) or .junie/mcp/mcp.json (project scope). The same config is reachable from Settings | Tools | MCP Server \u2192 Manual Client Configuration \u2192 Copy HTTP Stream Config.

    localhostmcp.aetoskia.com
    {\n  \"mcpServers\": {\n    \"standards\": { \"url\": \"http://localhost:8009/mcp\" },\n    \"doc-forge\": { \"url\": \"http://localhost:8001/mcp\" },\n    \"mail-intake\": { \"url\": \"http://localhost:8002/mcp\" },\n    \"omniread\": { \"url\": \"http://localhost:8003/mcp\" },\n    \"openapi-first\": { \"url\": \"http://localhost:8004/mcp\" },\n    \"py-jwt\": { \"url\": \"http://localhost:8005/mcp\" },\n    \"dagpipe\": { \"url\": \"http://localhost:8006/mcp\" },\n    \"mongo-ops\": { \"url\": \"http://localhost:8007/mcp\" },\n    \"hexa\": { \"url\": \"http://localhost:8008/mcp\" },\n    \"doc-weaver\": { \"url\": \"http://localhost:8010/mcp\" },\n    \"react-auth\": { \"url\": \"http://localhost:8011/mcp\" },\n    \"react-openapi\": { \"url\": \"http://localhost:8012/mcp\" }\n  }\n}\n
    {\n  \"mcpServers\": {\n    \"standards\": { \"url\": \"https://mcp.aetoskia.com/standards/mcp\" },\n    \"doc-forge\": { \"url\": \"https://mcp.aetoskia.com/doc-forge/mcp\" },\n    \"mail-intake\": { \"url\": \"https://mcp.aetoskia.com/mail-intake/mcp\" },\n    \"omniread\": { \"url\": \"https://mcp.aetoskia.com/omniread/mcp\" },\n    \"openapi-first\": { \"url\": \"https://mcp.aetoskia.com/openapi-first/mcp\" },\n    \"py-jwt\": { \"url\": \"https://mcp.aetoskia.com/py-jwt/mcp\" },\n    \"dagpipe\": { \"url\": \"https://mcp.aetoskia.com/dagpipe/mcp\" },\n    \"mongo-ops\": { \"url\": \"https://mcp.aetoskia.com/mongo-ops/mcp\" },\n    \"hexa\": { \"url\": \"https://mcp.aetoskia.com/hexa/mcp\" },\n    \"doc-weaver\": { \"url\": \"https://mcp.aetoskia.com/doc-weaver/mcp\" },\n    \"react-auth\": { \"url\": \"https://mcp.aetoskia.com/react-auth/mcp\" },\n    \"react-openapi\": { \"url\": \"https://mcp.aetoskia.com/react-openapi/mcp\" }\n  }\n}\n
    "},{"location":"05_mcp_setup/#verify","title":"\u2705 Verify","text":"
    • Health check: curl http://localhost:8000/healthz \u2192 ok.
    • Claude Code: claude mcp list \u2192 each server \u2026 \u2713 Connected.
    • Codex: codex mcp list \u2192 each server \u2026 http.
    • OpenCode: opencode mcp list \u2192 each server \u2026 connected.
    • In-session: run /mcp to list the connected servers and their tools.
    "},{"location":"05_mcp_setup/#reference","title":"\ud83d\udcda Reference","text":"
    • Library Registry \u2014 the catalog each server mirrors, and where find_library resolves
    • Start an App \u2014 connect standards, then invoke start_app
    • App Components \u2014 the component model
    "},{"location":"05_mcp_setup/#related","title":"Related","text":"
    • Start an App \u00b7 Testing & Quality \u00b7 Library Registry
    "},{"location":"04_development/01_testing/","title":"Testing & Quality Gates","text":"

    Two suites run under pytest with asyncio auto mode: integration (the HTTP edge against fakes) and unit (pure domain and pipeline logic). Finish a blueprint app only when every gate below passes.

    "},{"location":"04_development/01_testing/#layout","title":"\ud83d\uddc2\ufe0f Layout","text":"
    tests/\n    integration/     # TestClient(app) + mocked resources\n    unit/            # models, converters, pipeline stages/roots\n    conftest.py      # fixtures\n
    "},{"location":"04_development/01_testing/#integration-tests","title":"\ud83d\ude80 Integration tests","text":"
    • Build the app with TestClient.
    • Replace app.state.db_resources with fake repositories/services so no database is required.
    • Patch the auth introspection call (httpx.AsyncClient.post) to return a canned identity.
    • Assert status codes and bodies for list/get/create/update/delete, plus 400, 404, 204, and the streaming endpoint's fallback.
    client = TestClient(app)\napp.state.db_resources = {\"item_svc\": FakeItemService(), ...}\n\ndef test_create_item():\n    r = client.post(\"/items\", json={\"name\": \"x\"})\n    assert r.status_code == 200\n
    "},{"location":"04_development/01_testing/#unit-tests","title":"\ud83e\uddea Unit tests","text":"
    • Domain models and converters: pure, no I/O.
    • Pipeline stages: feed a fixture state, assert the transformed state.
    • Assembled pipeline roots: fixture parity \u2014 replay a stored fixture input and compare against a golden output file.
    "},{"location":"04_development/01_testing/#config-consistency-tests","title":"\ud83d\udd10 Config-consistency tests","text":"

    For rule-driven pipelines, add a test that cross-references rules against the declared pattern registry: every flag emitted must be defined; every defined flag must be reachable. This catches drift without running the pipeline.

    "},{"location":"04_development/01_testing/#quality-gates","title":"\u2705 Quality gates","text":"

    A definition of done for any standard app:

    • python -c \"import main\" succeeds.
    • pytest passes.
    • The app serves the spec's operations; the worker drains its queue.
    • No domain module imports a web framework.
    • No infrastructure was reimplemented that a library already provides.
    "},{"location":"04_development/01_testing/#conventions","title":"\ud83d\udca1 Conventions","text":"
    • asyncio_mode = \"auto\"; no manual event loops.
    • Fixtures are checked into the repo (inputs + golden outputs).
    • Tests skip (not fail) when a required fixture is absent.
    • Do not mock the domain; mock only the infrastructure edge.
    "},{"location":"04_development/01_testing/#related","title":"Related","text":"
    • 02 \u2013 App Components \u00b7 03 \u2013 Start an App \u00b7 Overview
    "},{"location":"04_development/02_releasing/","title":"Releasing & Versioning","text":"

    How to cut a release of any Aetos repository. The rule lives in the reference page Versioning & Changelog; this is the runbook you follow.

    Every repository is one of two Drone shapes \u2014 a PyPI package or a Docker deployment (Drone Pipelines). Both release the same way: the version bump and changelog promotion go in the merge request, and the tag is created on the merged commit.

    "},{"location":"04_development/02_releasing/#before-you-start","title":"\ud83e\uddfe Before you start","text":"
    • You have an open merge request holding the change, and main is green (the Drone quality gate passed on the last run).
    • You know whether this is a breaking, feature, or fix release.
    • The version file exists:
    • package \u2192 version in pyproject.toml
    • service \u2192 VERSION at the repo root
    "},{"location":"04_development/02_releasing/#pick-the-next-version","title":"\ud83d\udea6 Pick the next version","text":"

    The version file and the tag must be identical. For 0.y.z, 0.y is the major line (see the reference page); once at 1.0.0, normal SemVer applies.

    Change 0.y.z \u2192 1.0.0+ \u2192 Breaking 0.(y+1).0 (x+1).0.0 Feature 0.y.(z+1) x.(y+1).0 Fix 0.y.(z+1) x.y.(z+1)"},{"location":"04_development/02_releasing/#release-in-the-mr","title":"\ud83c\udf3f Release in the MR","text":"

    The version is known while the change is still a merge request, so the bump rides along with it. Do all edits in the MR:

    • Reconcile the changelog first. [Unreleased] is a draft, not the source of truth: diff the change against the last tag (git log <last-tag>..HEAD, git diff --stat <last-tag>..HEAD) and add an entry for every user-facing change that is missing. Always show the proposed additions to the user and wait for approval before promoting \u2014 ask rather than guess when an entry is ambiguous.

    • Promote the changelog in CHANGELOG.md: move the (now reconciled) [Unreleased] content under a new ## [x.y.z] - YYYY-MM-DD heading, leave a fresh empty [Unreleased] on top, and add the compare links \u2014 naming the version/tag, never a commit id:

    [Unreleased]: https://git.aetoskia.com/aetos/<repo>/compare/x.y.z...HEAD\n[x.y.z]: https://git.aetoskia.com/aetos/<repo>/compare/x.y.(z-1)...x.y.z\n
    • Bump the version file to the intended release version: version = \"x.y.z\" in pyproject.toml (packages) or the VERSION file (services).
    "},{"location":"04_development/02_releasing/#tag-the-merged-commit","title":"\ud83c\udff7\ufe0f Tag the merged commit","text":"
    1. Merge or squash the MR into main.
    2. Create the tag on the resulting commit: git tag x.y.z (lightweight).
    3. Push the tag (git push origin x.y.z) \u2014 Drone builds the release.
    "},{"location":"04_development/02_releasing/#agent-assisted-tagging","title":"\ud83e\udd16 Agent-assisted tagging","text":"

    The agent does not author or merge the MR and never tags before the merge. After you confirm the MR is merged/squashed and tell it to proceed, the agent:

    1. Waits for that explicit go-ahead.
    2. Finds the commit that introduced the bump:
    3. package: git log -S 'version = \"x.y.z\"' -n 1 --format='%H %s' -- pyproject.toml
    4. service: git log -S 'x.y.z' -n 1 --format='%H %s' -- VERSION
    5. Verifies that commit's version file equals x.y.z, then tags only that commit: git tag x.y.z <sha>.
    6. Does not push \u2014 it asks you to run git push origin x.y.z.
    "},{"location":"04_development/02_releasing/#what-drone-does","title":"\ud83e\udd16 What Drone does","text":"
    • Type A (package): asserts tag == pyproject version, then skips if that version already exists on the index, runs the quality gate, builds, and uploads the wheel + sdist.
    • Type B (service): asserts tag == VERSION, resolves the tag, skips if the image already exists, runs the quality gate, builds, pushes, and restarts the container.

    If the guard fails, nothing is published \u2014 fix the version file, amend the commit, and move the tag only if it was never pushed.

    "},{"location":"04_development/02_releasing/#backfill-recovery","title":"\ud83d\udedf Backfill / recovery","text":"

    If an older tag never published (a past version-file mismatch, an index outage):

    • Package: run the backfill-pypi-from-tags pipeline (custom event). It checks out every tag, verifies tag == pyproject version at that commit, and uploads the ones missing from the index. Already-fixed history is never rewritten.
    • Service: rebuild the image for the missing tag with a fresh commit and a new patch version; do not move a published tag.
    "},{"location":"04_development/02_releasing/#if-a-release-is-wrong","title":"\ud83d\udd01 If a release is wrong","text":"
    • Never reuse or force-move a pushed tag.
    • Fix forward: bump to the next patch, promote the changelog, tag again.
    "},{"location":"04_development/02_releasing/#related","title":"Related","text":"
    • Testing & Quality Gates \u00b7 Versioning & Changelog \u00b7 Drone Pipelines
    "},{"location":"06_reference_app/01_overview/","title":"Reference App \u2014 Overview","text":"

    The standards ship a runnable sample of the whole blueprint: the vet veterinary-clinic application. It exists to be copied \u2014 it is the reference for how a real domain sits on the architecture, and the code an agent pulls through the MCP tools (get_app_template, get_app_component).

    One sample, two templates

    The server sample lives at standards/src/templates/app/ (FastAPI + openapi-first + mongo-ops + hexa). The client sample lives at standards/src/templates/client/ (Vite + MUI + @aetoskia/react-openapi + @aetoskia/react-auth). Together they are the full-stack reference.

    "},{"location":"06_reference_app/01_overview/#the-domain-at-a-glance","title":"The domain at a glance","text":"Aggregate What it demonstrates parents The simplest CRUD slice (no foreign keys) \u2014 copy this first pets Embedded list-of-refs (parents[]), file upload, enum fields treatments Plain resource with pricing fields vets Plain resource; target of prefetch FKs appointments Single FKs (pet, vet, treatment) expanded on read, filtered list jobs The worker's queue \u2014 durable status machine

    The pipeline axis ships one worked family, appointment_reminder, plus the reusable pipelines/shared/ scaffold and a worker.py that dispatches jobs by job.kind.

    "},{"location":"06_reference_app/01_overview/#run-it","title":"Run it","text":"
    cp -r <docs>/standards/src/templates/app apps/vet-sample\ncd apps/vet-sample\npip install -e \".[dev]\"\n\nuvicorn main:app --reload     # API on :8020 (memory backend, zero services)\npython worker.py              # background worker (polls jobs)\npython -m scripts.seed        # optional: seed demo data (mongo backend)\n

    The default REPO_BACKEND=memory means the API boots with no external services \u2014 repositories are process-local dictionaries. Switch to REPO_BACKEND=mongo for persistence (see .env.example).

    The client pairs with it:

    cp -r <docs>/standards/src/templates/client apps/vet-sample-client\ncd apps/vet-sample-client\necho \"VITE_API_BASE_URL=http://localhost:8020\" > .env.local\necho \"VITE_AUTH_BASE_URL=http://localhost:8020\" >> .env.local\nnpm install\nnpm run dev                   # Vite dev server\n

    Auth

    The server ships with the bearerAuth security scheme declared ({AUTH_SERVER} introspection). Without a reachable auth-server the API answers 401 on resource routes; /health stays public. For a local no-auth demo, remove the security block from openapi.yaml and the global security entry \u2014 see Auth Integration.

    "},{"location":"06_reference_app/01_overview/#next","title":"Next","text":"
    • Domain & API \u2014 walking the resource axis
    • Pipelines & Worker \u2014 the pipeline axis
    • Client Admin \u2014 the spec-driven frontend
    "},{"location":"06_reference_app/02_domain_and_api/","title":"Domain & API \u2014 the resource axis","text":"

    Every aggregate follows the same four-file slice (CRUD Template); walk parents first, then appointments when you need foreign keys.

    "},{"location":"06_reference_app/02_domain_and_api/#the-slice-end-to-end-parents","title":"The slice, end to end (parents)","text":"
    domain/parents/\n    models.py               # framework-free Pydantic model (id optional)\n    ports/\n        db.py               # ParentDocument(Parent, BaseDocument) + create_parent_repo()\n        service.py          # ParentCrudsService(CrudsServiceAdaptor[...]) + get_parent_service()\ninterfaces/parents/\n    routes.py               # list/create/get/update/delete \u2014 names == operationIds\n

    Wiring happens in exactly one place \u2014 the composition root init.py:

    parent_repo = create_parent_repo()\n_resources[\"parent_svc\"] = get_parent_service(parent_repo)\n

    Routes resolve the service from app state through the shared helper (interfaces/_common.py):

    async def list_parents(request: Request, limit: int = 20, offset: int = 0) -> dict:\n    docs = await svc(request, \"parent\").search({})\n    return paginate([dump(d) for d in docs], limit, offset)\n
    "},{"location":"06_reference_app/02_domain_and_api/#the-spec-is-the-contract","title":"The spec is the contract","text":"

    openapi.yaml is OpenAPI 3.1 and the single source of truth: main.py builds an OpenAPIFirstApp that binds each operationId to a function of the same name in interfaces/openapi.py (a flat re-export of every routes module). There are no decorator routers.

    The x- extension fields on schemas and properties are what make the admin client render itself \u2014 see Auto-Rendered Admin and the worked Job schema (enum selects, hidden fields, filterable columns).

    "},{"location":"06_reference_app/02_domain_and_api/#foreign-keys-appointments","title":"Foreign keys (appointments)","text":"

    Appointment declares pet, vet, treatment as dict | int | None. The write path normalizes bare ids through expand_ref; the read path returns fully expanded objects:

    pet = await expand_ref(svc(request, \"pet\"), body.get(\"pet\"), \"pet\")\n

    The list endpoint additionally filters by date prefix and embedded fk ids \u2014 client-side over the repository result, the pattern for small reference data.

    "},{"location":"06_reference_app/02_domain_and_api/#ids-on-the-wire","title":"Ids on the wire","text":"

    The domain models declare id: int | None = None and documents use the integer-id repository variants (infra/db/repos/mongo_int.py, create_repo() in domain/*/ports/db.py, REPO_BACKEND=memory|mongo). This keeps the OpenAPI id: integer contract, the in-memory demo store, and the Mongo persistence layer consistent.

    "},{"location":"06_reference_app/02_domain_and_api/#tests","title":"Tests","text":"
    • tests/integration/test_openapi_app_builds.py \u2014 generic gate: every operationId resolves and the app builds. Copy it verbatim.
    • tests/unit/test_jobs_routes.py \u2014 the pattern for route unit tests with a service double (no HTTP client needed).
    "},{"location":"06_reference_app/03_pipelines_and_worker/","title":"Pipelines & Worker \u2014 the pipeline axis","text":"

    The sample proves the full loop: API creates a job document \u2192 worker polls \u2192 pipeline family runs \u2192 job reaches a terminal status.

    "},{"location":"06_reference_app/03_pipelines_and_worker/#the-jobs-slice","title":"The jobs slice","text":"

    domain/jobs/ is an ordinary CRUD slice; its model is the durable state machine the worker drives:

    class Job(BaseModel):\n    name: str\n    kind: str                                        # dispatch key\n    status: Literal[\"pending\", \"processing\", \"done\", \"failed\"] = \"pending\"\n    payload: dict | None = None\n    retry_count: int = 0\n    error_message: str | None = None\n

    POST /jobs with {\"name\": \"...\", \"kind\": \"appointment_reminder\", \"payload\": {\"appointment_id\": 1}} enqueues work; GET /jobs?status=... inspects the queue.

    "},{"location":"06_reference_app/03_pipelines_and_worker/#the-appointment_reminder-family","title":"The appointment_reminder family","text":"
    pipelines/appointment_reminder/\n    abc.py            # LoadPort / ComposePort / DeliverPort + root interface\n    impl.py           # concrete stages, wired root, register(\"appointment_reminder\", ...)\n    pipeline.yaml     # graph spec (nodes, edges, roots)\n    resources/\n        pipeline.yaml # runtime spec consumed by the loader\n        patterns/reminder.yaml   # message template fragments\npipelines/shared/\n    ports.py loader.py registry.py   # reused by every family\n

    Each stage is a small, independently testable unit. The loader (pipelines/shared/loader.py) wires the root through hexa.build and merges resources/patterns/*.yaml into the stage-injected registry:

    pipeline = get_appointment_reminder_pipeline(appointment_svc)\nreminder = await pipeline.run(appointment_id)\n

    To add a family: copy appointment_reminder/, replace the stages, call register(kind, variant, root_cls) \u2014 see Pipeline Scaffold.

    "},{"location":"06_reference_app/03_pipelines_and_worker/#the-worker","title":"The worker","text":"

    worker.py is the process that turns documents into execution:

    1. poll job_svc.search({\"status\": \"pending\"}) on WORKER_POLL_SECONDS
    2. claim a job: status \u2192 processing
    3. dispatch through the HANDLERS table \u2014 HANDLERS[job.kind](job, resources)
    4. on success status \u2192 done; on exception status \u2192 failed, retry_count += 1, error_message captured; jobs at WORKER_MAX_RETRIES are skipped
    HANDLERS: dict[str, Callable] = {\n    \"appointment_reminder\": run_appointment_reminder,\n    # add yours here\n}\n

    The worker shares the composition root: from init import init gives it the same wired services the API uses \u2014 one place to add a dependency.

    Same process model in production

    API and worker are separate processes (see docker-compose.yaml: api, worker, mongo). Any in-memory progress bus is per-process; to surface live progress through the API, swap the bus adaptor for a broker-backed one \u2014 see Worker & Progress.

    "},{"location":"06_reference_app/03_pipelines_and_worker/#tests","title":"Tests","text":"
    • tests/unit/test_appointment_reminder_pipeline.py \u2014 stage units + the wired root against a fake service.
    • tests/unit/test_worker.py \u2014 dispatch, done/failed transitions, retry exhaustion, using an in-memory job service double and a cancelled poll task.
    "},{"location":"06_reference_app/04_client_admin/","title":"Client Admin \u2014 the spec-driven frontend","text":"

    The client sample (standards/src/templates/client/) renders a full admin \u2014 auth pages, resource lists, forms, detail views \u2014 from the server's openapi.yaml, driven entirely by the x- extension fields. There are no hand-written screens per resource.

    "},{"location":"06_reference_app/04_client_admin/#what-ships","title":"What ships","text":"

    A Vite + React + MUI app wiring the two Aetos packages:

    • @aetoskia/react-openapi \u2014 fetches ${VITE_API_BASE_URL}/openapi.json, builds the router and the resource admin from the spec's schemas and x- fields (list columns, autocomplete, fk pickers, enum selects, hidden fields\u2026)
    • @aetoskia/react-auth \u2014 login/register/logout against the auth-server paths declared in the spec's securitySchemes.bearerAuth, and attaches the JWT to every API call
    • a shared MUI theme (src/shared-theme/) and the shell (Header / Footer / Home)

    src/openapi-config.ts is the only configuration:

    export const specConfiguration: SpecConfiguration = {\n  specUrl: `${apiBase}/openapi.json`,\n  baseApiUrl: apiBase,\n  title: \"Vet Admin\",\n  getToken: () => tokenStore.get(),\n};\n
    "},{"location":"06_reference_app/04_client_admin/#pair-it-with-the-sample-server","title":"Pair it with the sample server","text":"
    cd apps/vet-sample-client\nprintf 'VITE_API_BASE_URL=http://localhost:8020\\nVITE_AUTH_BASE_URL=http://localhost:8020\\n' > .env.local\nnpm install\nnpm run dev\n

    Open the dev-server URL; the admin lists Parents, Pets, Treatments, Vets, Appointments and Jobs straight from the vet spec. Edit openapi.yaml on the server and the client follows on reload \u2014 the spec is the contract on both sides.

    "},{"location":"06_reference_app/04_client_admin/#where-the-x-extensions-come-from","title":"Where the x- extensions come from","text":"

    The canonical catalog lives in the react-openapi package (spec-extensions.ts, served to agents as the specExtensions module of the react-openapi MCP bundle). The worked examples on the server side are the Job schema (enum selects via x-edit-as: select + x-ui-type: enum, x-hidden field masks, x-list-columns) and the FK fields on Appointment (x-fk: {resource: pets, prefetch: true}).

    "},{"location":"06_reference_app/04_client_admin/#ci","title":"CI","text":"

    The client ships a drone-npm shape (Type B in Drone Pipelines) and follows the Versioning standard (package.json + CHANGELOG.md).

    "}]} \ No newline at end of file