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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
---
|
||||
|
||||
|
||||
70
standards/src/wiki/06_reference_app/01_overview.md
Normal file
70
standards/src/wiki/06_reference_app/01_overview.md
Normal 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
|
||||
73
standards/src/wiki/06_reference_app/02_domain_and_api.md
Normal file
73
standards/src/wiki/06_reference_app/02_domain_and_api.md
Normal 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).
|
||||
@@ -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.
|
||||
60
standards/src/wiki/06_reference_app/04_client_admin.md
Normal file
60
standards/src/wiki/06_reference_app/04_client_admin.md
Normal 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`).
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user