Phase 8: add the Reference App wiki walkthrough (06_reference_app)

- four pages walking the vet sample: overview, domain & API, pipelines &
  worker, spec-driven client admin
- mkdocs nav section, index + start-an-app cross links
- bundle now exposes the walkthrough as wiki_reference_app_* modules
This commit is contained in:
2026-10-20 15:02:11 +05:30
parent 0cb35d10a6
commit f8cf35de0e
28 changed files with 5760 additions and 19 deletions

View File

@@ -1,6 +1,6 @@
{
"project": "standards",
"type": "standards",
"modules_count": 24,
"modules_count": 28,
"source": "hand-authored"
}

View File

@@ -2,7 +2,7 @@
"module": "wiki_index",
"content": {
"path": "wiki_index",
"docstring": "# 🧩 Standards — the Aetos App Blueprint\n\nOne reusable architecture for every backend application in the Aetos ecosystem:\na spec-first HTTP edge, a framework-free domain core, a pluggable infrastructure\nlayer, and a single composition root.\n\nFill in **two axes only** and a new app is done: your **resource model**, and\n(if needed) your **pipeline family**. The blueprint itself never names a\nbusiness domain — expenses, health metrics, inventory, and analytics are all\nthe same shape underneath.\n\n> **Doc model:** this wiki is written for humans — how-to guides, the build\n> recipe, and quality gates. The enforceable rules (layering, composition root,\n> CRUD template, pipeline scaffold) and the library registry live in the\n> **reference** pages, mirrored by the machine-readable `standards` MCP bundle.\n> Query `find_library` before writing any infrastructure.\n\n---\n\n## 🚀 Key Features\n\n- 🧭 **Spec-first routing** — the OpenAPI document, not decorator routers, is\n the source of truth (operationId lookup)\n- 🏗️ **One-way layering** — `interfaces → domain → infra`, enforced\n- 🔌 **Single composition root** — every dependency is wired in exactly one place\n- 🧩 **Generic, not app-specific** — reusable rules versus two authoring axes\n- 📚 **Library-backed** — DB, DI, pipelines, ingestion, auth, and docs come from\n internal libraries, never hand-written\n- 🚦 **Explicit state machines** — durable job statuses, progress, and\n human-in-the-loop pauses\n\n---\n\n## 📁 Documentation Structure\n\n| Section | Description |\n|-----------------------------------------------------|--------------------------------------------------------|\n| [Overview](01_overview.md) | Philosophy, the one-way dependency rule, architecture |\n| [App Components](02_components.md) | The component model and how the pieces wire together |\n| [Start an App](03_start_an_app.md) | The entry point: gather the two axes, then build |\n| [Testing & Quality](04_development/01_testing.md) | Test layout and acceptance gates |\n\n**Reference** (separate site, mirrors the MCP bundle): the enforceable\nanatomy — start at the [Build Recipe](../lib/01_build_recipe/), then the\npillars: domains ([Layering](../lib/10_layering_rules/)), the OpenAPI\ncontract ([Composition Root](../lib/02_composition_root/),\n[Spec-First Routing](../lib/20_spec_first_routing/), [CRUD\nTemplate](../lib/21_crud_template/), [Auth\nIntegration](../lib/22_auth_integration/)), interfaces & infra ([Infra\nAdapters](../lib/30_infra_adaptors/)), UI ([Auto-Rendered\nAdmin](../lib/40_ui_admin/)), the optional pipelines layer ([Pipeline\nScaffold](../lib/50_pipeline_scaffold/), [Worker &\nProgress](../lib/51_worker_and_progress/), [Human-in-the-Loop](../lib/52_human_in_loop/)),\nand delivery/meta ([Genericity\nMatrix](../lib/92_genericity_matrix/), [Library\nRegistry](../lib/93_library_registry/)).\n\n---\n\n## 🔗 Related Resources\n\n- **MCP bundle** — `standards` server on `:8009`: read `docs://index`,\n `docs://nav`, `docs://modules/{module}`; call `find_library(capability)`\n- **Library registry** — the catalog of internal libraries behind every app\n (`libraries.json`, served as `registry://libraries`)\n- **Template** — the runnable skeleton at `standards/src/templates/app/`\n- **Start a build** — invoke the `start_app` prompt on the `standards` server,\n or read the [Start an App](03_start_an_app.md) guide\n\n---\n\n© Aetoskia Internal — `standards`\n",
"docstring": "# 🧩 Standards — the Aetos App Blueprint\n\nOne reusable architecture for every backend application in the Aetos ecosystem:\na spec-first HTTP edge, a framework-free domain core, a pluggable infrastructure\nlayer, and a single composition root.\n\nFill in **two axes only** and a new app is done: your **resource model**, and\n(if needed) your **pipeline family**. The blueprint itself never names a\nbusiness domain — expenses, health metrics, inventory, and analytics are all\nthe same shape underneath.\n\n> **Doc model:** this wiki is written for humans — how-to guides, the build\n> recipe, and quality gates. The enforceable rules (layering, composition root,\n> CRUD template, pipeline scaffold) and the library registry live in the\n> **reference** pages, mirrored by the machine-readable `standards` MCP bundle.\n> Query `find_library` before writing any infrastructure.\n\n---\n\n## 🚀 Key Features\n\n- 🧭 **Spec-first routing** — the OpenAPI document, not decorator routers, is\n the source of truth (operationId lookup)\n- 🏗️ **One-way layering** — `interfaces → domain → infra`, enforced\n- 🔌 **Single composition root** — every dependency is wired in exactly one place\n- 🧩 **Generic, not app-specific** — reusable rules versus two authoring axes\n- 📚 **Library-backed** — DB, DI, pipelines, ingestion, auth, and docs come from\n internal libraries, never hand-written\n- 🚦 **Explicit state machines** — durable job statuses, progress, and\n human-in-the-loop pauses\n\n---\n\n## 📁 Documentation Structure\n\n| Section | Description |\n|-----------------------------------------------------|--------------------------------------------------------|\n| [Overview](01_overview.md) | Philosophy, the one-way dependency rule, architecture |\n| [App Components](02_components.md) | The component model and how the pieces wire together |\n| [Start an App](03_start_an_app.md) | The entry point: gather the two axes, then build |\n| [Reference App](06_reference_app/01_overview.md) | The runnable vet sample — server, pipelines, client |\n| [Testing & Quality](04_development/01_testing.md) | Test layout and acceptance gates |\n\n**Reference** (separate site, mirrors the MCP bundle): the enforceable\nanatomy — start at the [Build Recipe](../lib/01_build_recipe/), then the\npillars: domains ([Layering](../lib/10_layering_rules/)), the OpenAPI\ncontract ([Composition Root](../lib/02_composition_root/),\n[Spec-First Routing](../lib/20_spec_first_routing/), [CRUD\nTemplate](../lib/21_crud_template/), [Auth\nIntegration](../lib/22_auth_integration/)), interfaces & infra ([Infra\nAdapters](../lib/30_infra_adaptors/)), UI ([Auto-Rendered\nAdmin](../lib/40_ui_admin/)), the optional pipelines layer ([Pipeline\nScaffold](../lib/50_pipeline_scaffold/), [Worker &\nProgress](../lib/51_worker_and_progress/), [Human-in-the-Loop](../lib/52_human_in_loop/)),\nand delivery/meta ([Genericity\nMatrix](../lib/92_genericity_matrix/), [Library\nRegistry](../lib/93_library_registry/)).\n\n---\n\n## 🔗 Related Resources\n\n- **MCP bundle** — `standards` server on `:8009`: read `docs://index`,\n `docs://nav`, `docs://modules/{module}`; call `find_library(capability)`\n- **Library registry** — the catalog of internal libraries behind every app\n (`libraries.json`, served as `registry://libraries`)\n- **Template** — the runnable sample at `standards/src/templates/app/`\n (server) and `standards/src/templates/client/` (React admin); walkthrough in\n the [Reference App](06_reference_app/01_overview.md)\n- **Start a build** — invoke the `start_app` prompt on the `standards` server,\n or read the [Start an App](03_start_an_app.md) guide\n\n---\n\n© Aetoskia Internal — `standards`\n",
"objects": {}
}
}

View File

@@ -0,0 +1,8 @@
{
"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",
"objects": {}
}
}

View File

@@ -0,0 +1,8 @@
{
"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",
"objects": {}
}
}

View File

@@ -0,0 +1,8 @@
{
"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 <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```\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 <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```\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": {}
}
}

View File

@@ -0,0 +1,8 @@
{
"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",
"objects": {}
}
}

View File

@@ -2,7 +2,7 @@
"module": "wiki_start_an_app",
"content": {
"path": "wiki_start_an_app",
"docstring": "# 🚀 Start an App\n\nThis is the entry point for building a new application from the blueprint. The\ngoal is one deterministic pass: you supply only the **two axes**, and the agent\ndoes the rest by reading the `standards` MCP, importing Tier A libraries, and\ncopying Tier B template components.\n\n> **Doc model:** this page is the human entry point. The deterministic,\n> machine-facing checklist lives in the reference page\n> [Build Recipe](../lib/01_build_recipe/), served as `docs://modules/build_recipe`.\n\n---\n\n## 🧾 What the agent needs from you\n\nGather these before starting — the `start_app` MCP prompt asks for them:\n\n1. **App name + one-line purpose.**\n2. **Axis 1 — resources.** Each resource, its fields, and relationships. Paste\n `openapi.yaml` if you already have it.\n3. **Axis 2 — pipelines.** Only if an input needs a multi-step transformation:\n the families and their input formats (or \"none\").\n4. **Auth needed now?** Default **no**. Auth is deferred (`py-jwt` primitives\n only), and an authless app is just a spec with no `security` keys — see\n [Spec-First Routing → Authless apps](../lib/20_spec_first_routing/#authless-apps).\n5. **Integrations.** Mongo, ingestion formats, email, LLM provider.\n\nIf you can state axes 1 and 2, the rest is mechanical.\n\n---\n\n## ▶️ How to start\n\nFrom an MCP client connected to the `standards` server:\n\n1. Invoke the **`intake`** prompt (or `start_app` with no arguments) to get the\n intake questions.\n2. Answer them, then invoke **`build`**\n (`build(app_name=..., resources=..., pipelines=..., auth=...)`), or use\n **`start_app`** to intake-and-build in one call.\n3. The prompt hands off to `docs://modules/build_recipe`; the agent scaffolds\n with `get_app_template()` and pulls exact files with\n `get_app_component(kind)`, authoring only the app-specific parts.\n4. To validate the standard without touching a real app, invoke **`trial`**\n (`trial(app_name=..., build_path=...)`) to build into a scratch directory,\n prove it builds, and report findings.\n\nManual alternative — copy the skeleton and follow the reference recipe:\n\n```bash\ncp -r <docs>/standards/src/templates/app apps/<your-app>\n```\n\n---\n\n## 📚 Reference\n\n- [Build Recipe](../lib/01_build_recipe/) — the deterministic checklist\n- [Genericity Matrix](../lib/92_genericity_matrix/) — Tier A / B / C\n- [Library Registry](../lib/93_library_registry/) — the Tier A catalog\n- [App Components](02_components.md) — the component model\n\n---\n\n## Related\n\n- [01 – Overview](01_overview.md) · [02 – App Components](02_components.md) · [04 – Testing & Quality](04_development/01_testing.md)\n",
"docstring": "# 🚀 Start an App\n\nThis is the entry point for building a new application from the blueprint. The\ngoal is one deterministic pass: you supply only the **two axes**, and the agent\ndoes the rest by reading the `standards` MCP, importing Tier A libraries, and\ncopying Tier B template components.\n\n> **Doc model:** this page is the human entry point. The deterministic,\n> machine-facing checklist lives in the reference page\n> [Build Recipe](../lib/01_build_recipe/), served as `docs://modules/build_recipe`.\n\n---\n\n## 🧾 What the agent needs from you\n\nGather these before starting — the `start_app` MCP prompt asks for them:\n\n1. **App name + one-line purpose.**\n2. **Axis 1 — resources.** Each resource, its fields, and relationships. Paste\n `openapi.yaml` if you already have it.\n3. **Axis 2 — pipelines.** Only if an input needs a multi-step transformation:\n the families and their input formats (or \"none\").\n4. **Auth needed now?** Default **no**. Auth is deferred (`py-jwt` primitives\n only), and an authless app is just a spec with no `security` keys — see\n [Spec-First Routing → Authless apps](../lib/20_spec_first_routing/#authless-apps).\n5. **Integrations.** Mongo, ingestion formats, email, LLM provider.\n\nIf you can state axes 1 and 2, the rest is mechanical.\n\n---\n\n## ▶️ How to start\n\nFrom an MCP client connected to the `standards` server:\n\n1. Invoke the **`intake`** prompt (or `start_app` with no arguments) to get the\n intake questions.\n2. Answer them, then invoke **`build`**\n (`build(app_name=..., resources=..., pipelines=..., auth=...)`), or use\n **`start_app`** to intake-and-build in one call.\n3. The prompt hands off to `docs://modules/build_recipe`; the agent scaffolds\n with `get_app_template()` and pulls exact files with\n `get_app_component(kind)`, authoring only the app-specific parts.\n4. To validate the standard without touching a real app, invoke **`trial`**\n (`trial(app_name=..., build_path=...)`) to build into a scratch directory,\n prove it builds, and report findings.\n\nManual alternative — copy the skeleton and follow the reference recipe:\n\n```bash\ncp -r <docs>/standards/src/templates/app apps/<your-app>\n```\n\n---\n\n## 📚 Reference\n\n- [Build Recipe](../lib/01_build_recipe/) — the deterministic checklist\n- [Genericity Matrix](../lib/92_genericity_matrix/) — Tier A / B / C\n- [Library Registry](../lib/93_library_registry/) — the Tier A catalog\n- [App Components](02_components.md) — the component model\n- [Reference App](06_reference_app/01_overview.md) — the runnable vet sample to copy\n\n---\n\n## Related\n\n- [01 – Overview](01_overview.md) · [02 – App Components](02_components.md) · [04 – Testing & Quality](04_development/01_testing.md)\n",
"objects": {}
}
}

View File

@@ -114,6 +114,26 @@
"resource": "docs://modules/wiki_mcp_setup",
"title": "🔌 MCP Setup"
},
{
"module": "wiki_reference_app_overview",
"resource": "docs://modules/wiki_reference_app_overview",
"title": "Overview"
},
{
"module": "wiki_reference_app_domain_and_api",
"resource": "docs://modules/wiki_reference_app_domain_and_api",
"title": "Domain And Api"
},
{
"module": "wiki_reference_app_pipelines_and_worker",
"resource": "docs://modules/wiki_reference_app_pipelines_and_worker",
"title": "Pipelines And Worker"
},
{
"module": "wiki_reference_app_client_admin",
"resource": "docs://modules/wiki_reference_app_client_admin",
"title": "Client Admin"
},
{
"module": "wiki_index",
"resource": "docs://modules/wiki_index",

View File

@@ -70,6 +70,11 @@ nav:
- Overview: 01_overview.md
- App Components: 02_components.md
- Start an App: 03_start_an_app.md
- Reference App:
- Overview: 06_reference_app/01_overview.md
- Domain & API: 06_reference_app/02_domain_and_api.md
- Pipelines & Worker: 06_reference_app/03_pipelines_and_worker.md
- Client Admin: 06_reference_app/04_client_admin.md
- MCP:
- Setup: 05_mcp_setup.md
- Testing & Quality: 04_development/01_testing.md

View File

@@ -59,6 +59,7 @@ cp -r <docs>/standards/src/templates/app apps/<your-app>
- [Genericity Matrix](../lib/92_genericity_matrix/) — Tier A / B / C
- [Library Registry](../lib/93_library_registry/) — the Tier A catalog
- [App Components](02_components.md) — the component model
- [Reference App](06_reference_app/01_overview.md) — the runnable vet sample to copy
---

View File

@@ -0,0 +1,70 @@
# 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
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`).
!!! note "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.
---
## The domain at a glance
| Aggregate | What it demonstrates |
|---------------|---------------------------------------------------------------|
| **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 |
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`.
## Run it
```bash
cp -r <docs>/standards/src/templates/app apps/vet-sample
cd apps/vet-sample
pip install -e ".[dev]"
uvicorn main:app --reload # API on :8020 (memory backend, zero services)
python worker.py # background worker (polls jobs)
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
`REPO_BACKEND=mongo` for persistence (see `.env.example`).
The client pairs with it:
```bash
cp -r <docs>/standards/src/templates/client apps/vet-sample-client
cd apps/vet-sample-client
echo "VITE_API_BASE_URL=http://localhost:8020" > .env.local
echo "VITE_AUTH_BASE_URL=http://localhost:8020" >> .env.local
npm install
npm run dev # Vite dev server
```
!!! warning "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 — 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

View File

@@ -0,0 +1,73 @@
# Domain & API — the resource axis
Every aggregate follows the same four-file slice
([CRUD Template](../lib/21_crud_template/)); walk `parents` first, then
`appointments` when you need foreign keys.
## The slice, end to end (parents)
```
domain/parents/
models.py # framework-free Pydantic model (id optional)
ports/
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
```
Wiring happens in exactly one place — the composition root
`init.py`:
```python
parent_repo = create_parent_repo()
_resources["parent_svc"] = get_parent_service(parent_repo)
```
Routes resolve the service from app state through the shared helper
(`interfaces/_common.py`):
```python
async def list_parents(request: Request, limit: int = 20, offset: int = 0) -> dict:
docs = await svc(request, "parent").search({})
return paginate([dump(d) for d in docs], limit, offset)
```
## The spec is the contract
`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 — see [Auto-Rendered Admin](../lib/40_ui_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
write path normalizes bare ids through `expand_ref`; the read path returns
fully expanded objects:
```python
pet = await expand_ref(svc(request, "pet"), body.get("pet"), "pet")
```
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
**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.
## Tests
- `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
service double (no HTTP client needed).

View File

@@ -0,0 +1,85 @@
# 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
`domain/jobs/` is an ordinary CRUD slice; its model is the durable state
machine the worker drives:
```python
class Job(BaseModel):
name: str
kind: str # dispatch key
status: Literal["pending", "processing", "done", "failed"] = "pending"
payload: dict | None = None
retry_count: int = 0
error_message: str | None = None
```
`POST /jobs` with `{"name": "...", "kind": "appointment_reminder",
"payload": {"appointment_id": 1}}` enqueues work; `GET /jobs?status=...`
inspects the queue.
## The appointment_reminder family
```
pipelines/appointment_reminder/
abc.py # LoadPort / ComposePort / DeliverPort + root interface
impl.py # concrete stages, wired root, register("appointment_reminder", ...)
pipeline.yaml # graph spec (nodes, edges, roots)
resources/
pipeline.yaml # runtime spec consumed by the loader
patterns/reminder.yaml # message template fragments
pipelines/shared/
ports.py loader.py registry.py # reused by every family
```
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:
```python
pipeline = get_appointment_reminder_pipeline(appointment_svc)
reminder = await pipeline.run(appointment_id)
```
To add a family: copy `appointment_reminder/`, replace the stages, call
`register(kind, variant, root_cls)` — see
[Pipeline Scaffold](../lib/50_pipeline_scaffold/).
## The worker
`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 → processing`
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
```python
HANDLERS: dict[str, Callable] = {
"appointment_reminder": run_appointment_reminder,
# add yours here
}
```
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.
!!! 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/).
## Tests
- `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
exhaustion, using an in-memory job service double and a cancelled poll task.

View File

@@ -0,0 +1,60 @@
# 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`,
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
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:
```ts
export const specConfiguration: SpecConfiguration = {
specUrl: `${apiBase}/openapi.json`,
baseApiUrl: apiBase,
title: "Vet Admin",
getToken: () => tokenStore.get(),
};
```
## Pair it with the sample server
```bash
cd apps/vet-sample-client
printf 'VITE_API_BASE_URL=http://localhost:8020\nVITE_AUTH_BASE_URL=http://localhost:8020\n' > .env.local
npm install
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
sides.
## Where the x- extensions come from
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}`).
## CI
The client ships a `drone-npm` shape (Type B in
[Drone Pipelines](../lib/90_drone_pipelines/)) and follows the
[Versioning](../lib/91_versioning/) standard (`package.json` +
`CHANGELOG.md`).

View File

@@ -38,6 +38,7 @@ the same shape underneath.
| [Overview](01_overview.md) | Philosophy, the one-way dependency rule, architecture |
| [App Components](02_components.md) | The component model and how the pieces wire together |
| [Start an App](03_start_an_app.md) | The entry point: gather the two axes, then build |
| [Reference App](06_reference_app/01_overview.md) | The runnable vet sample — server, pipelines, client |
| [Testing & Quality](04_development/01_testing.md) | Test layout and acceptance gates |
**Reference** (separate site, mirrors the MCP bundle): the enforceable
@@ -63,7 +64,9 @@ Registry](../lib/93_library_registry/)).
`docs://nav`, `docs://modules/{module}`; call `find_library(capability)`
- **Library registry** — the catalog of internal libraries behind every app
(`libraries.json`, served as `registry://libraries`)
- **Template** — the runnable skeleton at `standards/src/templates/app/`
- **Template** — the runnable sample at `standards/src/templates/app/`
(server) and `standards/src/templates/client/` (React admin); walkthrough in
the [Reference App](06_reference_app/01_overview.md)
- **Start a build** — invoke the `start_app` prompt on the `standards` server,
or read the [Start an App](03_start_an_app.md) guide

View File

@@ -279,6 +279,28 @@
<li class="md-tabs__item">
<a href="../06_reference_app/01_overview/" class="md-tabs__link">
Reference App
</a>
</li>
<li class="md-tabs__item">
<a href="../05_mcp_setup/" class="md-tabs__link">
@@ -553,6 +575,12 @@
@@ -576,7 +604,7 @@
<span class="md-ellipsis">
MCP
Reference App
</span>
@@ -586,6 +614,154 @@
<nav class="md-nav" data-md-level="1" aria-labelledby="__nav_5_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_5">
<span class="md-nav__icon md-icon"></span>
Reference App
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../06_reference_app/01_overview/" class="md-nav__link">
<span class="md-ellipsis">
Overview
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../06_reference_app/02_domain_and_api/" class="md-nav__link">
<span class="md-ellipsis">
Domain & API
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../06_reference_app/03_pipelines_and_worker/" class="md-nav__link">
<span class="md-ellipsis">
Pipelines & Worker
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../06_reference_app/04_client_admin/" class="md-nav__link">
<span class="md-ellipsis">
Client Admin
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--nested">
<input class="md-nav__toggle md-toggle md-toggle--indeterminate" type="checkbox" id="__nav_6" >
<label class="md-nav__link" for="__nav_6" id="__nav_6_label" tabindex="0">
<span class="md-ellipsis">
MCP
</span>
<span class="md-nav__icon md-icon"></span>
</label>
<nav class="md-nav" data-md-level="1" aria-labelledby="__nav_6_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_6">
<span class="md-nav__icon md-icon"></span>
MCP
</label>

View File

@@ -279,6 +279,28 @@
<li class="md-tabs__item">
<a href="../06_reference_app/01_overview/" class="md-tabs__link">
Reference App
</a>
</li>
<li class="md-tabs__item">
<a href="../05_mcp_setup/" class="md-tabs__link">
@@ -628,6 +650,12 @@
@@ -651,7 +679,7 @@
<span class="md-ellipsis">
MCP
Reference App
</span>
@@ -661,6 +689,154 @@
<nav class="md-nav" data-md-level="1" aria-labelledby="__nav_5_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_5">
<span class="md-nav__icon md-icon"></span>
Reference App
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../06_reference_app/01_overview/" class="md-nav__link">
<span class="md-ellipsis">
Overview
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../06_reference_app/02_domain_and_api/" class="md-nav__link">
<span class="md-ellipsis">
Domain & API
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../06_reference_app/03_pipelines_and_worker/" class="md-nav__link">
<span class="md-ellipsis">
Pipelines & Worker
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../06_reference_app/04_client_admin/" class="md-nav__link">
<span class="md-ellipsis">
Client Admin
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--nested">
<input class="md-nav__toggle md-toggle md-toggle--indeterminate" type="checkbox" id="__nav_6" >
<label class="md-nav__link" for="__nav_6" id="__nav_6_label" tabindex="0">
<span class="md-ellipsis">
MCP
</span>
<span class="md-nav__icon md-icon"></span>
</label>
<nav class="md-nav" data-md-level="1" aria-labelledby="__nav_6_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_6">
<span class="md-nav__icon md-icon"></span>
MCP
</label>

View File

@@ -14,7 +14,7 @@
<link rel="prev" href="../02_components/">
<link rel="next" href="../05_mcp_setup/">
<link rel="next" href="../06_reference_app/01_overview/">
<link rel="icon" href="../assets/images/favicon.png">
@@ -279,6 +279,28 @@
<li class="md-tabs__item">
<a href="../06_reference_app/01_overview/" class="md-tabs__link">
Reference App
</a>
</li>
<li class="md-tabs__item">
<a href="../05_mcp_setup/" class="md-tabs__link">
@@ -544,6 +566,12 @@
@@ -567,7 +595,7 @@
<span class="md-ellipsis">
MCP
Reference App
</span>
@@ -577,6 +605,154 @@
<nav class="md-nav" data-md-level="1" aria-labelledby="__nav_5_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_5">
<span class="md-nav__icon md-icon"></span>
Reference App
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../06_reference_app/01_overview/" class="md-nav__link">
<span class="md-ellipsis">
Overview
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../06_reference_app/02_domain_and_api/" class="md-nav__link">
<span class="md-ellipsis">
Domain & API
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../06_reference_app/03_pipelines_and_worker/" class="md-nav__link">
<span class="md-ellipsis">
Pipelines & Worker
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../06_reference_app/04_client_admin/" class="md-nav__link">
<span class="md-ellipsis">
Client Admin
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--nested">
<input class="md-nav__toggle md-toggle md-toggle--indeterminate" type="checkbox" id="__nav_6" >
<label class="md-nav__link" for="__nav_6" id="__nav_6_label" tabindex="0">
<span class="md-ellipsis">
MCP
</span>
<span class="md-nav__icon md-icon"></span>
</label>
<nav class="md-nav" data-md-level="1" aria-labelledby="__nav_6_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_6">
<span class="md-nav__icon md-icon"></span>
MCP
</label>
@@ -723,6 +899,7 @@ machine-facing checklist lives in the reference page
<li><a href="../../lib/92_genericity_matrix/">Genericity Matrix</a> — Tier A / B / C</li>
<li><a href="../../lib/93_library_registry/">Library Registry</a> — the Tier A catalog</li>
<li><a href="../02_components/">App Components</a> — the component model</li>
<li><a href="../06_reference_app/01_overview/">Reference App</a> — the runnable vet sample to copy</li>
</ul>
<hr />
<h2 id="related">Related</h2>

View File

@@ -277,6 +277,28 @@
<li class="md-tabs__item">
<a href="../../06_reference_app/01_overview/" class="md-tabs__link">
Reference App
</a>
</li>
<li class="md-tabs__item">
<a href="../../05_mcp_setup/" class="md-tabs__link">
@@ -466,6 +488,12 @@
@@ -489,7 +517,7 @@
<span class="md-ellipsis">
MCP
Reference App
</span>
@@ -499,6 +527,154 @@
<nav class="md-nav" data-md-level="1" aria-labelledby="__nav_5_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_5">
<span class="md-nav__icon md-icon"></span>
Reference App
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../../06_reference_app/01_overview/" class="md-nav__link">
<span class="md-ellipsis">
Overview
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../06_reference_app/02_domain_and_api/" class="md-nav__link">
<span class="md-ellipsis">
Domain & API
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../06_reference_app/03_pipelines_and_worker/" class="md-nav__link">
<span class="md-ellipsis">
Pipelines & Worker
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../06_reference_app/04_client_admin/" class="md-nav__link">
<span class="md-ellipsis">
Client Admin
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--nested">
<input class="md-nav__toggle md-toggle md-toggle--indeterminate" type="checkbox" id="__nav_6" >
<label class="md-nav__link" for="__nav_6" id="__nav_6_label" tabindex="0">
<span class="md-ellipsis">
MCP
</span>
<span class="md-nav__icon md-icon"></span>
</label>
<nav class="md-nav" data-md-level="1" aria-labelledby="__nav_6_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_6">
<span class="md-nav__icon md-icon"></span>
MCP
</label>

View File

@@ -275,6 +275,28 @@
<li class="md-tabs__item">
<a href="../../06_reference_app/01_overview/" class="md-tabs__link">
Reference App
</a>
</li>
<li class="md-tabs__item">
<a href="../../05_mcp_setup/" class="md-tabs__link">
@@ -464,6 +486,12 @@
@@ -487,7 +515,7 @@
<span class="md-ellipsis">
MCP
Reference App
</span>
@@ -497,6 +525,154 @@
<nav class="md-nav" data-md-level="1" aria-labelledby="__nav_5_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_5">
<span class="md-nav__icon md-icon"></span>
Reference App
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../../06_reference_app/01_overview/" class="md-nav__link">
<span class="md-ellipsis">
Overview
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../06_reference_app/02_domain_and_api/" class="md-nav__link">
<span class="md-ellipsis">
Domain & API
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../06_reference_app/03_pipelines_and_worker/" class="md-nav__link">
<span class="md-ellipsis">
Pipelines & Worker
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../06_reference_app/04_client_admin/" class="md-nav__link">
<span class="md-ellipsis">
Client Admin
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--nested">
<input class="md-nav__toggle md-toggle md-toggle--indeterminate" type="checkbox" id="__nav_6" >
<label class="md-nav__link" for="__nav_6" id="__nav_6_label" tabindex="0">
<span class="md-ellipsis">
MCP
</span>
<span class="md-nav__icon md-icon"></span>
</label>
<nav class="md-nav" data-md-level="1" aria-labelledby="__nav_6_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_6">
<span class="md-nav__icon md-icon"></span>
MCP
</label>

View File

@@ -11,7 +11,7 @@
<link rel="prev" href="../03_start_an_app/">
<link rel="prev" href="../06_reference_app/04_client_admin/">
<link rel="next" href="../04_development/01_testing/">
@@ -274,6 +274,28 @@
<li class="md-tabs__item">
<a href="../06_reference_app/01_overview/" class="md-tabs__link">
Reference App
</a>
</li>
@@ -459,6 +481,160 @@
<li class="md-nav__item md-nav__item--nested">
<input class="md-nav__toggle md-toggle md-toggle--indeterminate" type="checkbox" id="__nav_5" >
<label class="md-nav__link" for="__nav_5" id="__nav_5_label" tabindex="0">
<span class="md-ellipsis">
Reference App
</span>
<span class="md-nav__icon md-icon"></span>
</label>
<nav class="md-nav" data-md-level="1" aria-labelledby="__nav_5_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_5">
<span class="md-nav__icon md-icon"></span>
Reference App
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../06_reference_app/01_overview/" class="md-nav__link">
<span class="md-ellipsis">
Overview
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../06_reference_app/02_domain_and_api/" class="md-nav__link">
<span class="md-ellipsis">
Domain & API
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../06_reference_app/03_pipelines_and_worker/" class="md-nav__link">
<span class="md-ellipsis">
Pipelines & Worker
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../06_reference_app/04_client_admin/" class="md-nav__link">
<span class="md-ellipsis">
Client Admin
</span>
</a>
</li>
</ul>
</nav>
</li>
@@ -484,10 +660,10 @@
<input class="md-nav__toggle md-toggle " type="checkbox" id="__nav_5" checked>
<input class="md-nav__toggle md-toggle " type="checkbox" id="__nav_6" checked>
<label class="md-nav__link" for="__nav_5" id="__nav_5_label" tabindex="">
<label class="md-nav__link" for="__nav_6" id="__nav_6_label" tabindex="">
@@ -500,8 +676,8 @@
<span class="md-nav__icon md-icon"></span>
</label>
<nav class="md-nav" data-md-level="1" aria-labelledby="__nav_5_label" aria-expanded="true">
<label class="md-nav__title" for="__nav_5">
<nav class="md-nav" data-md-level="1" aria-labelledby="__nav_6_label" aria-expanded="true">
<label class="md-nav__title" for="__nav_6">
<span class="md-nav__icon md-icon"></span>
MCP
</label>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,990 @@
<!doctype html>
<html lang="en" class="no-js">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="description" content="The philosophy and prompts behind the standardized application blueprint.">
<link rel="prev" href="../01_overview/">
<link rel="next" href="../03_pipelines_and_worker/">
<link rel="icon" href="../../assets/images/favicon.png">
<meta name="generator" content="mkdocs-1.6.1, mkdocs-material-9.6.23">
<title>Domain & API - Aetos Standards — Wiki</title>
<link rel="stylesheet" href="../../assets/stylesheets/main.84d31ad4.min.css">
<link rel="stylesheet" href="../../assets/stylesheets/palette.06af60db.min.css">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Inter:300,300i,400,400i,700,700i%7CJetBrains+Mono:400,400i,700,700i&display=fallback">
<style>:root{--md-text-font:"Inter";--md-code-font:"JetBrains Mono"}</style>
<script>__md_scope=new URL("../..",location),__md_hash=e=>[...e].reduce(((e,_)=>(e<<5)-e+_.charCodeAt(0)),0),__md_get=(e,_=localStorage,t=__md_scope)=>JSON.parse(_.getItem(t.pathname+"."+e)),__md_set=(e,_,t=localStorage,a=__md_scope)=>{try{t.setItem(a.pathname+"."+e,JSON.stringify(_))}catch(e){}}</script>
</head>
<body dir="ltr" data-md-color-scheme="slate" data-md-color-primary="deep-purple" data-md-color-accent="cyan">
<input class="md-toggle" data-md-toggle="drawer" type="checkbox" id="__drawer" autocomplete="off">
<input class="md-toggle" data-md-toggle="search" type="checkbox" id="__search" autocomplete="off">
<label class="md-overlay" for="__drawer"></label>
<div data-md-component="skip">
<a href="#domain-api-a-the-resource-axis" class="md-skip">
Skip to content
</a>
</div>
<div data-md-component="announce">
</div>
<header class="md-header" data-md-component="header">
<nav class="md-header__inner md-grid" aria-label="Header">
<a href="../.." title="Aetos Standards — Wiki" class="md-header__button md-logo" aria-label="Aetos Standards — Wiki" data-md-component="logo">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 8a3 3 0 0 0 3-3 3 3 0 0 0-3-3 3 3 0 0 0-3 3 3 3 0 0 0 3 3m0 3.54C9.64 9.35 6.5 8 3 8v11c3.5 0 6.64 1.35 9 3.54 2.36-2.19 5.5-3.54 9-3.54V8c-3.5 0-6.64 1.35-9 3.54"/></svg>
</a>
<label class="md-header__button md-icon" for="__drawer">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M3 6h18v2H3zm0 5h18v2H3zm0 5h18v2H3z"/></svg>
</label>
<div class="md-header__title" data-md-component="header-title">
<div class="md-header__ellipsis">
<div class="md-header__topic">
<span class="md-ellipsis">
Aetos Standards — Wiki
</span>
</div>
<div class="md-header__topic" data-md-component="header-topic">
<span class="md-ellipsis">
Domain & API
</span>
</div>
</div>
</div>
<form class="md-header__option" data-md-component="palette">
<input class="md-option" data-md-color-media="" data-md-color-scheme="slate" data-md-color-primary="deep-purple" data-md-color-accent="cyan" aria-hidden="true" type="radio" name="__palette" id="__palette_0">
</form>
<script>var palette=__md_get("__palette");if(palette&&palette.color){if("(prefers-color-scheme)"===palette.color.media){var media=matchMedia("(prefers-color-scheme: light)"),input=document.querySelector(media.matches?"[data-md-color-media='(prefers-color-scheme: light)']":"[data-md-color-media='(prefers-color-scheme: dark)']");palette.color.media=input.getAttribute("data-md-color-media"),palette.color.scheme=input.getAttribute("data-md-color-scheme"),palette.color.primary=input.getAttribute("data-md-color-primary"),palette.color.accent=input.getAttribute("data-md-color-accent")}for(var[key,value]of Object.entries(palette.color))document.body.setAttribute("data-md-color-"+key,value)}</script>
<label class="md-header__button md-icon" for="__search">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M9.5 3A6.5 6.5 0 0 1 16 9.5c0 1.61-.59 3.09-1.56 4.23l.27.27h.79l5 5-1.5 1.5-5-5v-.79l-.27-.27A6.52 6.52 0 0 1 9.5 16 6.5 6.5 0 0 1 3 9.5 6.5 6.5 0 0 1 9.5 3m0 2C7 5 5 7 5 9.5S7 14 9.5 14 14 12 14 9.5 12 5 9.5 5"/></svg>
</label>
<div class="md-search" data-md-component="search" role="dialog">
<label class="md-search__overlay" for="__search"></label>
<div class="md-search__inner" role="search">
<form class="md-search__form" name="search">
<input type="text" class="md-search__input" name="query" aria-label="Search" placeholder="Search" autocapitalize="off" autocorrect="off" autocomplete="off" spellcheck="false" data-md-component="search-query" required>
<label class="md-search__icon md-icon" for="__search">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M9.5 3A6.5 6.5 0 0 1 16 9.5c0 1.61-.59 3.09-1.56 4.23l.27.27h.79l5 5-1.5 1.5-5-5v-.79l-.27-.27A6.52 6.52 0 0 1 9.5 16 6.5 6.5 0 0 1 3 9.5 6.5 6.5 0 0 1 9.5 3m0 2C7 5 5 7 5 9.5S7 14 9.5 14 14 12 14 9.5 12 5 9.5 5"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M20 11v2H8l5.5 5.5-1.42 1.42L4.16 12l7.92-7.92L13.5 5.5 8 11z"/></svg>
</label>
<nav class="md-search__options" aria-label="Search">
<a href="javascript:void(0)" class="md-search__icon md-icon" title="Share" aria-label="Share" data-clipboard data-clipboard-text="" data-md-component="search-share" tabindex="-1">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7c.05-.23.09-.46.09-.7s-.04-.47-.09-.7l7.05-4.11c.54.5 1.25.81 2.04.81a3 3 0 0 0 3-3 3 3 0 0 0-3-3 3 3 0 0 0-3 3c0 .24.04.47.09.7L8.04 9.81C7.5 9.31 6.79 9 6 9a3 3 0 0 0-3 3 3 3 0 0 0 3 3c.79 0 1.5-.31 2.04-.81l7.12 4.15c-.05.21-.08.43-.08.66 0 1.61 1.31 2.91 2.92 2.91s2.92-1.3 2.92-2.91A2.92 2.92 0 0 0 18 16.08"/></svg>
</a>
<button type="reset" class="md-search__icon md-icon" title="Clear" aria-label="Clear" tabindex="-1">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>
</button>
</nav>
<div class="md-search__suggest" data-md-component="search-suggest"></div>
</form>
<div class="md-search__output">
<div class="md-search__scrollwrap" tabindex="0" data-md-scrollfix>
<div class="md-search-result" data-md-component="search-result">
<div class="md-search-result__meta">
Initializing search
</div>
<ol class="md-search-result__list" role="presentation"></ol>
</div>
</div>
</div>
</div>
</div>
</nav>
</header>
<div class="md-container" data-md-component="container">
<nav class="md-tabs" aria-label="Tabs" data-md-component="tabs">
<div class="md-grid">
<ul class="md-tabs__list">
<li class="md-tabs__item">
<a href="../.." class="md-tabs__link">
Home
</a>
</li>
<li class="md-tabs__item">
<a href="../../01_overview/" class="md-tabs__link">
Overview
</a>
</li>
<li class="md-tabs__item">
<a href="../../02_components/" class="md-tabs__link">
App Components
</a>
</li>
<li class="md-tabs__item">
<a href="../../03_start_an_app/" class="md-tabs__link">
Start an App
</a>
</li>
<li class="md-tabs__item md-tabs__item--active">
<a href="../01_overview/" class="md-tabs__link">
Reference App
</a>
</li>
<li class="md-tabs__item">
<a href="../../05_mcp_setup/" class="md-tabs__link">
MCP
</a>
</li>
<li class="md-tabs__item">
<a href="../../04_development/01_testing/" class="md-tabs__link">
Testing & Quality
</a>
</li>
<li class="md-tabs__item">
<a href="../../04_development/02_releasing/" class="md-tabs__link">
Releasing & Versioning
</a>
</li>
</ul>
</div>
</nav>
<main class="md-main" data-md-component="main">
<div class="md-main__inner md-grid">
<div class="md-sidebar md-sidebar--primary" data-md-component="sidebar" data-md-type="navigation" >
<div class="md-sidebar__scrollwrap">
<div class="md-sidebar__inner">
<nav class="md-nav md-nav--primary md-nav--lifted md-nav--integrated" aria-label="Navigation" data-md-level="0">
<label class="md-nav__title" for="__drawer">
<a href="../.." title="Aetos Standards — Wiki" class="md-nav__button md-logo" aria-label="Aetos Standards — Wiki" data-md-component="logo">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 8a3 3 0 0 0 3-3 3 3 0 0 0-3-3 3 3 0 0 0-3 3 3 3 0 0 0 3 3m0 3.54C9.64 9.35 6.5 8 3 8v11c3.5 0 6.64 1.35 9 3.54 2.36-2.19 5.5-3.54 9-3.54V8c-3.5 0-6.64 1.35-9 3.54"/></svg>
</a>
Aetos Standards — Wiki
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../.." class="md-nav__link">
<span class="md-ellipsis">
Home
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../01_overview/" class="md-nav__link">
<span class="md-ellipsis">
Overview
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../02_components/" class="md-nav__link">
<span class="md-ellipsis">
App Components
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../03_start_an_app/" class="md-nav__link">
<span class="md-ellipsis">
Start an App
</span>
</a>
</li>
<li class="md-nav__item md-nav__item--active md-nav__item--section md-nav__item--nested">
<input class="md-nav__toggle md-toggle " type="checkbox" id="__nav_5" checked>
<label class="md-nav__link" for="__nav_5" id="__nav_5_label" tabindex="">
<span class="md-ellipsis">
Reference App
</span>
<span class="md-nav__icon md-icon"></span>
</label>
<nav class="md-nav" data-md-level="1" aria-labelledby="__nav_5_label" aria-expanded="true">
<label class="md-nav__title" for="__nav_5">
<span class="md-nav__icon md-icon"></span>
Reference App
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../01_overview/" class="md-nav__link">
<span class="md-ellipsis">
Overview
</span>
</a>
</li>
<li class="md-nav__item md-nav__item--active">
<input class="md-nav__toggle md-toggle" type="checkbox" id="__toc">
<label class="md-nav__link md-nav__link--active" for="__toc">
<span class="md-ellipsis">
Domain & API
</span>
<span class="md-nav__icon md-icon"></span>
</label>
<a href="./" class="md-nav__link md-nav__link--active">
<span class="md-ellipsis">
Domain & API
</span>
</a>
<nav class="md-nav md-nav--secondary" aria-label="Table of contents">
<label class="md-nav__title" for="__toc">
<span class="md-nav__icon md-icon"></span>
Table of contents
</label>
<ul class="md-nav__list" data-md-component="toc" data-md-scrollfix>
<li class="md-nav__item">
<a href="#the-slice-end-to-end-parents" class="md-nav__link">
<span class="md-ellipsis">
The slice, end to end (parents)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#the-spec-is-the-contract" class="md-nav__link">
<span class="md-ellipsis">
The spec is the contract
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#foreign-keys-appointments" class="md-nav__link">
<span class="md-ellipsis">
Foreign keys (appointments)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#ids-on-the-wire" class="md-nav__link">
<span class="md-ellipsis">
Ids on the wire
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#tests" class="md-nav__link">
<span class="md-ellipsis">
Tests
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="../03_pipelines_and_worker/" class="md-nav__link">
<span class="md-ellipsis">
Pipelines & Worker
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../04_client_admin/" class="md-nav__link">
<span class="md-ellipsis">
Client Admin
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--nested">
<input class="md-nav__toggle md-toggle md-toggle--indeterminate" type="checkbox" id="__nav_6" >
<label class="md-nav__link" for="__nav_6" id="__nav_6_label" tabindex="0">
<span class="md-ellipsis">
MCP
</span>
<span class="md-nav__icon md-icon"></span>
</label>
<nav class="md-nav" data-md-level="1" aria-labelledby="__nav_6_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_6">
<span class="md-nav__icon md-icon"></span>
MCP
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../../05_mcp_setup/" class="md-nav__link">
<span class="md-ellipsis">
Setup
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="../../04_development/01_testing/" class="md-nav__link">
<span class="md-ellipsis">
Testing & Quality
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../04_development/02_releasing/" class="md-nav__link">
<span class="md-ellipsis">
Releasing & Versioning
</span>
</a>
</li>
</ul>
</nav>
</div>
</div>
</div>
<div class="md-content" data-md-component="content">
<article class="md-content__inner md-typeset">
<h1 id="domain-api-a-the-resource-axis">Domain &amp; API — the resource axis</h1>
<p>Every aggregate follows the same four-file slice
(<a href="../../../lib/21_crud_template/">CRUD Template</a>); walk <code>parents</code> first, then
<code>appointments</code> when you need foreign keys.</p>
<h2 id="the-slice-end-to-end-parents">The slice, end to end (parents)</h2>
<div class="language-text highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal"><a href="#__codelineno-0-1">1</a></span>
<span class="normal"><a href="#__codelineno-0-2">2</a></span>
<span class="normal"><a href="#__codelineno-0-3">3</a></span>
<span class="normal"><a href="#__codelineno-0-4">4</a></span>
<span class="normal"><a href="#__codelineno-0-5">5</a></span>
<span class="normal"><a href="#__codelineno-0-6">6</a></span>
<span class="normal"><a href="#__codelineno-0-7">7</a></span></pre></div></td><td class="code"><div><pre><span></span><code><span id="__span-0-1"><a id="__codelineno-0-1" name="__codelineno-0-1"></a>domain/parents/
</span><span id="__span-0-2"><a id="__codelineno-0-2" name="__codelineno-0-2"></a> models.py # framework-free Pydantic model (id optional)
</span><span id="__span-0-3"><a id="__codelineno-0-3" name="__codelineno-0-3"></a> ports/
</span><span id="__span-0-4"><a id="__codelineno-0-4" name="__codelineno-0-4"></a> db.py # ParentDocument(Parent, BaseDocument) + create_parent_repo()
</span><span id="__span-0-5"><a id="__codelineno-0-5" name="__codelineno-0-5"></a> service.py # ParentCrudsService(CrudsServiceAdaptor[...]) + get_parent_service()
</span><span id="__span-0-6"><a id="__codelineno-0-6" name="__codelineno-0-6"></a>interfaces/parents/
</span><span id="__span-0-7"><a id="__codelineno-0-7" name="__codelineno-0-7"></a> routes.py # list/create/get/update/delete — names == operationIds
</span></code></pre></div></td></tr></table></div>
<p>Wiring happens in exactly one place — the composition root
<code>init.py</code>:</p>
<div class="language-python highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal"><a href="#__codelineno-1-1">1</a></span>
<span class="normal"><a href="#__codelineno-1-2">2</a></span></pre></div></td><td class="code"><div><pre><span></span><code><span id="__span-1-1"><a id="__codelineno-1-1" name="__codelineno-1-1"></a><span class="n">parent_repo</span> <span class="o">=</span> <span class="n">create_parent_repo</span><span class="p">()</span>
</span><span id="__span-1-2"><a id="__codelineno-1-2" name="__codelineno-1-2"></a><span class="n">_resources</span><span class="p">[</span><span class="s2">&quot;parent_svc&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">get_parent_service</span><span class="p">(</span><span class="n">parent_repo</span><span class="p">)</span>
</span></code></pre></div></td></tr></table></div>
<p>Routes resolve the service from app state through the shared helper
(<code>interfaces/_common.py</code>):</p>
<div class="language-python highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal"><a href="#__codelineno-2-1">1</a></span>
<span class="normal"><a href="#__codelineno-2-2">2</a></span>
<span class="normal"><a href="#__codelineno-2-3">3</a></span></pre></div></td><td class="code"><div><pre><span></span><code><span id="__span-2-1"><a id="__codelineno-2-1" name="__codelineno-2-1"></a><span class="k">async</span> <span class="k">def</span><span class="w"> </span><span class="nf">list_parents</span><span class="p">(</span><span class="n">request</span><span class="p">:</span> <span class="n">Request</span><span class="p">,</span> <span class="n">limit</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">20</span><span class="p">,</span> <span class="n">offset</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">0</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">dict</span><span class="p">:</span>
</span><span id="__span-2-2"><a id="__codelineno-2-2" name="__codelineno-2-2"></a> <span class="n">docs</span> <span class="o">=</span> <span class="k">await</span> <span class="n">svc</span><span class="p">(</span><span class="n">request</span><span class="p">,</span> <span class="s2">&quot;parent&quot;</span><span class="p">)</span><span class="o">.</span><span class="n">search</span><span class="p">({})</span>
</span><span id="__span-2-3"><a id="__codelineno-2-3" name="__codelineno-2-3"></a> <span class="k">return</span> <span class="n">paginate</span><span class="p">([</span><span class="n">dump</span><span class="p">(</span><span class="n">d</span><span class="p">)</span> <span class="k">for</span> <span class="n">d</span> <span class="ow">in</span> <span class="n">docs</span><span class="p">],</span> <span class="n">limit</span><span class="p">,</span> <span class="n">offset</span><span class="p">)</span>
</span></code></pre></div></td></tr></table></div>
<h2 id="the-spec-is-the-contract">The spec is the contract</h2>
<p><code>openapi.yaml</code> is OpenAPI 3.1 and the
single source of truth: <code>main.py</code> builds an <code>OpenAPIFirstApp</code> that binds each
<code>operationId</code> to a function of the same name in <code>interfaces/openapi.py</code> (a flat
re-export of every routes module). There are no decorator routers.</p>
<p>The x- extension fields on schemas and properties are what make the admin
client render itself — see <a href="../../../lib/40_ui_admin/">Auto-Rendered Admin</a> and
the worked <code>Job</code> schema (enum selects, hidden fields, filterable columns).</p>
<h2 id="foreign-keys-appointments">Foreign keys (appointments)</h2>
<p><code>Appointment</code> declares <code>pet</code>, <code>vet</code>, <code>treatment</code> as <code>dict | int | None</code>. The
write path normalizes bare ids through <code>expand_ref</code>; the read path returns
fully expanded objects:</p>
<div class="language-python highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal"><a href="#__codelineno-3-1">1</a></span></pre></div></td><td class="code"><div><pre><span></span><code><span id="__span-3-1"><a id="__codelineno-3-1" name="__codelineno-3-1"></a><span class="n">pet</span> <span class="o">=</span> <span class="k">await</span> <span class="n">expand_ref</span><span class="p">(</span><span class="n">svc</span><span class="p">(</span><span class="n">request</span><span class="p">,</span> <span class="s2">&quot;pet&quot;</span><span class="p">),</span> <span class="n">body</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&quot;pet&quot;</span><span class="p">),</span> <span class="s2">&quot;pet&quot;</span><span class="p">)</span>
</span></code></pre></div></td></tr></table></div>
<p>The list endpoint additionally filters by date prefix and embedded fk ids —
client-side over the repository result, the pattern for small reference data.</p>
<h2 id="ids-on-the-wire">Ids on the wire</h2>
<p>The domain models declare <code>id: int | None = None</code> and documents use the
<strong>integer-id</strong> repository variants (<code>infra/db/repos/mongo_int.py</code>,
<code>create_repo()</code> in <code>domain/*/ports/db.py</code>, <code>REPO_BACKEND=memory|mongo</code>). This
keeps the OpenAPI <code>id: integer</code> contract, the in-memory demo store, and the
Mongo persistence layer consistent.</p>
<h2 id="tests">Tests</h2>
<ul>
<li><code>tests/integration/test_openapi_app_builds.py</code> — generic gate: every
<code>operationId</code> resolves and the app builds. Copy it verbatim.</li>
<li><code>tests/unit/test_jobs_routes.py</code> — the pattern for route unit tests with a
service double (no HTTP client needed).</li>
</ul>
</article>
</div>
<script>var tabs=__md_get("__tabs");if(Array.isArray(tabs))e:for(var set of document.querySelectorAll(".tabbed-set")){var labels=set.querySelector(".tabbed-labels");for(var tab of tabs)for(var label of labels.getElementsByTagName("label"))if(label.innerText.trim()===tab){var input=document.getElementById(label.htmlFor);input.checked=!0;continue e}}</script>
<script>var target=document.getElementById(location.hash.slice(1));target&&target.name&&(target.checked=target.name.startsWith("__tabbed_"))</script>
</div>
<button type="button" class="md-top md-icon" data-md-component="top" hidden>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M13 20h-2V8l-5.5 5.5-1.42-1.42L12 4.16l7.92 7.92-1.42 1.42L13 8z"/></svg>
Back to top
</button>
</main>
<footer class="md-footer">
<div class="md-footer-meta md-typeset">
<div class="md-footer-meta__inner md-grid">
<div class="md-copyright">
Made with
<a href="https://squidfunk.github.io/mkdocs-material/" target="_blank" rel="noopener">
Material for MkDocs
</a>
</div>
</div>
</div>
</footer>
</div>
<div class="md-dialog" data-md-component="dialog">
<div class="md-dialog__inner md-typeset"></div>
</div>
<script id="__config" type="application/json">{"base": "../..", "features": ["navigation.sections", "navigation.expand", "navigation.top", "navigation.instant", "navigation.tracking", "navigation.indexes", "navigation.tabs", "toc.integrate", "header.autohide", "content.code.copy", "content.code.annotate", "content.tabs.link", "content.action.edit", "search.highlight", "search.share", "search.suggest"], "search": "../../assets/javascripts/workers/search.973d3a69.min.js", "tags": null, "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}, "version": null}</script>
<script src="../../assets/javascripts/bundle.f55a23d4.min.js"></script>
<script src="https://unpkg.com/mermaid@10.9.1/dist/mermaid.min.js"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,974 @@
<!doctype html>
<html lang="en" class="no-js">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="description" content="The philosophy and prompts behind the standardized application blueprint.">
<link rel="prev" href="../03_pipelines_and_worker/">
<link rel="next" href="../../05_mcp_setup/">
<link rel="icon" href="../../assets/images/favicon.png">
<meta name="generator" content="mkdocs-1.6.1, mkdocs-material-9.6.23">
<title>Client Admin - Aetos Standards — Wiki</title>
<link rel="stylesheet" href="../../assets/stylesheets/main.84d31ad4.min.css">
<link rel="stylesheet" href="../../assets/stylesheets/palette.06af60db.min.css">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Inter:300,300i,400,400i,700,700i%7CJetBrains+Mono:400,400i,700,700i&display=fallback">
<style>:root{--md-text-font:"Inter";--md-code-font:"JetBrains Mono"}</style>
<script>__md_scope=new URL("../..",location),__md_hash=e=>[...e].reduce(((e,_)=>(e<<5)-e+_.charCodeAt(0)),0),__md_get=(e,_=localStorage,t=__md_scope)=>JSON.parse(_.getItem(t.pathname+"."+e)),__md_set=(e,_,t=localStorage,a=__md_scope)=>{try{t.setItem(a.pathname+"."+e,JSON.stringify(_))}catch(e){}}</script>
</head>
<body dir="ltr" data-md-color-scheme="slate" data-md-color-primary="deep-purple" data-md-color-accent="cyan">
<input class="md-toggle" data-md-toggle="drawer" type="checkbox" id="__drawer" autocomplete="off">
<input class="md-toggle" data-md-toggle="search" type="checkbox" id="__search" autocomplete="off">
<label class="md-overlay" for="__drawer"></label>
<div data-md-component="skip">
<a href="#client-admin-a-the-spec-driven-frontend" class="md-skip">
Skip to content
</a>
</div>
<div data-md-component="announce">
</div>
<header class="md-header" data-md-component="header">
<nav class="md-header__inner md-grid" aria-label="Header">
<a href="../.." title="Aetos Standards — Wiki" class="md-header__button md-logo" aria-label="Aetos Standards — Wiki" data-md-component="logo">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 8a3 3 0 0 0 3-3 3 3 0 0 0-3-3 3 3 0 0 0-3 3 3 3 0 0 0 3 3m0 3.54C9.64 9.35 6.5 8 3 8v11c3.5 0 6.64 1.35 9 3.54 2.36-2.19 5.5-3.54 9-3.54V8c-3.5 0-6.64 1.35-9 3.54"/></svg>
</a>
<label class="md-header__button md-icon" for="__drawer">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M3 6h18v2H3zm0 5h18v2H3zm0 5h18v2H3z"/></svg>
</label>
<div class="md-header__title" data-md-component="header-title">
<div class="md-header__ellipsis">
<div class="md-header__topic">
<span class="md-ellipsis">
Aetos Standards — Wiki
</span>
</div>
<div class="md-header__topic" data-md-component="header-topic">
<span class="md-ellipsis">
Client Admin
</span>
</div>
</div>
</div>
<form class="md-header__option" data-md-component="palette">
<input class="md-option" data-md-color-media="" data-md-color-scheme="slate" data-md-color-primary="deep-purple" data-md-color-accent="cyan" aria-hidden="true" type="radio" name="__palette" id="__palette_0">
</form>
<script>var palette=__md_get("__palette");if(palette&&palette.color){if("(prefers-color-scheme)"===palette.color.media){var media=matchMedia("(prefers-color-scheme: light)"),input=document.querySelector(media.matches?"[data-md-color-media='(prefers-color-scheme: light)']":"[data-md-color-media='(prefers-color-scheme: dark)']");palette.color.media=input.getAttribute("data-md-color-media"),palette.color.scheme=input.getAttribute("data-md-color-scheme"),palette.color.primary=input.getAttribute("data-md-color-primary"),palette.color.accent=input.getAttribute("data-md-color-accent")}for(var[key,value]of Object.entries(palette.color))document.body.setAttribute("data-md-color-"+key,value)}</script>
<label class="md-header__button md-icon" for="__search">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M9.5 3A6.5 6.5 0 0 1 16 9.5c0 1.61-.59 3.09-1.56 4.23l.27.27h.79l5 5-1.5 1.5-5-5v-.79l-.27-.27A6.52 6.52 0 0 1 9.5 16 6.5 6.5 0 0 1 3 9.5 6.5 6.5 0 0 1 9.5 3m0 2C7 5 5 7 5 9.5S7 14 9.5 14 14 12 14 9.5 12 5 9.5 5"/></svg>
</label>
<div class="md-search" data-md-component="search" role="dialog">
<label class="md-search__overlay" for="__search"></label>
<div class="md-search__inner" role="search">
<form class="md-search__form" name="search">
<input type="text" class="md-search__input" name="query" aria-label="Search" placeholder="Search" autocapitalize="off" autocorrect="off" autocomplete="off" spellcheck="false" data-md-component="search-query" required>
<label class="md-search__icon md-icon" for="__search">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M9.5 3A6.5 6.5 0 0 1 16 9.5c0 1.61-.59 3.09-1.56 4.23l.27.27h.79l5 5-1.5 1.5-5-5v-.79l-.27-.27A6.52 6.52 0 0 1 9.5 16 6.5 6.5 0 0 1 3 9.5 6.5 6.5 0 0 1 9.5 3m0 2C7 5 5 7 5 9.5S7 14 9.5 14 14 12 14 9.5 12 5 9.5 5"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M20 11v2H8l5.5 5.5-1.42 1.42L4.16 12l7.92-7.92L13.5 5.5 8 11z"/></svg>
</label>
<nav class="md-search__options" aria-label="Search">
<a href="javascript:void(0)" class="md-search__icon md-icon" title="Share" aria-label="Share" data-clipboard data-clipboard-text="" data-md-component="search-share" tabindex="-1">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7c.05-.23.09-.46.09-.7s-.04-.47-.09-.7l7.05-4.11c.54.5 1.25.81 2.04.81a3 3 0 0 0 3-3 3 3 0 0 0-3-3 3 3 0 0 0-3 3c0 .24.04.47.09.7L8.04 9.81C7.5 9.31 6.79 9 6 9a3 3 0 0 0-3 3 3 3 0 0 0 3 3c.79 0 1.5-.31 2.04-.81l7.12 4.15c-.05.21-.08.43-.08.66 0 1.61 1.31 2.91 2.92 2.91s2.92-1.3 2.92-2.91A2.92 2.92 0 0 0 18 16.08"/></svg>
</a>
<button type="reset" class="md-search__icon md-icon" title="Clear" aria-label="Clear" tabindex="-1">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>
</button>
</nav>
<div class="md-search__suggest" data-md-component="search-suggest"></div>
</form>
<div class="md-search__output">
<div class="md-search__scrollwrap" tabindex="0" data-md-scrollfix>
<div class="md-search-result" data-md-component="search-result">
<div class="md-search-result__meta">
Initializing search
</div>
<ol class="md-search-result__list" role="presentation"></ol>
</div>
</div>
</div>
</div>
</div>
</nav>
</header>
<div class="md-container" data-md-component="container">
<nav class="md-tabs" aria-label="Tabs" data-md-component="tabs">
<div class="md-grid">
<ul class="md-tabs__list">
<li class="md-tabs__item">
<a href="../.." class="md-tabs__link">
Home
</a>
</li>
<li class="md-tabs__item">
<a href="../../01_overview/" class="md-tabs__link">
Overview
</a>
</li>
<li class="md-tabs__item">
<a href="../../02_components/" class="md-tabs__link">
App Components
</a>
</li>
<li class="md-tabs__item">
<a href="../../03_start_an_app/" class="md-tabs__link">
Start an App
</a>
</li>
<li class="md-tabs__item md-tabs__item--active">
<a href="../01_overview/" class="md-tabs__link">
Reference App
</a>
</li>
<li class="md-tabs__item">
<a href="../../05_mcp_setup/" class="md-tabs__link">
MCP
</a>
</li>
<li class="md-tabs__item">
<a href="../../04_development/01_testing/" class="md-tabs__link">
Testing & Quality
</a>
</li>
<li class="md-tabs__item">
<a href="../../04_development/02_releasing/" class="md-tabs__link">
Releasing & Versioning
</a>
</li>
</ul>
</div>
</nav>
<main class="md-main" data-md-component="main">
<div class="md-main__inner md-grid">
<div class="md-sidebar md-sidebar--primary" data-md-component="sidebar" data-md-type="navigation" >
<div class="md-sidebar__scrollwrap">
<div class="md-sidebar__inner">
<nav class="md-nav md-nav--primary md-nav--lifted md-nav--integrated" aria-label="Navigation" data-md-level="0">
<label class="md-nav__title" for="__drawer">
<a href="../.." title="Aetos Standards — Wiki" class="md-nav__button md-logo" aria-label="Aetos Standards — Wiki" data-md-component="logo">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 8a3 3 0 0 0 3-3 3 3 0 0 0-3-3 3 3 0 0 0-3 3 3 3 0 0 0 3 3m0 3.54C9.64 9.35 6.5 8 3 8v11c3.5 0 6.64 1.35 9 3.54 2.36-2.19 5.5-3.54 9-3.54V8c-3.5 0-6.64 1.35-9 3.54"/></svg>
</a>
Aetos Standards — Wiki
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../.." class="md-nav__link">
<span class="md-ellipsis">
Home
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../01_overview/" class="md-nav__link">
<span class="md-ellipsis">
Overview
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../02_components/" class="md-nav__link">
<span class="md-ellipsis">
App Components
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../03_start_an_app/" class="md-nav__link">
<span class="md-ellipsis">
Start an App
</span>
</a>
</li>
<li class="md-nav__item md-nav__item--active md-nav__item--section md-nav__item--nested">
<input class="md-nav__toggle md-toggle " type="checkbox" id="__nav_5" checked>
<label class="md-nav__link" for="__nav_5" id="__nav_5_label" tabindex="">
<span class="md-ellipsis">
Reference App
</span>
<span class="md-nav__icon md-icon"></span>
</label>
<nav class="md-nav" data-md-level="1" aria-labelledby="__nav_5_label" aria-expanded="true">
<label class="md-nav__title" for="__nav_5">
<span class="md-nav__icon md-icon"></span>
Reference App
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../01_overview/" class="md-nav__link">
<span class="md-ellipsis">
Overview
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../02_domain_and_api/" class="md-nav__link">
<span class="md-ellipsis">
Domain & API
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../03_pipelines_and_worker/" class="md-nav__link">
<span class="md-ellipsis">
Pipelines & Worker
</span>
</a>
</li>
<li class="md-nav__item md-nav__item--active">
<input class="md-nav__toggle md-toggle" type="checkbox" id="__toc">
<label class="md-nav__link md-nav__link--active" for="__toc">
<span class="md-ellipsis">
Client Admin
</span>
<span class="md-nav__icon md-icon"></span>
</label>
<a href="./" class="md-nav__link md-nav__link--active">
<span class="md-ellipsis">
Client Admin
</span>
</a>
<nav class="md-nav md-nav--secondary" aria-label="Table of contents">
<label class="md-nav__title" for="__toc">
<span class="md-nav__icon md-icon"></span>
Table of contents
</label>
<ul class="md-nav__list" data-md-component="toc" data-md-scrollfix>
<li class="md-nav__item">
<a href="#what-ships" class="md-nav__link">
<span class="md-ellipsis">
What ships
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#pair-it-with-the-sample-server" class="md-nav__link">
<span class="md-ellipsis">
Pair it with the sample server
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#where-the-x-extensions-come-from" class="md-nav__link">
<span class="md-ellipsis">
Where the x- extensions come from
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#ci" class="md-nav__link">
<span class="md-ellipsis">
CI
</span>
</a>
</li>
</ul>
</nav>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--nested">
<input class="md-nav__toggle md-toggle md-toggle--indeterminate" type="checkbox" id="__nav_6" >
<label class="md-nav__link" for="__nav_6" id="__nav_6_label" tabindex="0">
<span class="md-ellipsis">
MCP
</span>
<span class="md-nav__icon md-icon"></span>
</label>
<nav class="md-nav" data-md-level="1" aria-labelledby="__nav_6_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_6">
<span class="md-nav__icon md-icon"></span>
MCP
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../../05_mcp_setup/" class="md-nav__link">
<span class="md-ellipsis">
Setup
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="../../04_development/01_testing/" class="md-nav__link">
<span class="md-ellipsis">
Testing & Quality
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../04_development/02_releasing/" class="md-nav__link">
<span class="md-ellipsis">
Releasing & Versioning
</span>
</a>
</li>
</ul>
</nav>
</div>
</div>
</div>
<div class="md-content" data-md-component="content">
<article class="md-content__inner md-typeset">
<h1 id="client-admin-a-the-spec-driven-frontend">Client Admin — the spec-driven frontend</h1>
<p>The client sample (<code>standards/src/templates/client/</code>) renders a full admin —
auth pages, resource lists, forms, detail views — <strong>from the server's
<code>openapi.yaml</code></strong>, driven entirely by the x- extension fields. There are no
hand-written screens per resource.</p>
<h2 id="what-ships">What ships</h2>
<p>A Vite + React + MUI app wiring the two Aetos packages:</p>
<ul>
<li><strong><code>@aetoskia/react-openapi</code></strong> — fetches <code>${VITE_API_BASE_URL}/openapi.json</code>,
builds the router and the resource admin from the spec's schemas and x-
fields (list columns, autocomplete, fk pickers, enum selects, hidden fields…)</li>
<li><strong><code>@aetoskia/react-auth</code></strong> — login/register/logout against the auth-server
paths declared in the spec's <code>securitySchemes.bearerAuth</code>, and attaches the
JWT to every API call</li>
<li>a shared MUI theme (<code>src/shared-theme/</code>) and the shell
(<code>Header</code> / <code>Footer</code> / <code>Home</code>)</li>
</ul>
<p><code>src/openapi-config.ts</code> is the only configuration:</p>
<div class="language-ts highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal"><a href="#__codelineno-0-1">1</a></span>
<span class="normal"><a href="#__codelineno-0-2">2</a></span>
<span class="normal"><a href="#__codelineno-0-3">3</a></span>
<span class="normal"><a href="#__codelineno-0-4">4</a></span>
<span class="normal"><a href="#__codelineno-0-5">5</a></span>
<span class="normal"><a href="#__codelineno-0-6">6</a></span></pre></div></td><td class="code"><div><pre><span></span><code><span id="__span-0-1"><a id="__codelineno-0-1" name="__codelineno-0-1"></a><span class="k">export</span><span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">specConfiguration</span><span class="o">:</span><span class="w"> </span><span class="kt">SpecConfiguration</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-0-2"><a id="__codelineno-0-2" name="__codelineno-0-2"></a><span class="w"> </span><span class="nx">specUrl</span><span class="o">:</span><span class="w"> </span><span class="sb">`</span><span class="si">${</span><span class="nx">apiBase</span><span class="si">}</span><span class="sb">/openapi.json`</span><span class="p">,</span>
</span><span id="__span-0-3"><a id="__codelineno-0-3" name="__codelineno-0-3"></a><span class="w"> </span><span class="nx">baseApiUrl</span><span class="o">:</span><span class="w"> </span><span class="kt">apiBase</span><span class="p">,</span>
</span><span id="__span-0-4"><a id="__codelineno-0-4" name="__codelineno-0-4"></a><span class="w"> </span><span class="nx">title</span><span class="o">:</span><span class="w"> </span><span class="s2">&quot;Vet Admin&quot;</span><span class="p">,</span>
</span><span id="__span-0-5"><a id="__codelineno-0-5" name="__codelineno-0-5"></a><span class="w"> </span><span class="nx">getToken</span><span class="o">:</span><span class="w"> </span><span class="p">()</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="nx">tokenStore</span><span class="p">.</span><span class="nx">get</span><span class="p">(),</span>
</span><span id="__span-0-6"><a id="__codelineno-0-6" name="__codelineno-0-6"></a><span class="p">};</span>
</span></code></pre></div></td></tr></table></div>
<h2 id="pair-it-with-the-sample-server">Pair it with the sample server</h2>
<div class="language-bash highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal"><a href="#__codelineno-1-1">1</a></span>
<span class="normal"><a href="#__codelineno-1-2">2</a></span>
<span class="normal"><a href="#__codelineno-1-3">3</a></span>
<span class="normal"><a href="#__codelineno-1-4">4</a></span></pre></div></td><td class="code"><div><pre><span></span><code><span id="__span-1-1"><a id="__codelineno-1-1" name="__codelineno-1-1"></a><span class="nb">cd</span><span class="w"> </span>apps/vet-sample-client
</span><span id="__span-1-2"><a id="__codelineno-1-2" name="__codelineno-1-2"></a><span class="nb">printf</span><span class="w"> </span><span class="s1">&#39;VITE_API_BASE_URL=http://localhost:8020\nVITE_AUTH_BASE_URL=http://localhost:8020\n&#39;</span><span class="w"> </span>&gt;<span class="w"> </span>.env.local
</span><span id="__span-1-3"><a id="__codelineno-1-3" name="__codelineno-1-3"></a>npm<span class="w"> </span>install
</span><span id="__span-1-4"><a id="__codelineno-1-4" name="__codelineno-1-4"></a>npm<span class="w"> </span>run<span class="w"> </span>dev
</span></code></pre></div></td></tr></table></div>
<p>Open the dev-server URL; the admin lists Parents, Pets, Treatments, Vets,
Appointments and Jobs straight from the vet spec. Edit <code>openapi.yaml</code> on the
server and the client follows on reload — the spec is the contract on both
sides.</p>
<h2 id="where-the-x-extensions-come-from">Where the x- extensions come from</h2>
<p>The canonical catalog lives in the react-openapi package
(<code>spec-extensions.ts</code>, served to agents as the <code>specExtensions</code> module of the
react-openapi MCP bundle). The worked examples on the server side are the
<code>Job</code> schema (enum selects via <code>x-edit-as: select</code> + <code>x-ui-type: enum</code>,
<code>x-hidden</code> field masks, <code>x-list-columns</code>) and the FK fields on <code>Appointment</code>
(<code>x-fk: {resource: pets, prefetch: true}</code>).</p>
<h2 id="ci">CI</h2>
<p>The client ships a <code>drone-npm</code> shape (Type B in
<a href="../../../lib/90_drone_pipelines/">Drone Pipelines</a>) and follows the
<a href="../../../lib/91_versioning/">Versioning</a> standard (<code>package.json</code> +
<code>CHANGELOG.md</code>).</p>
</article>
</div>
<script>var tabs=__md_get("__tabs");if(Array.isArray(tabs))e:for(var set of document.querySelectorAll(".tabbed-set")){var labels=set.querySelector(".tabbed-labels");for(var tab of tabs)for(var label of labels.getElementsByTagName("label"))if(label.innerText.trim()===tab){var input=document.getElementById(label.htmlFor);input.checked=!0;continue e}}</script>
<script>var target=document.getElementById(location.hash.slice(1));target&&target.name&&(target.checked=target.name.startsWith("__tabbed_"))</script>
</div>
<button type="button" class="md-top md-icon" data-md-component="top" hidden>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M13 20h-2V8l-5.5 5.5-1.42-1.42L12 4.16l7.92 7.92-1.42 1.42L13 8z"/></svg>
Back to top
</button>
</main>
<footer class="md-footer">
<div class="md-footer-meta md-typeset">
<div class="md-footer-meta__inner md-grid">
<div class="md-copyright">
Made with
<a href="https://squidfunk.github.io/mkdocs-material/" target="_blank" rel="noopener">
Material for MkDocs
</a>
</div>
</div>
</div>
</footer>
</div>
<div class="md-dialog" data-md-component="dialog">
<div class="md-dialog__inner md-typeset"></div>
</div>
<script id="__config" type="application/json">{"base": "../..", "features": ["navigation.sections", "navigation.expand", "navigation.top", "navigation.instant", "navigation.tracking", "navigation.indexes", "navigation.tabs", "toc.integrate", "header.autohide", "content.code.copy", "content.code.annotate", "content.tabs.link", "content.action.edit", "search.highlight", "search.share", "search.suggest"], "search": "../../assets/javascripts/workers/search.973d3a69.min.js", "tags": null, "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}, "version": null}</script>
<script src="../../assets/javascripts/bundle.f55a23d4.min.js"></script>
<script src="https://unpkg.com/mermaid@10.9.1/dist/mermaid.min.js"></script>
</body>
</html>

View File

@@ -268,6 +268,28 @@
<li class="md-tabs__item">
<a href="/06_reference_app/01_overview/" class="md-tabs__link">
Reference App
</a>
</li>
<li class="md-tabs__item">
<a href="/05_mcp_setup/" class="md-tabs__link">
@@ -455,6 +477,12 @@
@@ -478,7 +506,7 @@
<span class="md-ellipsis">
MCP
Reference App
</span>
@@ -488,6 +516,154 @@
<nav class="md-nav" data-md-level="1" aria-labelledby="__nav_5_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_5">
<span class="md-nav__icon md-icon"></span>
Reference App
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="/06_reference_app/01_overview/" class="md-nav__link">
<span class="md-ellipsis">
Overview
</span>
</a>
</li>
<li class="md-nav__item">
<a href="/06_reference_app/02_domain_and_api/" class="md-nav__link">
<span class="md-ellipsis">
Domain & API
</span>
</a>
</li>
<li class="md-nav__item">
<a href="/06_reference_app/03_pipelines_and_worker/" class="md-nav__link">
<span class="md-ellipsis">
Pipelines & Worker
</span>
</a>
</li>
<li class="md-nav__item">
<a href="/06_reference_app/04_client_admin/" class="md-nav__link">
<span class="md-ellipsis">
Client Admin
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--nested">
<input class="md-nav__toggle md-toggle md-toggle--indeterminate" type="checkbox" id="__nav_6" >
<label class="md-nav__link" for="__nav_6" id="__nav_6_label" tabindex="0">
<span class="md-ellipsis">
MCP
</span>
<span class="md-nav__icon md-icon"></span>
</label>
<nav class="md-nav" data-md-level="1" aria-labelledby="__nav_6_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_6">
<span class="md-nav__icon md-icon"></span>
MCP
</label>

View File

@@ -277,6 +277,28 @@
<li class="md-tabs__item">
<a href="06_reference_app/01_overview/" class="md-tabs__link">
Reference App
</a>
</li>
<li class="md-tabs__item">
<a href="05_mcp_setup/" class="md-tabs__link">
@@ -533,6 +555,12 @@
@@ -556,7 +584,7 @@
<span class="md-ellipsis">
MCP
Reference App
</span>
@@ -566,6 +594,154 @@
<nav class="md-nav" data-md-level="1" aria-labelledby="__nav_5_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_5">
<span class="md-nav__icon md-icon"></span>
Reference App
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="06_reference_app/01_overview/" class="md-nav__link">
<span class="md-ellipsis">
Overview
</span>
</a>
</li>
<li class="md-nav__item">
<a href="06_reference_app/02_domain_and_api/" class="md-nav__link">
<span class="md-ellipsis">
Domain & API
</span>
</a>
</li>
<li class="md-nav__item">
<a href="06_reference_app/03_pipelines_and_worker/" class="md-nav__link">
<span class="md-ellipsis">
Pipelines & Worker
</span>
</a>
</li>
<li class="md-nav__item">
<a href="06_reference_app/04_client_admin/" class="md-nav__link">
<span class="md-ellipsis">
Client Admin
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--nested">
<input class="md-nav__toggle md-toggle md-toggle--indeterminate" type="checkbox" id="__nav_6" >
<label class="md-nav__link" for="__nav_6" id="__nav_6_label" tabindex="0">
<span class="md-ellipsis">
MCP
</span>
<span class="md-nav__icon md-icon"></span>
</label>
<nav class="md-nav" data-md-level="1" aria-labelledby="__nav_6_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_6">
<span class="md-nav__icon md-icon"></span>
MCP
</label>
@@ -712,6 +888,10 @@ Query <code>find_library</code> before writing any infrastructure.</p>
<td>The entry point: gather the two axes, then build</td>
</tr>
<tr>
<td><a href="06_reference_app/01_overview/">Reference App</a></td>
<td>The runnable vet sample — server, pipelines, client</td>
</tr>
<tr>
<td><a href="04_development/01_testing/">Testing &amp; Quality</a></td>
<td>Test layout and acceptance gates</td>
</tr>
@@ -738,7 +918,9 @@ Registry</a>).</p>
<code>docs://nav</code>, <code>docs://modules/{module}</code>; call <code>find_library(capability)</code></li>
<li><strong>Library registry</strong> — the catalog of internal libraries behind every app
(<code>libraries.json</code>, served as <code>registry://libraries</code>)</li>
<li><strong>Template</strong> — the runnable skeleton at <code>standards/src/templates/app/</code></li>
<li><strong>Template</strong> — the runnable sample at <code>standards/src/templates/app/</code>
(server) and <code>standards/src/templates/client/</code> (React admin); walkthrough in
the <a href="06_reference_app/01_overview/">Reference App</a></li>
<li><strong>Start a build</strong> — invoke the <code>start_app</code> prompt on the <code>standards</code> server,
or read the <a href="03_start_an_app/">Start an App</a> guide</li>
</ul>

File diff suppressed because one or more lines are too long