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:
@@ -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}`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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
|
||||
54
standards/src/templates/README.md
Normal file
54
standards/src/templates/README.md
Normal 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.
|
||||
8
standards/src/templates/app/.dockerignore
Normal file
8
standards/src/templates/app/.dockerignore
Normal file
@@ -0,0 +1,8 @@
|
||||
.venv/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.env
|
||||
.git/
|
||||
docs/
|
||||
dist/
|
||||
build/
|
||||
5
standards/src/templates/app/.env.example
Normal file
5
standards/src/templates/app/.env.example
Normal file
@@ -0,0 +1,5 @@
|
||||
DEBUG=false
|
||||
MONGO_URI=mongodb://localhost:27017
|
||||
MONGO_DB=app_template
|
||||
WORKER_POLL_SECONDS=30
|
||||
WORKER_MAX_RETRIES=3
|
||||
15
standards/src/templates/app/Dockerfile
Normal file
15
standards/src/templates/app/Dockerfile
Normal 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"]
|
||||
36
standards/src/templates/app/config.py
Normal file
36
standards/src/templates/app/config.py
Normal 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")),
|
||||
)
|
||||
21
standards/src/templates/app/docker-compose.yaml
Normal file
21
standards/src/templates/app/docker-compose.yaml
Normal 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"
|
||||
0
standards/src/templates/app/domain/__init__.py
Normal file
0
standards/src/templates/app/domain/__init__.py
Normal file
0
standards/src/templates/app/domain/item/__init__.py
Normal file
0
standards/src/templates/app/domain/item/__init__.py
Normal file
19
standards/src/templates/app/domain/item/models.py
Normal file
19
standards/src/templates/app/domain/item/models.py
Normal 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"}
|
||||
20
standards/src/templates/app/domain/item/ports/db.py
Normal file
20
standards/src/templates/app/domain/item/ports/db.py
Normal 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)
|
||||
33
standards/src/templates/app/domain/item/ports/service.py
Normal file
33
standards/src/templates/app/domain/item/ports/service.py
Normal 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})
|
||||
0
standards/src/templates/app/domain/job/__init__.py
Normal file
0
standards/src/templates/app/domain/job/__init__.py
Normal file
26
standards/src/templates/app/domain/job/models.py
Normal file
26
standards/src/templates/app/domain/job/models.py
Normal 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"}
|
||||
20
standards/src/templates/app/domain/job/ports/db.py
Normal file
20
standards/src/templates/app/domain/job/ports/db.py
Normal 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)
|
||||
33
standards/src/templates/app/domain/job/ports/service.py
Normal file
33
standards/src/templates/app/domain/job/ports/service.py
Normal 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})
|
||||
0
standards/src/templates/app/infra/__init__.py
Normal file
0
standards/src/templates/app/infra/__init__.py
Normal file
128
standards/src/templates/app/infra/adapters/service.py
Normal file
128
standards/src/templates/app/infra/adapters/service.py
Normal 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)
|
||||
0
standards/src/templates/app/infra/db/__init__.py
Normal file
0
standards/src/templates/app/infra/db/__init__.py
Normal file
42
standards/src/templates/app/infra/db/repos/mongo.py
Normal file
42
standards/src/templates/app/infra/db/repos/mongo.py
Normal 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)
|
||||
23
standards/src/templates/app/infra/db/setup.py
Normal file
23
standards/src/templates/app/infra/db/setup.py
Normal 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()
|
||||
51
standards/src/templates/app/init.py
Normal file
51
standards/src/templates/app/init.py
Normal 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
|
||||
0
standards/src/templates/app/interfaces/__init__.py
Normal file
0
standards/src/templates/app/interfaces/__init__.py
Normal file
42
standards/src/templates/app/interfaces/item/routes.py
Normal file
42
standards/src/templates/app/interfaces/item/routes.py
Normal 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}
|
||||
34
standards/src/templates/app/interfaces/job/routes.py
Normal file
34
standards/src/templates/app/interfaces/job/routes.py
Normal 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]
|
||||
8
standards/src/templates/app/interfaces/openapi.py
Normal file
8
standards/src/templates/app/interfaces/openapi.py
Normal 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 *
|
||||
54
standards/src/templates/app/main.py
Normal file
54
standards/src/templates/app/main.py
Normal 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=["*"])
|
||||
161
standards/src/templates/app/openapi.yaml
Normal file
161
standards/src/templates/app/openapi.yaml
Normal 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
|
||||
0
standards/src/templates/app/pipelines/__init__.py
Normal file
0
standards/src/templates/app/pipelines/__init__.py
Normal file
32
standards/src/templates/app/pipelines/item_sync.py
Normal file
32
standards/src/templates/app/pipelines/item_sync.py
Normal 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))
|
||||
43
standards/src/templates/app/pyproject.toml
Normal file
43
standards/src/templates/app/pyproject.toml
Normal 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"]
|
||||
0
standards/src/templates/app/tests/__init__.py
Normal file
0
standards/src/templates/app/tests/__init__.py
Normal 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
|
||||
0
standards/src/templates/app/tests/unit/__init__.py
Normal file
0
standards/src/templates/app/tests/unit/__init__.py
Normal file
22
standards/src/templates/app/tests/unit/test_item_models.py
Normal file
22
standards/src/templates/app/tests/unit/test_item_models.py
Normal 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")
|
||||
28
standards/src/templates/app/tests/unit/test_item_sync.py
Normal file
28
standards/src/templates/app/tests/unit/test_item_sync.py
Normal 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"
|
||||
67
standards/src/templates/app/worker.py
Normal file
67
standards/src/templates/app/worker.py
Normal 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())
|
||||
@@ -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)
|
||||
137
standards/src/wiki/02_components.md
Normal file
137
standards/src/wiki/02_components.md
Normal 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)
|
||||
124
standards/src/wiki/03_build_recipe.md
Normal file
124
standards/src/wiki/03_build_recipe.md
Normal 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)
|
||||
@@ -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)
|
||||
@@ -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.
|
||||
@@ -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`
|
||||
Reference in New Issue
Block a user