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

@@ -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