docs(standards): mirror doc-forge wiki style; mermaid + copyable build prompt

- move templates into src/templates; renumber wiki pages; add app-components page

- embed agentic build prompt as a copyable markdown block; add mermaid diagrams

- build.py writes <name>/mcp; config static bundle -> standards/mcp

- registry reads libraries.json as bytes (committed file was UTF-16LE); normalize to UTF-8

- Dockerfile: drop stale blog copy; per-repo <name>/mcp copies

- drop nested standards/mcp/standards duplicate and empty root mcp/
This commit is contained in:
2026-10-12 00:21:41 +05:30
parent 9929f05334
commit 06ff5bce6e
80 changed files with 2009 additions and 829 deletions

View File

@@ -8,7 +8,7 @@ is a defect.
Each library also exposes its own documentation as an MCP server. An AI agent
can read a library's full API by connecting to its MCP and reading
`docs://index`, `docs://nav`, and `docs://modules/{module}`.
`doc://index`, `doc://nav`, and `doc://modules/{module}`.
---

View File

@@ -29,7 +29,11 @@ theme:
- search.suggest
markdown_extensions:
- pymdownx.superfences
- pymdownx.superfences:
custom_fences:
- name: mermaid
class: mermaid
format: !!python/name:pymdownx.superfences.fence_code_format
- pymdownx.inlinehilite
- pymdownx.snippets
- admonition
@@ -52,8 +56,12 @@ markdown_extensions:
plugins:
- search
extra_javascript:
- https://unpkg.com/mermaid@10.9.1/dist/mermaid.min.js
nav:
- Home: index.md
- Overview: 01_overview.md
- Testing: 10_testing.md
- Build Recipe: 11_build_recipe.md
- App Components: 02_components.md
- Build Recipe: 03_build_recipe.md
- Testing & Quality: 04_development/01_testing.md

View File

@@ -0,0 +1,54 @@
# Application Template
A minimal, runnable skeleton of the standardized architecture. It demonstrates
both axes of the blueprint (see `standards`):
1. **Resource axis** — items and jobs, each a Claude-style generic CRUD slice
(model → document → repo → service → route), all driven from `openapi.yaml`.
2. **Pipeline axis** — a background `worker.py` that polls pending jobs and runs
`pipelines/item_sync.py`, a deterministic async step chain.
Everything here is deliberately small so an agent can rewrite each file rather
than delete unused code. The domain is a neutral "item", not an app noun.
## Instantiate a new app
cp -r src/templates/app apps/<your-app>
cd apps/<your-app>
1. `pyproject.toml` — rename the package; keep the internal library pins.
2. `config.py` — replace/add settings consumed by your domain.
3. `openapi.yaml` — replace the `Item`/`Job` resources with your resources.
4. `domain/` — rename `item` and `job` slices to your aggregates.
5. `pipelines/item_sync.py` — replace the demo steps with your pipeline.
6. `tests/` — keep the two test files as the pattern for your own.
Run the API:
pip install -e ".[dev]"
uvicorn main:app --reload
Run the worker:
python worker.py
## What to keep, what to replace
| Keep | Replace / rename |
|------------------------------|---------------------------------------|
| `config.py` shape | Settings fields |
| `init.py` wiring pattern | Resource names |
| openapi-first routing | Paths, schemas, operationIds |
| mongo-ops repo factory | Collections, doc models |
| generic CRUD adaptor | Nothing (reuse as-is) |
| worker poll + status loop | Job payloads, pipeline steps |
## Where the internal libraries slot in
- `openapi_first.app.OpenAPIFirstApp` — main.py, interfaces/openapi.py
- `mongo_ops` — infra/db/setup.py, infra/db/repos/mongo.py, domain/*/ports/db.py
- `hexa.build` — domain/*/ports/service.py
- `dagpipe` — pipelines/ (wrap step chains in a DAG once they branch)
Confirm current endpoints and ownership with the `standards` registry MCP tool:
`find_library("pipeline")`, `find_library("auth")`, etc.

View File

@@ -0,0 +1,8 @@
.venv/
__pycache__/
*.pyc
.env
.git/
docs/
dist/
build/

View File

@@ -0,0 +1,5 @@
DEBUG=false
MONGO_URI=mongodb://localhost:27017
MONGO_DB=app_template
WORKER_POLL_SECONDS=30
WORKER_MAX_RETRIES=3

View File

@@ -0,0 +1,15 @@
FROM python:3.11-slim
WORKDIR /app
COPY pyproject.toml ./
COPY . .
RUN pip install --no-cache-dir --upgrade pip \
&& pip install --no-cache-dir .
EXPOSE 8000
# Two processes, per the two axes:
# - main.py : the spec-first FastAPI application
# - worker.py : the pipeline consumer polling pending jobs
CMD ["sh", "-c", "uvicorn main:app --host 0.0.0.0 --port 8000 & exec python worker.py"]

View File

@@ -0,0 +1,36 @@
"""# Summary
App configuration loaded from environment — a Settings dataclass with defaults
plus a singleton settings instance populated from OS environment variables at
import time.
"""
import os
from dataclasses import dataclass
@dataclass
class Settings:
"""Runtime configuration settings for the application.
Attributes:
debug (bool): Enable debug behavior. Defaults to False.
mongo_uri (str): MongoDB connection URI. Defaults to mongodb://localhost:27017.
mongo_db (str): MongoDB database name. Defaults to app_template.
worker_poll_seconds (int): Worker poll interval in seconds. Defaults to 30.
worker_max_retries (int): Maximum job retries before skipping. Defaults to 3.
"""
debug: bool = False
mongo_uri: str = "mongodb://localhost:27017"
mongo_db: str = "app_template"
worker_poll_seconds: int = 30
worker_max_retries: int = 3
settings = Settings(
debug=os.environ.get("DEBUG", "").lower() in ("1", "true", "yes"),
mongo_uri=os.environ.get("MONGO_URI", "mongodb://localhost:27017"),
mongo_db=os.environ.get("MONGO_DB", "app_template"),
worker_poll_seconds=int(os.environ.get("WORKER_POLL_SECONDS", "30")),
worker_max_retries=int(os.environ.get("WORKER_MAX_RETRIES", "3")),
)

View File

@@ -0,0 +1,21 @@
services:
api:
build: .
command: uvicorn main:app --host 0.0.0.0 --port 8000
env_file: .env
ports:
- "8000:8000"
depends_on:
- mongo
worker:
build: .
command: python worker.py
env_file: .env
depends_on:
- mongo
mongo:
image: mongo:7
ports:
- "27017:27017"

View File

@@ -0,0 +1,19 @@
"""# Summary
Item domain model — the neutral resource used throughout the template.
"""
from pydantic import BaseModel, Field
class Item(BaseModel):
"""A single countable resource.
Attributes:
name (str): Item name.
quantity (int): Item quantity. Defaults to 1.
"""
name: str
quantity: int = Field(default=1)
model_config = {"extra": "ignore"}

View File

@@ -0,0 +1,20 @@
"""# Summary
Mongo persistence for items: document schema and repository factory.
"""
from domain.item.models import Item
from infra.db.repos.mongo import _repo_factory as mongo_repo_factory
from mongo_ops import BaseDocument, BaseRepository
class ItemDocument(Item, BaseDocument):
"""Mongo document form of :class:`Item`."""
def create_item_repo() -> BaseRepository:
"""Create the Mongo-backed item repository.
Returns:
BaseRepository: Repository wired to the ``items`` collection.
"""
return mongo_repo_factory("items", ItemDocument)

View File

@@ -0,0 +1,33 @@
"""# Summary
CRUD service wiring for the item aggregate via the hexa container.
"""
from hexa import build
from domain.item.models import Item
from domain.item.ports.db import ItemDocument
from infra.adapters.service import CrudsServiceAdaptor
class ItemCrudsService(CrudsServiceAdaptor[Item, ItemDocument]):
"""Item CRUD service resolving :class:`ItemDocument` from a repo.
Notes:
Instances are created by :func:`get_item_service`. The ``model`` and
``schema`` class attributes bind the domain model and its document type.
"""
model = Item
schema = ItemDocument
def get_item_service(repo) -> ItemCrudsService:
"""Build an item CRUD service wired to ``repo``.
Args:
repo (Any): Mongo-backed item repository from :func:`create_item_repo`.
Returns:
ItemCrudsService: Ready-to-use item CRUD service.
"""
return build(ItemCrudsService, instances={"repo": repo})

View File

@@ -0,0 +1,26 @@
"""# Summary
Job domain model — a pipeline work item consumed by the background worker.
"""
from typing import Literal
from pydantic import BaseModel, Field
class Job(BaseModel):
"""A unit of background work.
Attributes:
name (str): Job name.
status (str): Lifecycle status. Defaults to "pending".
retry_count (int): Times the job failed. Defaults to 0.
error_message (str | None): Last failure message. Defaults to None.
payload (dict | None): Arbitrary job payload. Defaults to None.
"""
name: str
status: Literal["pending", "processing", "done", "failed"] = "pending"
retry_count: int = Field(default=0)
error_message: str | None = None
payload: dict | None = None
model_config = {"extra": "ignore"}

View File

@@ -0,0 +1,20 @@
"""# Summary
Mongo persistence for jobs: document schema and repository factory.
"""
from domain.job.models import Job
from infra.db.repos.mongo import _repo_factory as mongo_repo_factory
from mongo_ops import BaseDocument, BaseRepository
class JobDocument(Job, BaseDocument):
"""Mongo document form of :class:`Job`."""
def create_job_repo() -> BaseRepository:
"""Create the Mongo-backed job repository.
Returns:
BaseRepository: Repository wired to the ``jobs`` collection.
"""
return mongo_repo_factory("jobs", JobDocument)

View File

@@ -0,0 +1,33 @@
"""# Summary
CRUD service wiring for the job aggregate via the hexa container.
"""
from hexa import build
from domain.job.models import Job
from domain.job.ports.db import JobDocument
from infra.adapters.service import CrudsServiceAdaptor
class JobCrudsService(CrudsServiceAdaptor[Job, JobDocument]):
"""Job CRUD service resolving :class:`JobDocument` from a repo.
Notes:
Instances are created by :func:`get_job_service`. The ``model`` and
``schema`` class attributes bind the domain model and its document type.
"""
model = Job
schema = JobDocument
def get_job_service(repo) -> JobCrudsService:
"""Build a job CRUD service wired to ``repo``.
Args:
repo (Any): Mongo-backed job repository from :func:`create_job_repo`.
Returns:
JobCrudsService: Ready-to-use job CRUD service.
"""
return build(JobCrudsService, instances={"repo": repo})

View File

@@ -0,0 +1,128 @@
"""# Summary
Generic CRUD service adaptor over a Mongo repository.
Bridge pydantic models and mongo-ops documents, exposing create, read, update,
patch, delete, and search operations for every domain that needs them.
"""
from typing import Generic, Type, TypeVar
from infra.db.repos import mongo
from pydantic import BaseModel
from mongo_ops import BaseDocument
from mongo_ops.models import PyObjectId
Model = TypeVar("Model", bound=BaseModel)
Schema = TypeVar("Schema", bound=BaseDocument)
class CrudsServiceAdaptor(Generic[Model, Schema]):
"""Adapt a generic Mongo repository to a model/schema CRUD service.
Attributes:
repo (mongo.BaseRepository): Repository backing all CRUD operations.
model (Type[Model]): Pydantic model type handled by this service.
schema (Type[Schema]): Document schema type persisted by the repository.
"""
repo: mongo.BaseRepository
model: Type[Model]
schema: Type[Schema]
def __init__(self, *, repo=None):
"""Initialize the adaptor with a backing repository.
Args:
repo (mongo.BaseRepository): Repository used for all operations.
"""
self.repo = repo
@staticmethod
def model_to_dict(model_obj: Model) -> dict:
"""Serialize a model into a sparse dictionary.
Args:
model_obj (Model): Model instance to serialize.
Returns:
dict: Fields that are set and not ``None``.
"""
return model_obj.model_dump(exclude_unset=True, exclude_none=True)
async def create(self, data: Model | dict) -> Schema:
"""Create a document from a model or raw mapping.
Args:
data (Model | dict): Model instance or field mapping to persist.
Returns:
Schema: Persisted document.
"""
if isinstance(data, dict):
data = self.schema(**data)
return await self.repo.create(data)
async def read(self, id: PyObjectId) -> Schema | None:
"""Read a document by primary key.
Args:
id (PyObjectId): Document identifier.
Returns:
Schema | None: Matching document, or ``None`` when absent.
"""
return await self.repo.get_by_id(id)
async def update(self, id: PyObjectId, data: Model | dict) -> Schema | None:
"""Replace a document by primary key.
Args:
id (PyObjectId): Document identifier.
data (Model | dict): Replacement model or field mapping.
Returns:
Schema | None: Updated document, or ``None`` when absent.
"""
return await self.repo.update(
id,
data if isinstance(data, dict) else self.model_to_dict(data),
)
async def patch(self, id: PyObjectId, data: dict) -> Schema | None:
"""Partially update a document by primary key.
Args:
id (PyObjectId): Document identifier.
data (dict): Fields to patch.
Returns:
Schema | None: Updated document, or ``None`` when absent.
"""
return await self.repo.patch(id, data)
async def delete(self, id: PyObjectId) -> bool:
"""Delete a document by primary key.
Args:
id (PyObjectId): Document identifier.
Returns:
bool: ``True`` when a document was deleted.
"""
return await self.repo.delete(id)
async def search(self, filter: dict | None = None, *args, **kwargs) -> list[Schema]:
"""List documents matching an optional filter.
Args:
filter (dict | None): Query filter passed to the repository.
Returns:
list[Schema]: Matching documents.
Notes:
Extra positional and keyword arguments are forwarded to the
repository.
"""
return await self.repo.get_many(filter=filter, *args, **kwargs)

View File

@@ -0,0 +1,42 @@
"""# Summary
Generic MongoDB CRUD repo — single implementation for all domains.
Select a concrete mongo-ops repository (base, cached, or populating) from the
options supplied to the factory.
"""
from typing import List, Optional, Type
from mongo_ops import BaseRepository, CachedBaseRepository, PopulatingRepository
from mongo_ops.cache import CacheBackend, CacheConfig
from mongo_ops.models import BaseDocument
from mongo_ops.populate import PopulationEngine, PopulateRule
def _repo_factory(
collection: str,
db_model: Type[BaseDocument],
cache_backend: Optional[CacheBackend] = None,
cache_config: Optional[CacheConfig] = None,
engine: Optional[PopulationEngine] = None,
populate_rules: Optional[List[PopulateRule]] = None,
) -> BaseRepository:
"""Build the appropriate mongo-ops repository.
Args:
collection (str): Collection name to operate on.
db_model (Type[BaseDocument]): Document model bound to the collection.
cache_backend (Optional[CacheBackend]): Cache backend enabling cached reads.
cache_config (Optional[CacheConfig]): Cache configuration for the backend.
engine (Optional[PopulationEngine]): Population engine for reference expansion.
populate_rules (Optional[List[PopulateRule]]): Rules applied by the engine.
Returns:
BaseRepository: Repository instance matching the supplied options.
"""
if engine and populate_rules is not None:
return PopulatingRepository(collection, db_model, engine, populate_rules)
elif cache_backend is not None:
return CachedBaseRepository(collection, db_model, cache_backend, cache_config)
else:
return BaseRepository(collection, db_model)

View File

@@ -0,0 +1,23 @@
"""# Summary
Database setup — connection lifecycle only. No domain imports.
Connect and disconnect the shared Mongo connection manager at application
startup and shutdown.
"""
from mongo_ops import MongoConnectionManager
async def setup_database(uri: str, db_name: str):
"""Connect the shared Mongo connection manager.
Args:
uri (str): Mongo connection URI.
db_name (str): Target database name.
"""
await MongoConnectionManager.connect(uri=uri, db_name=db_name)
async def shutdown_database():
"""Disconnect the shared Mongo connection manager."""
await MongoConnectionManager.disconnect()

View File

@@ -0,0 +1,51 @@
"""# Summary
Infrastructure initialization — connects MongoDB and wires all repos and services.
Shared bootstrap for both the FastAPI app and the worker, guarded against
repeated initialization via a module-level flag.
"""
from typing import Any, Dict
from config import settings
from domain.item.ports.db import create_item_repo
from domain.item.ports.service import get_item_service
from domain.job.ports.db import create_job_repo
from domain.job.ports.service import get_job_service
from infra.db.setup import setup_database, shutdown_database
_resources: Dict[str, Any] = {}
_initialized: bool = False
async def init() -> Dict[str, Any]:
"""Connect MongoDB, wire all repos and services, and return the resources dict.
Idempotent: repeated calls short-circuit and return the cached resources.
Returns:
Dict[str, Any]: Named services keyed by resource name.
"""
global _resources, _initialized
if _initialized:
return _resources
await setup_database(uri=settings.mongo_uri, db_name=settings.mongo_db)
item_repo = create_item_repo()
job_repo = create_job_repo()
_resources["item_repo"] = item_repo
_resources["job_repo"] = job_repo
_resources["item_svc"] = get_item_service(item_repo)
_resources["job_svc"] = get_job_service(job_repo)
_initialized = True
return _resources
async def shutdown() -> None:
"""Disconnect MongoDB and reset the resources cache."""
global _initialized
await shutdown_database()
_resources.clear()
_initialized = False

View File

@@ -0,0 +1,42 @@
"""# Summary
Item route handlers — names must match openapi.yaml operationIds.
"""
from fastapi import Request
from fastapi.responses import JSONResponse
from domain.item.models import Item
def svc(request: Request):
"""Resolve the item CRUD service from app state."""
return request.app.state.db_resources["item_svc"]
async def create_item(request: Request, body: dict):
"""Create an item from the request body."""
data = body.model_dump() if hasattr(body, "model_dump") else dict(body)
doc = await svc(request).create(Item(**data))
return JSONResponse(status_code=201, content=doc.model_dump(mode="json", by_alias=True))
async def get_item(request: Request, id: str):
"""Fetch one item by id."""
doc = await svc(request).read(id)
if doc is None:
return JSONResponse(status_code=404, content={"detail": "item not found"})
return doc.model_dump(mode="json", by_alias=True)
async def list_items(request: Request):
"""List all items."""
docs = await svc(request).search({})
return [d.model_dump(mode="json", by_alias=True) for d in docs]
async def delete_item(request: Request, id: str):
"""Delete one item by id."""
ok = await svc(request).delete(id)
if not ok:
return JSONResponse(status_code=404, content={"detail": "item not found"})
return {"deleted": True}

View File

@@ -0,0 +1,34 @@
"""# Summary
Job route handlers — names must match openapi.yaml operationIds.
"""
from fastapi import Request
from fastapi.responses import JSONResponse
from domain.job.models import Job
def svc(request: Request):
"""Resolve the job CRUD service from app state."""
return request.app.state.db_resources["job_svc"]
async def create_job(request: Request, body: dict):
"""Create a job from the request body."""
data = body.model_dump() if hasattr(body, "model_dump") else dict(body)
doc = await svc(request).create(Job(**data))
return JSONResponse(status_code=201, content=doc.model_dump(mode="json", by_alias=True))
async def get_job(request: Request, id: str):
"""Fetch one job by id."""
doc = await svc(request).read(id)
if doc is None:
return JSONResponse(status_code=404, content={"detail": "job not found"})
return doc.model_dump(mode="json", by_alias=True)
async def list_jobs(request: Request):
"""List all jobs."""
docs = await svc(request).search({})
return [d.model_dump(mode="json", by_alias=True) for d in docs]

View File

@@ -0,0 +1,8 @@
"""# Summary
All handler functions — names must match openapi.yaml operationId values. The
OpenAPI-first router resolves each operation to a handler by name, so this module
re-exports every route handler from the subpackages into one flat namespace.
"""
from interfaces.item.routes import *
from interfaces.job.routes import *

View File

@@ -0,0 +1,54 @@
"""# Summary
FastAPI application bootstrap — uses OpenAPIFirstApp for spec-driven routing.
Handlers are looked up by operationId in interfaces.openapi, so no router tables
are declared here; the lifespan initializes and shuts down app resources.
"""
import logging
from contextlib import asynccontextmanager
from dotenv import load_dotenv
from openapi_first.app import OpenAPIFirstApp
from starlette.middleware.cors import CORSMiddleware
from init import init, shutdown
load_dotenv()
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(name)s] %(levelname)s: %(message)s",
)
import interfaces.openapi as openapi_routes
logger = logging.getLogger(__name__)
@asynccontextmanager
async def lifespan(app: OpenAPIFirstApp):
"""Initialize resource wiring on startup and shut it down on exit.
Args:
app (OpenAPIFirstApp): The ASGI application exposing app.state.
Yields:
None: The application runs while the context is open.
"""
resources = await init()
app.state.db_resources = resources
yield
await shutdown()
app = OpenAPIFirstApp(
openapi_path="openapi.yaml",
routes_module=openapi_routes,
title="Item API",
version="0.1.0",
lifespan=lifespan,
)
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])

View File

@@ -0,0 +1,161 @@
openapi: 3.0.3
info:
title: Item API
version: 0.1.0
paths:
/items:
get:
operationId: list_items
summary: List items
responses:
"200":
description: OK
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Item"
post:
operationId: create_item
summary: Create an item
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/ItemInput"
responses:
"201":
description: Created
content:
application/json:
schema:
$ref: "#/components/schemas/Item"
/items/{id}:
get:
operationId: get_item
summary: Get an item
parameters:
- name: id
in: path
required: true
schema:
type: string
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: "#/components/schemas/Item"
"404":
description: Not found
delete:
operationId: delete_item
summary: Delete an item
parameters:
- name: id
in: path
required: true
schema:
type: string
responses:
"200":
description: Deleted
"404":
description: Not found
/jobs:
get:
operationId: list_jobs
summary: List jobs
responses:
"200":
description: OK
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Job"
post:
operationId: create_job
summary: Create a job
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/JobInput"
responses:
"201":
description: Created
content:
application/json:
schema:
$ref: "#/components/schemas/Job"
/jobs/{id}:
get:
operationId: get_job
summary: Get a job
parameters:
- name: id
in: path
required: true
schema:
type: string
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: "#/components/schemas/Job"
"404":
description: Not found
components:
schemas:
Item:
type: object
required: [name, quantity]
properties:
id:
type: string
name:
type: string
quantity:
type: integer
ItemInput:
type: object
required: [name]
properties:
name:
type: string
quantity:
type: integer
default: 1
Job:
type: object
required: [name, status]
properties:
id:
type: string
name:
type: string
status:
type: string
enum: [pending, processing, done, failed]
retry_count:
type: integer
error_message:
type: string
payload:
type: object
JobInput:
type: object
required: [name]
properties:
name:
type: string
payload:
type: object

View File

@@ -0,0 +1,32 @@
"""# Summary
Item-sync pipeline — the demo pipeline axis of the blueprint.
Runs on the worker, consumes a pending job, and materializes its payload as an
item. Steps are plain async functions here; once the chain branches or needs
deterministic replay, wrap the steps in a dagpipe DAG (see standards
"06_pipeline_scaffold").
"""
import logging
logger = logging.getLogger(__name__)
async def run_item_sync(job, resources: dict) -> None:
"""Materialize a pending job payload as an item.
Step 1: extract the payload from the job.
Step 2: create the item through the item service.
Args:
job: The pending job document.
resources (dict): Wired services and adaptors.
"""
payload = job.payload or {}
item_svc = resources["item_svc"]
await item_svc.create({
"name": payload.get("name", job.name),
"quantity": payload.get("quantity", 1),
})
logger.info("Created item from job %s", str(job.id))

View File

@@ -0,0 +1,43 @@
[build-system]
requires = ["setuptools>=68"]
build-backend = "setuptools.build_meta"
[project]
name = "app-template"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = [
"fastapi",
"uvicorn[standard]",
"pydantic>=2",
"motor",
"python-dotenv",
"openapi-first",
"mongo-ops",
"hexa",
"dagpipe",
]
[project.optional-dependencies]
dev = [
"pytest",
"pytest-asyncio",
"httpx",
"ruff",
"doc-forge",
]
[tool.setuptools.packages.find]
where = ["."]
include = ["domain*", "infra*", "interfaces*", "pipelines*"]
[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]
[tool.ruff]
line-length = 100
target-version = "py311"
[tool.ruff.lint]
select = ["E", "F", "I", "UP"]

View File

@@ -0,0 +1,21 @@
import importlib
import interfaces.openapi as routes_module
from openapi_first.app import OpenAPIFirstApp
def test_openapi_app_builds():
"""The spec-first app must build and plant each route handler."""
module = importlib.import_module(routes_module.__name__)
for operation in ("create_item", "get_item", "list_items", "delete_item",
"create_job", "get_job", "list_jobs"):
assert hasattr(module, operation)
app = OpenAPIFirstApp(
openapi_path="openapi.yaml",
routes_module=module,
title="Item API",
version="0.1.0",
)
assert app is not None

View File

@@ -0,0 +1,22 @@
from domain.item.models import Item
from domain.job.models import Job
def test_item_defaults():
item = Item(name="widget")
assert item.quantity == 1
def test_job_defaults():
job = Job(name="sync")
assert job.status == "pending"
assert job.retry_count == 0
assert job.error_message is None
def test_job_rejects_unknown_status():
try:
Job(name="sync", status="exploded")
except Exception:
return
raise AssertionError("status should be rejected")

View File

@@ -0,0 +1,28 @@
from types import SimpleNamespace
from pipelines.item_sync import run_item_sync
class FakeItemSvc:
def __init__(self):
self.created = []
async def create(self, data: dict):
self.created.append(dict(data))
async def test_run_item_sync_creates_item():
svc = FakeItemSvc()
job = SimpleNamespace(id="j1", name="widget", payload={"quantity": 4})
await run_item_sync(job, {"item_svc": svc})
assert len(svc.created) == 1
assert svc.created[0] == {"name": "widget", "quantity": 4}
async def test_run_item_sync_uses_payload_name():
svc = FakeItemSvc()
job = SimpleNamespace(id="j1", name="ignored", payload={"name": "gadget"})
await run_item_sync(job, {"item_svc": svc})
assert svc.created[0]["name"] == "gadget"

View File

@@ -0,0 +1,67 @@
"""# Summary
Background worker — polls for pending jobs and runs the item-sync pipeline.
The poll loop is the pipeline axis of the blueprint: resources are created by
the API axis, workers consume them on a fixed cadence.
Notes:
- This process is separate from the API process. Any in-memory event bus is per
process — if the pipeline must surface live progress to the API, swap the bus
adaptor for a broker-backed one (see standards 07_worker_and_progress).
"""
import asyncio
import logging
from config import settings
from pipelines.item_sync import run_item_sync
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(name)s] %(levelname)s: %(message)s",
)
logger = logging.getLogger(__name__)
async def poll_jobs(resources: dict):
"""Poll for pending jobs and run the item-sync pipeline on each.
Args:
resources (dict): Wired services from :func:`init.init`.
"""
job_svc = resources["job_svc"]
logger.info("WORKER STARTED — poll=%ss max_retries=%d",
settings.worker_poll_seconds, settings.worker_max_retries)
while True:
pending = await job_svc.search(filter={"status": "pending"}, limit=10)
logger.info("Found %d pending job(s)", len(pending) if pending else 0)
for job in pending:
job_id = str(job.id)
if (job.retry_count or 0) >= settings.worker_max_retries:
logger.warning("SKIPPING job %s — retries exhausted", job_id)
continue
await job_svc.patch(job_id, {"status": "processing"})
try:
await run_item_sync(job, resources)
await job_svc.patch(job_id, {"status": "done", "error_message": ""})
except Exception as exc: # noqa: BLE001
logger.exception("job %s FAILED: %s", job_id, exc)
await job_svc.patch(job_id, {"status": "failed", "error_message": str(exc)})
await asyncio.sleep(settings.worker_poll_seconds)
if __name__ == "__main__":
from init import init, shutdown
async def _run():
resources = await init()
try:
await poll_jobs(resources)
finally:
await shutdown()
asyncio.run(_run())

View File

@@ -1,54 +1,99 @@
# Overview
## Summary
The blueprint is a **hexagonal (ports and adapters) application** with a
The blueprint is a **hexagonal (ports & adapters) application** with a
**spec-first** web edge and a **single composition root**. It favors explicit
wiring over implicit magic, and it prefers existing internal libraries over
hand-written infrastructure.
wiring over implicit magic and existing internal libraries over hand-written
infrastructure. Start here to build the mental model; the enforceable details
live in the reference pages.
---
## The three layers
## 🏗️ Architecture
```
interfaces/ ← HTTP edge: thin handlers bound to an OpenAPI operationId
domain/ ← business core: models, ports (interfaces), services, orchestrators
infra/ ← adapters: databases, caches, external clients, hardware
```mermaid
flowchart LR
subgraph EDGE["interfaces/ — HTTP edge"]
OAS["openapi.yaml<br/>operationId contract"]
RT["<resource>/routes.py<br/>thin handlers"]
end
subgraph CORE["domain/ — pure business core"]
MD["models"]
PT["ports (abstract)"]
SV["services"]
end
subgraph INFRA["infra/ — adapters"]
RP["mongo repos (mongo-ops)"]
AD["cache · stores · llm · mail"]
DB[("MongoDB")]
end
CR["init.py<br/>composition root"]
OAS --> RT
RT --> SV
SV --> PT
SV --> RP
PT -. depends on .- RP
CR --- RT
CR --- SV
CR --- RP
RP --> DB
AD --- CR
```
Plus two optional layers:
The rule holds at every edge: code in an outer ring may depend on a ring closer
to the core, never the reverse. `domain/` never imports a web framework or a
database driver.
```
pipelines/ ← data-driven transformation graphs (extraction, enrichment)
scripts/ ← operator scripts that reuse the composition root
```
> **Legend** — solid arrows are compile-time calls; dotted arrows are the
> port/adapter contract, resolved by the composition root at startup.
## Core principles
---
1. **One-way dependencies.** `interfaces -> domain -> infra` is allowed;
the reverse is forbidden. The domain never imports a framework it does not
own.
2. **Ports in the domain, adapters in infra.** A service depends on an
abstract port; the composition root injects a concrete adapter.
## 🔄 The one-way dependency rule
| Ring | May import | Never imports |
|-------------|------------------------------------|----------------------------------|
| `interfaces/` | `domain`, framework glue (glue only) | business logic, `infra` directly |
| `domain/` | nothing but its own ports | web frameworks, drivers, `infra` |
| `infra/` | `domain` ports, drivers, libraries | — |
| `init.py` | everything (the wiring point) | — |
---
## 🧭 Core principles
1. **One-way dependencies.** `interfaces -> domain -> infra` is allowed; the
reverse is forbidden.
2. **Ports in the domain, adapters in infra.** A service depends on an abstract
port; the composition root injects a concrete adapter.
3. **One composition root.** Nothing constructs its own dependencies. All
wiring lives in one module (`init.py`) and is exposed as a flat resource
registry.
4. **Spec-first.** The HTTP contract is declared in an OpenAPI document. Route
wiring lives in one module (`init.py`) exposed as a flat resource registry.
4. **Spec-first.** The HTTP contract is declared in `openapi.yaml`; route
handlers are looked up by `operationId`. There are no decorator routers.
5. **Reuse the libraries.** Pipeline orchestration, DI, Mongo repositories,
ingestion, auth, and docs are provided by internal libraries. Do not
reimplement them — see the library registry.
5. **Reuse the libraries.** Orchestration, DI, Mongo repositories, ingestion,
auth, and docs come from internal libraries — see the [Library
Registry](../lib/13_library_registry/).
6. **Explicit state machines.** Long-running work moves through explicit,
durable statuses and can pause for human input.
## Runtime processes
---
A standard app may run as up to three processes that share the same
composition root:
## ⚙️ Runtime processes
| Process | Entry point | Responsibility |
|---|---|---|
| API | `main.py` | serve HTTP; start/stop resources via lifespan |
| Worker | `worker.py` | poll durable jobs, run pipelines, publish progress |
| Scripts | `python -m scripts.<name>` | one-shot operator tasks |
A standard app may run as up to three processes that share the same composition
root:
| Process | Entry point | Responsibility |
|---------|------------------------|-------------------------------------------|
| API | `main.py` | serve HTTP; start/stop resources via lifespan |
| Worker | `worker.py` | poll durable jobs, run pipelines, publish progress |
| Scripts | `python -m scripts.<name>` | one-shot operator tasks |
---
## ➡️ Read Next
- [02 – App Components](02_components.md) · [03 – Build Recipe](03_build_recipe.md)

View File

@@ -0,0 +1,137 @@
# App Components
What ships in every standard app, and how the pieces relate. This is the
blueprint's component model — `templates/app/` is a runnable instance of it.
---
## 🗂️ Component model
```mermaid
flowchart LR
subgraph EDGE["interfaces/ — HTTP edge"]
OAS["openapi.yaml"]
RU["resource routes"]
JU["job routes"]
end
subgraph CORE["domain/ — business core"]
RI["<resource> · models · ports · services"]
JI["<job/worker> · models · ports · services"]
PI["pipelines/<family>"]
end
subgraph INFRA["infra/ — adapters"]
DB[("MongoDB repos")]
CA["redis cache"]
ST["stores · progress bus"]
LL["llm clients"]
end
W["worker.py"]
CR["init.py — composition root"]
OAS --> RU
OAS --> JU
RU --> RI
JU --> JI
JI --> PI
PI --> DB
PI --> LL
RI --> CA
RI --> DB
JI --> ST
W --> JI
W --> PI
CR === RU
CR === JU
CR === RI
CR === JI
CR === PI
CR === W
CR === DB
CR === CA
CR === ST
```
> **Legend** — solid arrows are direct calls; thick links are wires created by
> the composition root. Every component is injected, never self-constructed.
---
## 📦 Component roles
### 1. `interfaces/` — the edge
Thin handlers bound to `openapi.yaml` `operationId`s. They parse the request,
call a service, and shape the response. No business rules, no I/O.
- `interfaces/openapi.py` re-exports every route module so the OpenAPI-first
app can resolve handlers.
- Routes may mount the `cache` decorator on read endpoints.
### 2. `domain/` — the core
Pure models, **ports** (abstract contracts), and services (use cases). No
framework imports, no drivers.
- `<aggregate>/models.py` — dataclasses, enums, document shapes.
- `<aggregate>/ports/db.py` — repo contract; `<aggregate>/ports/service.py` —
service contract.
- Services compose ports and enforce the business rules; the composition root
injects the implementations.
### 3. `infra/` — the adapters
Concrete implementations of the domain ports plus cross-cutting infrastructure:
- `db/` — mongo-ops repositories and the connection lifecycle.
- `adapters/` — cache store, progress bus, storages, fetchers (library-backed).
- Anything else the domain needs, implemented against its ports.
### 4. `pipelines/` — data-driven families *(optional)*
Extraction and enrichment families, each a deterministic step chain
(`abc.py` / `impl.py` / `pipeline.yaml` / `resources/`). Roots are registered
in the pattern registry and driven either by a worker job or a CLI.
### 5. `worker.py` — the job worker *(optional)*
A separate process that polls durable job collections, runs pipeline roots, and
publishes progress to the progress bus. It shares the same composition root as
the API.
### 6. `init.py` — the composition root
The only place anything is constructed: leaf repositories → dependent
repositories → services → adapters. Exposes `get_svc` overloads and starts /
stops resources on the app lifespan.
---
## 🔗 How the request flows
```mermaid
sequenceDiagram
participant C as Client
participant R as route (interfaces)
participant S as service (domain)
participant P as repo (infra)
participant DB as MongoDB
C->>R: POST /items (operationId)
R->>S: svc.create(payload)
S->>P: repo.insert(doc)
P->>DB: insert
DB-->>P: ack
P-->>S: doc
S-->>R: result
R-->>C: 200 JSON
```
---
## Related
- [01 – Overview](01_overview.md) · [03 – Build Recipe](03_build_recipe.md) · [04 – Testing & Quality](04_development/01_testing.md)

View File

@@ -0,0 +1,124 @@
# Build Recipe
The ordered, deterministic checklist for generating a new application of the
standard shape. Every step is fixed; only steps 2 and 3 need app-specific
authoring. Give an AI agent the copyable prompt below (or read it from the
`standards` MCP bundle as `doc://modules/build_recipe`) and it can produce a
complete app in one pass.
---
## 🚀 Quick start
```bash
# Copy the runnable skeleton (from the standards docs checkout)
cp -r <docs>/standards/src/templates/app apps/<your-app>
cd apps/<your-app>
```
Then follow the numbered steps in the prompt.
---
## 📋 Agent prompt (copyable)
````markdown
# Build a new application from the Aetos Standards blueprint
You are coding a complete new backend application conforming to the Aetos
standard app blueprint. Execute every step in order. Only steps 2 and 3 use the
app-specific details provided by the user; everything else is deterministic.
Before writing any code, connect to the `standards` MCP server and read:
- `doc://nav` (the module list)
- `doc://modules/layering_rules` (the dependency rules)
- `doc://modules/genericity_matrix` (what is generic vs app-specific)
Then call `find_library` with capabilities like `db`, `di`, `routing`,
`ingestion`, `auth`, `docs`, `pipeline`, and note the libraries that already
exist. Never reimplement one.
1. **Scaffold** — copy `src/templates/app/` into `apps/<your-app>/`. Keep the
package tree: `domain/`, `infra/`, `interfaces/`, `pipelines/`, `tests/`,
`scripts/`, plus `pyproject.toml`, `openapi.yaml`, `config.py`, `init.py`,
`main.py`, `worker.py`, `Dockerfile`, `docker-compose.yaml`, `.env.example`.
2. **Author the spec (app-specific)** — write `openapi.yaml`: resources,
schemas, `operationId`s, security, and extension metadata. This is the
contract and drives all routing.
3. **Write each CRUD slice (app-specific)** — per resource create the
four-file slice: `models.py`, `ports/db.py`, `ports/service.py`, and
optional `converters.py`. Follow the CRUD template.
4. **Wire the composition root** — in `init.py`, build leaf repositories, then
dependent repositories, then services; add infra adapters (db, cache,
stores, progress bus, fetchers). Expose `get_svc` overloads.
5. **Add routes** — create `interfaces/<resource>/routes.py` thin handlers
bound to `operationId`s and re-export them in `interfaces/openapi.py`. Add
the cache decorator to read endpoints.
6. **Bootstrap app and worker** — confirm `main.py` runs the OpenAPI-first app
with its lifespan. If the app has long-running jobs, adapt `worker.py` to
poll the job collection, run the pipeline, and publish progress.
7. **Add pipeline families (only if data-driven)** — create
`pipelines/<family>/` with `abc.py`, `impl.py`, `pipeline.yaml`,
`resources/`, and register the root. Add fixtures and a parity test.
8. **Add infrastructure only where needed** — prefer a library-backed adapter.
Never hand-write what an internal library already provides.
9. **Tests** — integration tests with fakes at the HTTP edge; unit tests for
domain and pipeline logic; a config-consistency test for rule-driven
pipelines.
10. **Scripts and seeds** — operator scripts under `scripts/` using
`init()` / `get_svc()` / `shutdown()`.
11. **Docs** — author GSDFC docstrings, generate `.pyi` stubs, mark the package
`py.typed`, and build the docs.
**Definition of done:**
- `python -c "import main"` succeeds.
- `pytest` passes.
- The app serves every operation in the spec; the worker drains its queue.
- No domain module imports a web framework.
- No infrastructure was reimplemented that a library already provides.
````
---
## 🧭 Where the steps land
The steps map onto the layers as follows:
| Step | Package / file | Depends on |
|---------------------|-----------------------------------|---------------------------------------------|
| 1 · Scaffold | whole `templates/app/` | — |
| 2 · Spec | `openapi.yaml` | — |
| 3 · CRUD slices | `domain/<resource>/` | spec, CRUD template |
| 4 · Wiring | `init.py` | every slice |
| 5 · Routes | `interfaces/<resource>/` | services, openapi route registry |
| 6 · Bootstrap | `main.py`, `worker.py` | composition root |
| 7 · Pipelines | `pipelines/<family>/` | library pipeline engine |
| 8 · Infra | `infra/` | domain ports |
| 9 · Tests | `tests/` | app + fixtures |
| 10 · Scripts | `scripts/` | composition root |
| 11 · Docs | docstrings + docs build | code, Doc Forge |
---
## ⚠️ Definition of done
- `python -c "import main"` succeeds.
- `pytest` passes.
- The app serves the spec's operations; the worker drains its queue.
- No domain module imports a web framework.
- No infrastructure was reimplemented that a library already provides.
---
## Related
- [01 – Overview](01_overview.md) · [02 – App Components](02_components.md) · [04 – Testing & Quality](04_development/01_testing.md)

View File

@@ -1,13 +1,12 @@
# Testing
# Testing & Quality Gates
## Summary
Two test suites: **integration** (the HTTP edge with fakes) and **unit** (pure
domain and pipeline logic). Both run under pytest with asyncio auto mode.
Two suites run under pytest with asyncio auto mode: **integration** (the HTTP
edge against fakes) and **unit** (pure domain and pipeline logic). Finish a
blueprint app only when every gate below passes.
---
## Layout
## 🗂️ Layout
```
tests/
@@ -16,7 +15,9 @@ tests/
conftest.py # fixtures
```
## Integration tests
---
## 🚀 Integration tests
- Build the app with `TestClient`.
- Replace `app.state.resources` with fake repositories/services so no database
@@ -35,22 +36,46 @@ def test_create_item():
assert r.status_code == 200
```
## Unit tests
---
## 🧪 Unit tests
- Domain models and converters: pure, no I/O.
- Pipeline stages: feed a fixture state, assert the transformed state.
- Assembled pipeline roots: **fixture parity** — replay a stored fixture input
and compare against a golden output file.
## Config-consistency tests
---
## 🔐 Config-consistency tests
For rule-driven pipelines, add a test that cross-references rules against the
declared pattern registry: every flag emitted must be defined; every defined
flag must be reachable. This catches drift without running the pipeline.
## Conventions
---
## ✅ Quality gates
A **definition of done** for any standard app:
- `python -c "import main"` succeeds.
- `pytest` passes.
- The app serves the spec's operations; the worker drains its queue.
- No domain module imports a web framework.
- No infrastructure was reimplemented that a library already provides.
---
## 💡 Conventions
- `asyncio_mode = "auto"`; no manual event loops.
- Fixtures are checked into the repo (inputs + golden outputs).
- Tests skip (not fail) when a required fixture is absent.
- Do not mock the domain; mock only the infrastructure edge.
---
## Related
- [02 – App Components](02_components.md) · [03 – Build Recipe](03_build_recipe.md) · [Overview](../01_overview.md)

View File

@@ -1,82 +0,0 @@
# Build Recipe
## Summary
Follow this ordered checklist to generate a new application of the standard
shape. Every step is deterministic; only steps 2 and 7 require app-specific
authoring.
---
## 0. Query the registry
Before writing anything, read the **library registry** and note which internal
libraries provide DB, DI, routing, ingestion, auth, and docs. Do not
reimplement them.
## 1. Scaffold the repository
Copy the `templates/` skeleton. It provides: `pyproject.toml`, `openapi.yaml`,
`main.py`, `init.py`, `worker.py`, `config.py`, `Dockerfile`,
`docker-compose.yaml`, `.env.example`, and the package tree
(`domain/ infra/ interfaces/ pipelines/ tests/ scripts/`).
## 2. Author the spec *(app-specific)*
Write `openapi.yaml`: resources, schemas, `operationId`s, security, and any
extension metadata. This is the contract.
## 3. Write each CRUD slice *(app-specific)*
For each resource, create the four-file slice (see **CRUD Template**):
`models.py`, `ports/db.py`, `ports/service.py`, optional `converters.py`.
## 4. Wire the composition root
In `init.py`: build leaf repositories, then dependent repositories, then
services; add infrastructure adaptors (cache store, progress bus, stores,
fetchers). Add `get_svc` overloads.
## 5. Add routes
Create `interfaces/<resource>/routes.py` (thin handlers) and re-export them in
`interfaces/openapi.py`. Add a cache decorator to read endpoints.
## 6. Bootstrap the app and worker
Confirm `main.py` wires the OpenAPI-first app with the lifespan. If the app has
long-running jobs, adapt `worker.py` to poll the relevant job collection.
## 7. Add a pipeline family *(only if data-driven)*
Create `pipelines/<family>/` with `abc.py`, `impl.py`, `pipeline.yaml`,
`resources/`, and register the root in `registry.py`. Add fixtures and a parity
test.
## 8. Add infrastructure as needed
Only where the standard adaptors do not cover the need. Prefer library-backed
adapters.
## 9. Tests
Add integration tests with fakes and unit tests for domain/pipeline logic; add
a config-consistency test if using rule-driven pipelines.
## 10. Scripts and seeds
Add operator scripts under `scripts/` that use `init()` / `get_svc()` /
`shutdown()`.
## 11. Docs
Author GSDFC docstrings, generate the type stubs (`.pyi`), mark the package
`py.typed`, and build docs.
## Definition of done
- `python -c "import main"` succeeds.
- `pytest` passes.
- The app serves the spec's operations; the worker drains its queue.
- No domain module imports a web framework.
- No infrastructure was reimplemented that a library already provides.

View File

@@ -1,45 +1,68 @@
# The Aetos Standard App Blueprint
# 🧩 Standards — the Aetos App Blueprint
A single, reusable architecture for backend applications in the Aetos ecosystem.
One reusable architecture for every backend application in the Aetos ecosystem:
a spec-first HTTP edge, a pure domain core, a pluggable infrastructure layer,
and a single composition root.
Every application built with this blueprint has the same shape:
Fill in **two axes only** and a new app is done: your **resource model**, and
(if needed) your **pipeline family**. The blueprint itself never names a
business domain — expenses, health metrics, inventory, and analytics are all
the same shape underneath.
- a **spec-first HTTP layer** driven by an OpenAPI document;
- a **domain layer** of pure models, ports, and services;
- an **infrastructure layer** of adapters (MongoDB, cache, workers, LLM, mail);
- a **composition root** that wires everything in exactly one place;
- an optional **pipeline layer** for data-driven extraction/enrichment;
- an optional **background worker** that runs long-running jobs and reports progress.
> **Doc model:** this wiki is written for humans — how-to guides, the build
> recipe, and quality gates. The enforceable rules (layering, composition root,
> CRUD template, pipeline scaffold) and the library registry live in the
> **reference** pages, mirrored by the machine-readable `standards` MCP bundle.
> Query `find_library` before writing any infrastructure.
> This blueprints is deliberately **app-agnostic**. It never names a concrete
> business domain. Any application — expenses, health metrics, inventory,
> analytics — is produced by filling in **two axes only**: the resource model
> and (if needed) the pipeline family.
---
## How to read this
## 🚀 Key Features
| Page | What it covers |
|---|---|
| Overview | The philosophy and the one-way dependency rule |
| Layering Rules | Which package may import which |
| Composition Root | The single wiring point |
| Spec-First Routing | OpenAPI as the source of truth |
| CRUD Template | The canonical resource slice |
| Pipeline Scaffold | Data-driven processing families |
| Worker & Progress | Async jobs, status machine, streaming progress |
| Human-in-the-Loop | Pausing a job to ask a question |
| Infra Adapters | MongoDB, cache, stores, LLM, mail |
| Testing | Integration + unit conventions |
| Build Recipe | The ordered checklist to generate a new app |
| Genericity Matrix | What is reusable vs what you must write |
- 🧭 **Spec-first routing** — the OpenAPI document, not decorator routers, is
the source of truth (operationId lookup)
- 🏗️ **One-way layering** — `interfaces → domain → infra`, enforced
- 🔌 **Single composition root** — every dependency is wired in exactly one place
- 🧩 **Generic, not app-specific** — reusable rules versus two authoring axes
- 📚 **Library-backed** — DB, DI, pipelines, ingestion, auth, and docs come from
internal libraries, never hand-written
- 🚦 **Explicit state machines** — durable job statuses, progress, and
human-in-the-loop pauses
## For AI agents
---
This content is served as MCP resources under `docs://index`, `docs://nav`,
and `docs://modules/{module}`. To build a new app:
## 📁 Documentation Structure
1. Read `build_recipe` first.
2. Read `genericity_matrix` to know what you must author.
3. Query the **library registry** (`find_library`) before writing any
infrastructure yourself.
4. Copy the `templates/` skeleton and fill the two app-specific axes.
| Section | Description |
|-----------------------------------------------------|--------------------------------------------------------|
| [Overview](01_overview.md) | Philosophy, the one-way dependency rule, architecture |
| [App Components](02_components.md) | The component model and how the pieces wire together |
| [Build Recipe](03_build_recipe.md) | The ordered, agentic checklist for a new app |
| [Testing & Quality](04_development/01_testing.md) | Test layout and acceptance gates |
**Reference** (separate site, mirrors the MCP bundle): the enforceable
rules — [Layering](../lib/02_layering_rules/), [Composition
Root](../lib/03_composition_root/), [Spec-First
Routing](../lib/04_spec_first_routing/), [CRUD
Template](../lib/05_crud_template/), [Pipeline
Scaffold](../lib/06_pipeline_scaffold/), [Worker &
Progress](../lib/07_worker_and_progress/), [Human-in-the-Loop](../lib/08_human_in_loop/),
[Infra Adapters](../lib/09_infra_adaptors/), [Genericity
Matrix](../lib/12_genericity_matrix/), [Library
Registry](../lib/13_library_registry/).
---
## 🔗 Related Resources
- **MCP bundle** — `standards` server on `:8009`: read `doc://index`,
`doc://nav`, `doc://modules/{module}`; call `find_library(capability)`
- **Library registry** — the catalog of internal libraries behind every app
(`libraries.json`, served as `registry://libraries`)
- **Template** — the runnable skeleton at `standards/src/templates/app/`
- **Copy the repo's `AGENTS.md`-style build prompt** from
[Build Recipe](03_build_recipe.md)
---
© Aetoskia Internal — `standards`