@eltonssouza/development-utility-kit 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/.claude/agents/README.md +24 -0
  2. package/.claude/agents/analyst.md +198 -0
  3. package/.claude/agents/backend-developer.md +126 -0
  4. package/.claude/agents/brain-keeper.md +229 -0
  5. package/.claude/agents/code-reviewer.md +181 -0
  6. package/.claude/agents/database-engineer.md +94 -0
  7. package/.claude/agents/devops-engineer.md +141 -0
  8. package/.claude/agents/frontend-developer.md +97 -0
  9. package/.claude/agents/gate-keeper.md +118 -0
  10. package/.claude/agents/migrator.md +291 -0
  11. package/.claude/agents/mobile-developer.md +80 -0
  12. package/.claude/agents/n8n-specialist.md +94 -0
  13. package/.claude/agents/product-owner.md +115 -0
  14. package/.claude/agents/qa-engineer.md +232 -0
  15. package/.claude/agents/release-engineer.md +204 -0
  16. package/.claude/agents/scaffold.md +87 -0
  17. package/.claude/agents/security-engineer.md +199 -0
  18. package/.claude/agents/sprint-runner.md +46 -0
  19. package/.claude/agents/stack-resolver.md +104 -0
  20. package/.claude/agents/tech-lead.md +182 -0
  21. package/.claude/agents/update-template.md +54 -0
  22. package/.claude/agents/ux-designer.md +118 -0
  23. package/.claude/hooks/flow-guard.js +261 -0
  24. package/.claude/hooks/flow-state.js +197 -0
  25. package/.claude/local/CLAUDE.md +71 -0
  26. package/.claude/settings.json +55 -0
  27. package/.claude/skills/README.md +331 -0
  28. package/.claude/skills/active-project/SKILL.md +131 -0
  29. package/.claude/skills/api-integration-test/SKILL.md +84 -0
  30. package/.claude/skills/auto-test-guard/SKILL.md +239 -0
  31. package/.claude/skills/auto-test-guard/resources/backend-tests.md +20 -0
  32. package/.claude/skills/auto-test-guard/resources/e2e-tests.md +24 -0
  33. package/.claude/skills/auto-test-guard/resources/execution-report.md +49 -0
  34. package/.claude/skills/auto-test-guard/resources/frontend-tests.md +18 -0
  35. package/.claude/skills/auto-test-guard/resources/initial-setup.md +108 -0
  36. package/.claude/skills/auto-test-guard/resources/run-suite.md +48 -0
  37. package/.claude/skills/auto-test-guard/resources/senior-gate.md +19 -0
  38. package/.claude/skills/brain-keeper/SKILL.md +62 -0
  39. package/.claude/skills/brain-keeper/obsidian/app.json +9 -0
  40. package/.claude/skills/brain-keeper/obsidian/appearance.json +4 -0
  41. package/.claude/skills/brain-keeper/obsidian/core-plugins.json +20 -0
  42. package/.claude/skills/brain-keeper/obsidian/daily-notes.json +5 -0
  43. package/.claude/skills/brain-keeper/obsidian/graph.json +32 -0
  44. package/.claude/skills/brain-keeper/obsidian/snippets/folder-colors.css +90 -0
  45. package/.claude/skills/brain-keeper/obsidian/templates.json +5 -0
  46. package/.claude/skills/brain-keeper/templates/README.md +51 -0
  47. package/.claude/skills/brain-keeper/templates/adr.md +40 -0
  48. package/.claude/skills/brain-keeper/templates/bug.md +35 -0
  49. package/.claude/skills/brain-keeper/templates/daily.md +38 -0
  50. package/.claude/skills/brain-keeper/templates/feature.md +62 -0
  51. package/.claude/skills/brain-keeper/templates/meeting.md +34 -0
  52. package/.claude/skills/brain-keeper/templates/tech-debt.md +21 -0
  53. package/.claude/skills/caveman/SKILL.md +189 -0
  54. package/.claude/skills/create-stack-pack/SKILL.md +281 -0
  55. package/.claude/skills/grill-me/SKILL.md +80 -0
  56. package/.claude/skills/pair-debug/SKILL.md +288 -0
  57. package/.claude/skills/prd-ready-check/SKILL.md +86 -0
  58. package/.claude/skills/project-manager/SKILL.md +334 -0
  59. package/.claude/skills/quality-standards/SKILL.md +203 -0
  60. package/.claude/skills/quick-feature/SKILL.md +266 -0
  61. package/.claude/skills/run-sprint/SKILL.md +41 -0
  62. package/.claude/skills/scaffold/SKILL.md +60 -0
  63. package/.claude/skills/stack-discovery/SKILL.md +161 -0
  64. package/.claude/skills/test-coverage-auditor/SKILL.md +87 -0
  65. package/.claude/skills/to-issues/SKILL.md +163 -0
  66. package/.claude/skills/to-prd/SKILL.md +130 -0
  67. package/.claude/skills/update-template/SKILL.md +256 -0
  68. package/.claude/stacks/CODEOWNERS +30 -0
  69. package/.claude/stacks/README.md +97 -0
  70. package/.claude/stacks/_template.md +116 -0
  71. package/.claude/stacks/dotnet/aspire-9.md +528 -0
  72. package/.claude/stacks/go/gin-1.10.md +570 -0
  73. package/.claude/stacks/java/spring-boot-3.md +376 -0
  74. package/.claude/stacks/java/spring-boot-4.md +438 -0
  75. package/.claude/stacks/node/express-5.md +538 -0
  76. package/.claude/stacks/python/django-5.md +483 -0
  77. package/.claude/stacks/python/fastapi-0.115.md +522 -0
  78. package/.claude/stacks/typescript/angular-18.md +420 -0
  79. package/.claude/stacks/typescript/angular-19.md +397 -0
  80. package/.claude/stacks/typescript/angular-21.md +494 -0
  81. package/CLAUDE.md +472 -0
  82. package/README.md +412 -0
  83. package/bin/cli.js +848 -0
  84. package/bin/lib/adr.js +146 -0
  85. package/bin/lib/backup.js +62 -0
  86. package/bin/lib/detect-stack.js +476 -0
  87. package/bin/lib/doctor.js +527 -0
  88. package/bin/lib/help.js +328 -0
  89. package/bin/lib/identity.js +108 -0
  90. package/bin/lib/lint-allowlist.json +15 -0
  91. package/bin/lib/lint.js +798 -0
  92. package/bin/lib/local-dir.js +68 -0
  93. package/bin/lib/manifest.js +236 -0
  94. package/bin/lib/sync-all.js +394 -0
  95. package/bin/lib/version-check.js +398 -0
  96. package/dashboard/db.js +321 -0
  97. package/dashboard/package.json +22 -0
  98. package/dashboard/public/app.js +853 -0
  99. package/dashboard/public/content/docs/agents-reference.en.md +911 -0
  100. package/dashboard/public/content/docs/architecture-overview.en.md +252 -0
  101. package/dashboard/public/content/docs/autonomy-matrix.en.md +186 -0
  102. package/dashboard/public/content/docs/cli-reference.en.md +538 -0
  103. package/dashboard/public/content/docs/git-flow.en.md +525 -0
  104. package/dashboard/public/content/docs/honcho-memory.en.md +394 -0
  105. package/dashboard/public/content/docs/hooks-reference.en.md +404 -0
  106. package/dashboard/public/content/docs/pipeline.en.md +414 -0
  107. package/dashboard/public/content/docs/plugins.en.md +289 -0
  108. package/dashboard/public/content/docs/quality-gate.en.md +315 -0
  109. package/dashboard/public/content/docs/skills-reference.en.md +484 -0
  110. package/dashboard/public/content/docs/stack-rules.en.md +362 -0
  111. package/dashboard/public/content/docs/troubleshooting.en.md +565 -0
  112. package/dashboard/public/content/manifest.json +114 -0
  113. package/dashboard/public/content/manual/backend.en.md +1053 -0
  114. package/dashboard/public/content/manual/existing-project.en.md +848 -0
  115. package/dashboard/public/content/manual/frontend.en.md +1008 -0
  116. package/dashboard/public/content/manual/fullstack.en.md +1459 -0
  117. package/dashboard/public/content/manual/mobile.en.md +837 -0
  118. package/dashboard/public/content/manual/quickstart.en.md +169 -0
  119. package/dashboard/public/index.html +217 -0
  120. package/dashboard/public/style.css +857 -0
  121. package/dashboard/public/vendor/marked.min.js +69 -0
  122. package/dashboard/rtk.js +143 -0
  123. package/dashboard/server-app.js +421 -0
  124. package/dashboard/server.js +104 -0
  125. package/dashboard/test/sprint1.test.js +406 -0
  126. package/dashboard/test/sprint2.test.js +571 -0
  127. package/dashboard/test/sprint3.test.js +560 -0
  128. package/package.json +33 -0
  129. package/scripts/hooks/subagent-telemetry.sh +14 -0
  130. package/scripts/hooks/telemetry-writer.js +250 -0
  131. package/scripts/latest-versions.json +56 -0
@@ -0,0 +1,522 @@
1
+ ---
2
+ stack: python/fastapi-0.115
3
+ versions_covered: "0.115.x — 0.118.x"
4
+ last_validated: 2026-05-28
5
+ validated_against: "reference pack — Python 3.13 + FastAPI 0.115 + Pydantic 2.9 + SQLAlchemy 2.0"
6
+ status: active
7
+ pack_owner: "@elton"
8
+ security_review: 2026-05-28
9
+ next_review_due: 2027-05-28
10
+ ---
11
+
12
+ # Python 3.13 + FastAPI 0.115+
13
+
14
+ Canonical knowledge pack for FastAPI projects on Python 3.12+/3.13 + FastAPI 0.115+ (current line) + Pydantic v2 + SQLAlchemy 2.0. FastAPI's strengths show in async-first API services, MLOps endpoints, microservices, and projects where the admin/ORM/forms of Django would be dead weight. For projects that want Django's batteries-included experience (admin, forms, auth, ORM, migrations), use `python/django-5.md` instead.
15
+
16
+ ## 1. When to use this pack
17
+
18
+ - Project declares `Primary stack: Python 3.12+ + FastAPI 0.115+` in `## Project Identity`.
19
+ - `pyproject.toml` declares `fastapi>=0.115,<0.120` and `python_requires = ">=3.12"`.
20
+ - Service is API-only (REST or gRPC), no admin UI, no server-rendered HTML at scale.
21
+ - Heavy async I/O — multiple external HTTP calls, websockets, server-sent events, streaming.
22
+ - MLOps / model-serving — Pydantic validation + automatic OpenAPI is FastAPI's home turf.
23
+ - For full-stack monolith with admin + forms + ORM: use Django (`python/django-5.md`).
24
+ - For batch processing without HTTP layer: use a plain Python project with `celery` or `prefect`, not FastAPI.
25
+
26
+ ## 2. Stack baseline (what this pack assumes)
27
+
28
+ | Component | Version range | Notes |
29
+ |---|---|---|
30
+ | Python | 3.12 (min) / 3.13 (recommended) | 3.13 free-threaded build works with FastAPI; type hints are mandatory for the framework to function |
31
+ | FastAPI | 0.115.x — 0.118.x | API stable; Annotated dependencies are the canonical form (0.100+) |
32
+ | Pydantic | 2.9.x+ | Pydantic v1 unsupported as of FastAPI 0.100; do not mix |
33
+ | Pydantic Settings | 2.5.x+ | Config via env vars, validated at startup |
34
+ | SQLAlchemy | 2.0.x | New 2.0-style API (`select(...).where(...)`), `Mapped[...]` type annotations |
35
+ | Async DB driver | `asyncpg` (Postgres), `aiomysql` (MySQL), `aiosqlite` (tests only) | NEVER mix sync `psycopg2` with async engine; pick one stack |
36
+ | Migrations | Alembic 1.13+ | autogenerate is a draft, always review; never manual `Base.metadata.create_all()` in prod |
37
+ | Build / packaging | `uv` (recommended) or `pip` + `pyproject.toml` | `uv pip sync requirements.lock` for deterministic installs |
38
+ | ASGI server | `uvicorn[standard]` workers + `gunicorn` master | Or `granian` for lower latency in production |
39
+ | Tests | pytest 8.x + `pytest-asyncio` + `httpx` AsyncClient + Testcontainers Postgres | NEVER SQLite if prod is Postgres |
40
+ | Mutation | mutmut 3.x | Target ≥70% on `domain/` + `application/` |
41
+ | Coverage | coverage.py 7.x with `branch = True` | Target ≥85% lines, ≥80% branches |
42
+ | Static analysis | `ruff` + `mypy --strict` | Pydantic models get static checks via mypy plugin |
43
+ | Security scan | `pip-audit` + `bandit` | 0 CVE with CVSS ≥7.0 |
44
+ | Observability | OpenTelemetry SDK + `opentelemetry-instrumentation-fastapi` | W3C Trace Context; auto-instruments routes + DB + HTTP client |
45
+ | Background jobs | Celery 5.x + Redis OR `arq` (async-native) | `arq` integrates with FastAPI lifespan cleanly |
46
+ | OpenAPI docs | Auto-generated at `/docs` (Swagger UI) and `/redoc` (Redoc) | Disable in prod via `FastAPI(docs_url=None, redoc_url=None)` |
47
+
48
+ ## 3. Project structure (DDD-flavored FastAPI)
49
+
50
+ ```
51
+ service/
52
+ ├── pyproject.toml
53
+ ├── uv.lock
54
+ ├── alembic.ini
55
+ ├── alembic/
56
+ │ ├── env.py
57
+ │ └── versions/
58
+ ├── env.example
59
+ ├── src/
60
+ │ ├── domain/ # pure Python, no FastAPI/SQLAlchemy imports
61
+ │ │ ├── products/
62
+ │ │ │ ├── entities.py
63
+ │ │ │ ├── repository.py # Protocol/ABC port
64
+ │ │ │ └── services.py
65
+ │ │ └── shared/
66
+ │ ├── application/ # use cases (1 callable each)
67
+ │ │ ├── products/
68
+ │ │ │ ├── create_product.py
69
+ │ │ │ ├── list_products.py
70
+ │ │ │ └── dto.py
71
+ │ │ └── shared/
72
+ │ ├── infrastructure/ # SQLAlchemy + external clients
73
+ │ │ ├── db/
74
+ │ │ │ ├── engine.py # async engine + sessionmaker
75
+ │ │ │ └── base.py # DeclarativeBase
76
+ │ │ ├── products/
77
+ │ │ │ ├── models.py # ORM (SQLAlchemy 2.0 Mapped)
78
+ │ │ │ └── repository_impl.py
79
+ │ │ └── http/
80
+ │ │ └── upstream_client.py # httpx.AsyncClient wrapper
81
+ │ ├── api/ # FastAPI routes + schemas
82
+ │ │ ├── products/
83
+ │ │ │ ├── router.py
84
+ │ │ │ ├── schemas.py # Pydantic req/res models
85
+ │ │ │ └── deps.py # Depends() factories
86
+ │ │ └── shared/
87
+ │ │ ├── exception_handlers.py # RFC 9457 ProblemDetail
88
+ │ │ └── middleware.py
89
+ │ ├── config/
90
+ │ │ └── settings.py # pydantic-settings BaseSettings
91
+ │ └── main.py # FastAPI() instance + lifespan
92
+ └── tests/
93
+ ├── unit/ # domain + application
94
+ ├── integration/ # API + DB with Testcontainers
95
+ └── e2e/ # full HTTP through TestClient
96
+ ```
97
+
98
+ **Rule**: `domain/` and `application/` contain **zero FastAPI and zero SQLAlchemy imports**. They are pure Python testable without any HTTP server or DB. `infrastructure/` imports SQLAlchemy. `api/` imports FastAPI. The 4 layers do not skip — `api/` never imports `infrastructure/` directly; it goes through `application/`.
99
+
100
+ ## 4. Code patterns
101
+
102
+ ### Pydantic schemas (request/response separated)
103
+
104
+ ```python
105
+ # src/api/products/schemas.py
106
+ from pydantic import BaseModel, Field, ConfigDict
107
+ from decimal import Decimal
108
+ from uuid import UUID
109
+ from datetime import datetime
110
+
111
+ class CreateProductRequest(BaseModel):
112
+ model_config = ConfigDict(extra="forbid") # reject unknown fields
113
+ name: str = Field(min_length=1, max_length=120)
114
+ price: Decimal = Field(gt=Decimal("0"))
115
+ stock: int = Field(ge=0)
116
+
117
+ class ProductResponse(BaseModel):
118
+ model_config = ConfigDict(from_attributes=True)
119
+ id: UUID
120
+ name: str
121
+ price: Decimal
122
+ stock: int
123
+ created_at: datetime
124
+ ```
125
+
126
+ **Rule**: `extra="forbid"` on every request model — silent acceptance of unknown fields is a footgun. Response models declared explicitly (never just return ORM object dict).
127
+
128
+ ### Route (Annotated dependencies, FastAPI 0.115 canonical)
129
+
130
+ ```python
131
+ # src/api/products/router.py
132
+ from typing import Annotated
133
+ from fastapi import APIRouter, Depends, status
134
+ from src.api.products.schemas import CreateProductRequest, ProductResponse
135
+ from src.application.products.create_product import CreateProductUseCase
136
+ from src.api.products.deps import get_create_product_use_case
137
+
138
+ router = APIRouter(prefix="/api/v1/products", tags=["products"])
139
+
140
+ @router.post("", status_code=status.HTTP_201_CREATED, response_model=ProductResponse)
141
+ async def create_product(
142
+ req: CreateProductRequest,
143
+ use_case: Annotated[CreateProductUseCase, Depends(get_create_product_use_case)],
144
+ ) -> ProductResponse:
145
+ product = await use_case.execute(name=req.name, price=req.price, stock=req.stock)
146
+ return ProductResponse.model_validate(product)
147
+ ```
148
+
149
+ **Rule**: `Annotated[T, Depends(...)]` is the canonical form (FastAPI 0.95+). Routes are thin — parse, call use case, validate response. Business logic stays in `application/`.
150
+
151
+ ### Domain entity (no SQLAlchemy / no FastAPI)
152
+
153
+ ```python
154
+ # src/domain/products/entities.py
155
+ from dataclasses import dataclass
156
+ from decimal import Decimal
157
+ from uuid import UUID
158
+ from datetime import datetime
159
+
160
+ @dataclass(frozen=True)
161
+ class Product:
162
+ id: UUID
163
+ name: str
164
+ price: Decimal
165
+ stock: int
166
+ created_at: datetime
167
+
168
+ def reserve(self, qty: int) -> "Product":
169
+ if qty <= 0:
170
+ raise ValueError("qty must be positive")
171
+ if qty > self.stock:
172
+ raise ValueError("insufficient stock")
173
+ return Product(self.id, self.name, self.price, self.stock - qty, self.created_at)
174
+ ```
175
+
176
+ ### Domain port + infrastructure adapter (SQLAlchemy 2.0)
177
+
178
+ ```python
179
+ # src/domain/products/repository.py
180
+ from typing import Protocol, Optional
181
+ from uuid import UUID
182
+ from .entities import Product
183
+
184
+ class ProductRepository(Protocol):
185
+ async def get(self, id: UUID) -> Optional[Product]: ...
186
+ async def save(self, product: Product) -> None: ...
187
+ ```
188
+
189
+ ```python
190
+ # src/infrastructure/products/models.py
191
+ from sqlalchemy.orm import Mapped, mapped_column
192
+ from sqlalchemy import String, Numeric, DateTime, func
193
+ from datetime import datetime
194
+ from decimal import Decimal
195
+ from uuid import UUID, uuid4
196
+ import sqlalchemy as sa
197
+ from src.infrastructure.db.base import Base
198
+
199
+ class ProductORM(Base):
200
+ __tablename__ = "products"
201
+ id: Mapped[UUID] = mapped_column(sa.Uuid, primary_key=True, default=uuid4)
202
+ name: Mapped[str] = mapped_column(String(120), index=True)
203
+ price: Mapped[Decimal] = mapped_column(Numeric(12, 2))
204
+ stock: Mapped[int] = mapped_column()
205
+ created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
206
+ ```
207
+
208
+ ```python
209
+ # src/infrastructure/products/repository_impl.py
210
+ from sqlalchemy.ext.asyncio import AsyncSession
211
+ from sqlalchemy import select
212
+ from uuid import UUID
213
+ from typing import Optional
214
+ from src.domain.products.entities import Product
215
+ from src.domain.products.repository import ProductRepository
216
+ from src.infrastructure.products.models import ProductORM
217
+
218
+ class SqlAlchemyProductRepository(ProductRepository):
219
+ def __init__(self, session: AsyncSession):
220
+ self._session = session
221
+
222
+ async def get(self, id: UUID) -> Optional[Product]:
223
+ row = await self._session.scalar(select(ProductORM).where(ProductORM.id == id))
224
+ return self._to_domain(row) if row else None
225
+
226
+ async def save(self, product: Product) -> None:
227
+ row = ProductORM(
228
+ id=product.id, name=product.name,
229
+ price=product.price, stock=product.stock,
230
+ )
231
+ await self._session.merge(row)
232
+ await self._session.commit()
233
+
234
+ @staticmethod
235
+ def _to_domain(row: ProductORM) -> Product:
236
+ return Product(
237
+ id=row.id, name=row.name, price=row.price,
238
+ stock=row.stock, created_at=row.created_at,
239
+ )
240
+ ```
241
+
242
+ **Rule**: SQLAlchemy 2.0 style — `select(Model).where(...)` not legacy `Model.query`. `Mapped[T]` annotations for static type-check. Repository implements the domain `Protocol`.
243
+
244
+ ### Lifespan + dependency injection
245
+
246
+ ```python
247
+ # src/main.py
248
+ from contextlib import asynccontextmanager
249
+ from fastapi import FastAPI
250
+ from src.infrastructure.db.engine import engine, AsyncSessionLocal
251
+ from src.api.products.router import router as products_router
252
+
253
+ @asynccontextmanager
254
+ async def lifespan(app: FastAPI):
255
+ # Startup: warm-up, health checks against DB, etc.
256
+ yield
257
+ # Shutdown: close engine cleanly
258
+ await engine.dispose()
259
+
260
+ app = FastAPI(
261
+ title="Products Service",
262
+ lifespan=lifespan,
263
+ docs_url=None, # disable in prod via settings
264
+ redoc_url=None,
265
+ )
266
+ app.include_router(products_router)
267
+ ```
268
+
269
+ **Rule**: `lifespan` is the canonical startup/shutdown hook (FastAPI 0.93+). Never use deprecated `@app.on_event("startup")` — it does not support async DI correctly.
270
+
271
+ ## 5. Testing
272
+
273
+ ### Unit (pytest, no FastAPI/DB)
274
+
275
+ ```python
276
+ # tests/unit/domain/products/test_product.py
277
+ import pytest
278
+ from decimal import Decimal
279
+ from uuid import uuid4
280
+ from datetime import datetime, timezone
281
+ from src.domain.products.entities import Product
282
+
283
+ def test_reserve_decreases_stock():
284
+ p = Product(uuid4(), "x", Decimal("9.90"), 10, datetime.now(timezone.utc))
285
+ p2 = p.reserve(3)
286
+ assert p2.stock == 7
287
+
288
+ def test_reserve_refuses_excess():
289
+ p = Product(uuid4(), "x", Decimal("9.90"), 10, datetime.now(timezone.utc))
290
+ with pytest.raises(ValueError):
291
+ p.reserve(11)
292
+ ```
293
+
294
+ ### Integration (httpx AsyncClient + Testcontainers Postgres)
295
+
296
+ ```python
297
+ # conftest.py
298
+ import pytest
299
+ import pytest_asyncio
300
+ from testcontainers.postgres import PostgresContainer
301
+ from httpx import ASGITransport, AsyncClient
302
+ from src.main import app
303
+
304
+ @pytest.fixture(scope="session")
305
+ def postgres():
306
+ with PostgresContainer("postgres:16-alpine") as pg:
307
+ yield pg
308
+
309
+ @pytest_asyncio.fixture
310
+ async def client():
311
+ transport = ASGITransport(app=app)
312
+ async with AsyncClient(transport=transport, base_url="http://test") as ac:
313
+ yield ac
314
+ ```
315
+
316
+ ```python
317
+ # tests/integration/api/products/test_create.py
318
+ import pytest
319
+
320
+ @pytest.mark.asyncio
321
+ async def test_create_product_returns_201(client):
322
+ r = await client.post("/api/v1/products", json={
323
+ "name": "widget", "price": "9.90", "stock": 100,
324
+ })
325
+ assert r.status_code == 201
326
+ body = r.json()
327
+ assert body["name"] == "widget"
328
+ assert body["stock"] == 100
329
+ ```
330
+
331
+ **Rule**: TestClient from FastAPI is sync — use `httpx.AsyncClient` with `ASGITransport` for async tests. `pytest-asyncio` strict mode.
332
+
333
+ ### Mutation (mutmut)
334
+
335
+ ```bash
336
+ mutmut run --paths-to-mutate=src/domain,src/application
337
+ mutmut results
338
+ # Target: killed/total >= 70% on domain + application
339
+ ```
340
+
341
+ ## 6. Build & run commands
342
+
343
+ ```bash
344
+ # Setup
345
+ uv venv
346
+ uv pip sync requirements.lock # or: uv pip install -e ".[dev]"
347
+
348
+ # Migrations
349
+ alembic upgrade head
350
+
351
+ # Run dev
352
+ uvicorn src.main:app --reload --host 0.0.0.0 --port 8000
353
+
354
+ # Run prod (multiple workers)
355
+ gunicorn src.main:app -w 4 -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:8000
356
+
357
+ # OR with granian for lower latency
358
+ granian --interface asgi src.main:app --workers 4 --port 8000
359
+
360
+ # Lint + format
361
+ ruff check --fix .
362
+ ruff format .
363
+
364
+ # Type check
365
+ mypy --strict src/
366
+
367
+ # Tests
368
+ pytest # all
369
+ pytest tests/unit/ # fast loop
370
+ pytest --cov=src --cov-branch --cov-fail-under=85
371
+ pytest -m "not slow" # skip Testcontainers
372
+
373
+ # Mutation
374
+ mutmut run --paths-to-mutate=src/domain,src/application
375
+
376
+ # Security scan
377
+ pip-audit
378
+ bandit -ll -r src/
379
+
380
+ # Migrations (always review autogenerate)
381
+ alembic revision --autogenerate -m "add products table"
382
+ alembic upgrade head
383
+ alembic downgrade -1
384
+ ```
385
+
386
+ ## 7. Security (per ADR-007 + ADR-027 — MANDATORY section)
387
+
388
+ ### 7.1 Authentication & Authorization
389
+
390
+ - **JWT (stateless)**: `python-jose[cryptography]` + `passlib[bcrypt]`. RS256 (asymmetric) recommended for multi-service setups; HS256 acceptable for single-service.
391
+ - **OAuth2 + OpenID Connect**: `authlib` integrates with Auth0/Keycloak/Cognito.
392
+ - **API keys for internal services**: `secrets.token_urlsafe(32)` + hashed storage. Never plaintext.
393
+ - **Password hashing**: bcrypt cost 12 (passlib default), or Argon2 via `argon2-cffi` (preferred for new projects).
394
+ - **Authorization**: dependency-based. `Depends(get_current_user)` → `Depends(require_role("admin"))` chain. Object-level checks inside the use case.
395
+
396
+ ### 7.2 CORS
397
+
398
+ ```python
399
+ # src/main.py
400
+ from fastapi.middleware.cors import CORSMiddleware
401
+
402
+ app.add_middleware(
403
+ CORSMiddleware,
404
+ allow_origins=["https://app.example.com"], # NEVER ["*"] in prod
405
+ allow_credentials=True,
406
+ allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
407
+ allow_headers=["Authorization", "Content-Type"],
408
+ )
409
+ ```
410
+
411
+ ### 7.3 Validation & input sanitization
412
+
413
+ - **SQL injection**: SQLAlchemy 2.0 parameterizes by default. NEVER `text(f"SELECT * FROM users WHERE id = {user_input}")` — use `text("... :id").bindparams(id=user_input)`.
414
+ - **NoSQL injection**: if using MongoDB via `motor`, use the typed query builder, not dict concatenation.
415
+ - **Pydantic strictness**: `extra="forbid"` on all request models. Type validation is the first line of defense.
416
+ - **File uploads**: `UploadFile.content_type` is client-supplied — always re-validate via `python-magic` server-side. Never trust the extension.
417
+
418
+ ### 7.4 Secrets management
419
+
420
+ ```python
421
+ # src/config/settings.py
422
+ from pydantic_settings import BaseSettings, SettingsConfigDict
423
+
424
+ class Settings(BaseSettings):
425
+ model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8")
426
+ database_url: str
427
+ jwt_secret_key: str
428
+ sentry_dsn: str | None = None
429
+
430
+ settings = Settings()
431
+ ```
432
+
433
+ - `.env` gitignored; `env.example` committed.
434
+ - Cloud-native: prefer Secret Manager (AWS / GCP) over `.env` in prod. `pydantic-settings` can read from there.
435
+
436
+ ### 7.5 Rate limiting
437
+
438
+ ```python
439
+ # pip install slowapi
440
+ from slowapi import Limiter, _rate_limit_exceeded_handler
441
+ from slowapi.errors import RateLimitExceeded
442
+ from slowapi.util import get_remote_address
443
+
444
+ limiter = Limiter(key_func=get_remote_address)
445
+ app.state.limiter = limiter
446
+ app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
447
+
448
+ @router.post("/login")
449
+ @limiter.limit("5/minute")
450
+ async def login(request: Request, ...): ...
451
+ ```
452
+
453
+ - `5/minute` on `/login`, `/signup`, `/password-reset`.
454
+ - `100/minute` on regular authenticated GETs.
455
+ - Behind reverse proxy: use `X-Forwarded-For` (validate the proxy is trusted!).
456
+
457
+ ### 7.6 OWASP Top 10 mapping
458
+
459
+ | OWASP | Mitigation in FastAPI 0.115 |
460
+ |---|---|
461
+ | A01 Broken Access Control | `Depends(require_role(...))` per route; object-level check inside use case; default-deny pattern |
462
+ | A02 Cryptographic Failures | Argon2/bcrypt password hashing; TLS terminated at proxy with HSTS; JWT RS256 with kid rotation |
463
+ | A03 Injection | SQLAlchemy parameterization; Pydantic `extra="forbid"`; never `text()` with f-strings |
464
+ | A04 Insecure Design | DDD layering enforced (`domain/` no FastAPI); use case = single async callable; ADRs document deviations |
465
+ | A05 Security Misconfiguration | `docs_url=None, redoc_url=None` in prod; `DEBUG=False` via env split; explicit CORS allowlist |
466
+ | A06 Vulnerable Components | `pip-audit` in CI; `renovate` for PR bumps; `bandit` for code patterns |
467
+ | A07 Auth Failures | slowapi rate limit on auth endpoints; account lock via Redis counter; MFA via `pyotp` |
468
+ | A08 Data Integrity | `uv pip install --require-hashes`; SBOM via `cyclonedx-py`; signed releases |
469
+ | A09 Logging Failures | Structured JSON via `structlog`; correlation ID via `asgi-correlation-id`; **NEVER** log request body raw |
470
+ | A10 SSRF | Allowlist outbound HTTP via centralized `httpx.AsyncClient` with explicit allowed hosts; reject `localhost`, `169.254.169.254`, private CIDRs by default |
471
+
472
+ ### 7.7 LGPD / GDPR / compliance specifics
473
+
474
+ - **PII fields tagged**: convention `pii=True` in column comments + custom mypy check.
475
+ - **Soft delete**: `is_deleted` + `deleted_at` columns; filter via SQLAlchemy event listener that injects `WHERE is_deleted = false`.
476
+ - **Data subject access (Article 15)**: `GET /api/v1/me/export` returns all user data as ZIP/JSON.
477
+ - **Erasure (Article 17)**: `DELETE /api/v1/me` redacts PII columns while preserving FKs (orders kept, customer = null).
478
+ - **Encryption at rest**: PostgreSQL TDE (cloud-managed) or column-level via `sqlalchemy-utils` `EncryptedType`.
479
+
480
+ ## 8. Anti-patterns (block in code-review)
481
+
482
+ | ❌ Bad | ✅ Good | Why |
483
+ |---|---|---|
484
+ | `def` async route that does sync I/O (e.g., `requests.get`) | `async def` + `httpx.AsyncClient` OR `def` sync route with sync I/O | Sync I/O inside `async def` blocks the event loop and kills throughput |
485
+ | `extra="allow"` on request `BaseModel` (default `extra="ignore"`) | `extra="forbid"` explicitly | Silent acceptance of typos / unintended fields = footgun |
486
+ | Returning ORM object directly from route | `response_model=Schema` + `Schema.model_validate(obj)` | API contract leaks data model otherwise |
487
+ | `Base.metadata.create_all()` in `lifespan` startup | Alembic migrations, applied separately | Auto-create skips review and migration history |
488
+ | `Session.commit()` inside a use case AND inside repository | Commit at exactly one layer (typically the use case via UnitOfWork) | Double commit / nested transactions = subtle bugs |
489
+ | SQLite for tests when prod is Postgres | Testcontainers Postgres 16 | Subtle SQL differences cause prod-only bugs |
490
+ | `engine = create_engine(url, echo=True)` in prod | `echo=False` in prod; `echo=True` only for local debugging | Logs every query; PII leakage; performance hit |
491
+ | Catching `Exception:` in routes | Custom exception handlers via `app.add_exception_handler` | Hides bugs and breaks 500 telemetry |
492
+ | Storing JWT secret in code | `pydantic_settings` from env, validated at startup | Secret in code = secret in git history |
493
+ | Disabling `docs_url`/`redoc_url` via global if/else | Settings flag + `docs_url=settings.openapi_url` pattern | Centralized config wins |
494
+ | Returning `dict` directly without a Pydantic response model | `response_model=...` declared explicitly | Type safety + OpenAPI schema correctness |
495
+
496
+ ## 9. Migration hints — FastAPI 0.95 → 0.115
497
+
498
+ Breaking changes worth flagging when `migrator` agent runs FastAPI 0.95+ → 0.115:
499
+
500
+ - **Pydantic v1 unsupported** (FastAPI 0.100+). All `BaseModel`/`Field`/`validator` imports must use Pydantic v2. `@validator` → `@field_validator`. `Config` class → `model_config = ConfigDict(...)`.
501
+ - **`Annotated` dependencies are the canonical form**. `param: SomeType = Depends(...)` still works but is being phased out. Migrate to `param: Annotated[SomeType, Depends(...)]`.
502
+ - **`lifespan` replaces `on_event`** (FastAPI 0.93+). `@app.on_event("startup")` is deprecated; use `lifespan` context manager.
503
+ - **`response_model_exclude_unset`** behavior changed slightly when combined with `response_model_by_alias`. Test both options if you depend on aliasing.
504
+ - **WebSocket** API gained stricter typing. If you use `WebSocket.receive_json()` patterns from <0.100, double-check.
505
+ - **OpenAPI 3.1**: FastAPI 0.99+ emits OpenAPI 3.1 by default. Older clients may need explicit `openapi_version="3.0.0"` or use the `openapi_extra` shim.
506
+
507
+ Hand off to `migrator` with: current FastAPI version, current Pydantic version, list of routes using `Depends()` without `Annotated`, list of `@app.on_event` handlers.
508
+
509
+ ## 10. References
510
+
511
+ - [FastAPI 0.115 docs](https://fastapi.tiangolo.com/)
512
+ - [Pydantic v2 migration guide](https://docs.pydantic.dev/latest/migration/)
513
+ - [SQLAlchemy 2.0 docs](https://docs.sqlalchemy.org/en/20/)
514
+ - [Alembic docs](https://alembic.sqlalchemy.org/en/latest/)
515
+ - [pytest-asyncio docs](https://pytest-asyncio.readthedocs.io/)
516
+ - [slowapi (rate limiting)](https://github.com/laurentS/slowapi)
517
+ - [asgi-correlation-id](https://github.com/snok/asgi-correlation-id)
518
+ - [OWASP REST Security Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/REST_Security_Cheat_Sheet.html)
519
+ - ADR-007 (Senior+ gate thresholds — coverage ≥85%, mutation ≥70%)
520
+ - ADR-026 (Generic agents + stack packs architecture)
521
+ - ADR-027 (Pack governance — frontmatter + security mandatory + CODEOWNERS + annual review)
522
+ - ADR-029 (Canonical pack format — this document follows it)