create-rigel 0.1.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.
- package/LICENSE +21 -0
- package/README.md +156 -0
- package/cli.js +122 -0
- package/package.json +47 -0
- package/templates/express/.claude/CLAUDE.md +114 -0
- package/templates/express/.claude/agents/arch-validator.md +85 -0
- package/templates/express/.claude/agents/db-optimizer.md +72 -0
- package/templates/express/.claude/agents/doc-gardener.md +67 -0
- package/templates/express/.claude/agents/garbage-collector.md +94 -0
- package/templates/express/.claude/agents/gate-checker.md +176 -0
- package/templates/express/.claude/agents/reviewer.md +84 -0
- package/templates/express/.claude/agents/security-auditor.md +82 -0
- package/templates/express/.claude/hooks/post-write.sh +93 -0
- package/templates/express/.claude/rules/api.md +149 -0
- package/templates/express/.claude/rules/architecture.md +78 -0
- package/templates/express/.claude/rules/database.md +104 -0
- package/templates/express/.claude/rules/jobs.md +81 -0
- package/templates/express/.claude/rules/security.md +92 -0
- package/templates/express/.claude/rules/testing.md +97 -0
- package/templates/express/.claude/settings.json +35 -0
- package/templates/express/.claude/skills/00-infra-setup/SKILL.md +479 -0
- package/templates/express/.claude/skills/01-write-roadmap/SKILL.md +72 -0
- package/templates/express/.claude/skills/02-write-spec/SKILL.md +119 -0
- package/templates/express/.claude/skills/03-write-plan/SKILL.md +124 -0
- package/templates/express/.claude/skills/04-build-layer/SKILL.md +216 -0
- package/templates/express/.claude/skills/05-validate-layer/SKILL.md +43 -0
- package/templates/express/.claude/skills/06-push-layer/SKILL.md +59 -0
- package/templates/express/.claude/skills/07-layer-check/SKILL.md +62 -0
- package/templates/express/.claude/skills/08-garbage-collect/SKILL.md +86 -0
- package/templates/express/.claude/skills/09-doc-garden/SKILL.md +64 -0
- package/templates/express/.claude/skills/10-db-optimize/SKILL.md +96 -0
- package/templates/express/.claude/skills/11-load-test/SKILL.md +77 -0
- package/templates/express/.dockerignore +43 -0
- package/templates/express/.env.example +42 -0
- package/templates/express/.github/CODEOWNERS +22 -0
- package/templates/express/.github/dependabot.yml +48 -0
- package/templates/express/.github/pull_request_template.md +58 -0
- package/templates/express/.github/workflows/ci.yml +278 -0
- package/templates/express/.github/workflows/load-test.yml +41 -0
- package/templates/express/.gitleaks.toml +18 -0
- package/templates/express/.lintstagedrc.json +3 -0
- package/templates/express/.nvmrc +1 -0
- package/templates/express/.prettierrc +8 -0
- package/templates/express/.sequelizerc +8 -0
- package/templates/express/AGENTS.md +94 -0
- package/templates/express/ARCHITECTURE.md +161 -0
- package/templates/express/Dockerfile +40 -0
- package/templates/express/Makefile +79 -0
- package/templates/express/QUICKSTART.md +131 -0
- package/templates/express/docker-compose.yml +114 -0
- package/templates/express/docs/PLANS.md +111 -0
- package/templates/express/docs/QUALITY_SCORE.md +66 -0
- package/templates/express/docs/design-docs/core-beliefs.md +89 -0
- package/templates/express/docs/design-docs/decisions/ADR-000-infrastructure.md +70 -0
- package/templates/express/docs/design-docs/decisions/ADR-001-observability.md +82 -0
- package/templates/express/docs/design-docs/decisions/index.md +23 -0
- package/templates/express/docs/design-docs/index.md +11 -0
- package/templates/express/docs/design-docs/observability.md +194 -0
- package/templates/express/docs/design-docs/team-workflow.md +64 -0
- package/templates/express/docs/exec-plans/active/.gitkeep +0 -0
- package/templates/express/docs/exec-plans/completed/.gitkeep +0 -0
- package/templates/express/docs/exec-plans/tech-debt-tracker.md +31 -0
- package/templates/express/docs/generated/.gitkeep +0 -0
- package/templates/express/docs/product-specs/ROADMAP.md +49 -0
- package/templates/express/docs/product-specs/draft/.gitkeep +0 -0
- package/templates/express/docs/product-specs/index.md +21 -0
- package/templates/express/docs/product-specs/ready/.gitkeep +0 -0
- package/templates/express/eslint.config.mjs +94 -0
- package/templates/express/gitignore +36 -0
- package/templates/express/infra/monitoring/alloy/config.alloy +59 -0
- package/templates/express/jest.config.ts +61 -0
- package/templates/express/package.json +45 -0
- package/templates/express/scripts/openapi.export.ts +77 -0
- package/templates/express/tests/architecture/isolation.test.ts +51 -0
- package/templates/express/tests/architecture/layers.test.ts +83 -0
- package/templates/express/tests/integration/health.test.ts +40 -0
- package/templates/express/tests/integration/isolation.test.template.ts +88 -0
- package/templates/express/tests/load/smoke.js +46 -0
- package/templates/express/tests/load/soak.js +45 -0
- package/templates/express/tests/load/stress.js +44 -0
- package/templates/express/tests/unit/utils/response.util.test.ts +53 -0
- package/templates/express/tsconfig.build.json +8 -0
- package/templates/express/tsconfig.json +20 -0
- package/templates/fastapi/.claude/CLAUDE.md +122 -0
- package/templates/fastapi/.claude/agents/arch-validator.md +64 -0
- package/templates/fastapi/.claude/agents/db-optimizer.md +59 -0
- package/templates/fastapi/.claude/agents/doc-gardener.md +55 -0
- package/templates/fastapi/.claude/agents/garbage-collector.md +74 -0
- package/templates/fastapi/.claude/agents/gate-checker.md +133 -0
- package/templates/fastapi/.claude/agents/reviewer.md +74 -0
- package/templates/fastapi/.claude/agents/security-auditor.md +69 -0
- package/templates/fastapi/.claude/hooks/post-write.sh +65 -0
- package/templates/fastapi/.claude/rules/api.md +119 -0
- package/templates/fastapi/.claude/rules/architecture.md +84 -0
- package/templates/fastapi/.claude/rules/database.md +153 -0
- package/templates/fastapi/.claude/rules/jobs.md +85 -0
- package/templates/fastapi/.claude/rules/security.md +102 -0
- package/templates/fastapi/.claude/rules/testing.md +207 -0
- package/templates/fastapi/.claude/settings.json +50 -0
- package/templates/fastapi/.claude/skills/00-infra-setup/SKILL.md +311 -0
- package/templates/fastapi/.claude/skills/01-write-roadmap/SKILL.md +61 -0
- package/templates/fastapi/.claude/skills/02-write-spec/SKILL.md +82 -0
- package/templates/fastapi/.claude/skills/03-write-plan/SKILL.md +83 -0
- package/templates/fastapi/.claude/skills/04-build-layer/SKILL.md +170 -0
- package/templates/fastapi/.claude/skills/05-validate-layer/SKILL.md +20 -0
- package/templates/fastapi/.claude/skills/06-push-layer/SKILL.md +31 -0
- package/templates/fastapi/.claude/skills/07-layer-check/SKILL.md +51 -0
- package/templates/fastapi/.claude/skills/08-garbage-collect/SKILL.md +47 -0
- package/templates/fastapi/.claude/skills/09-doc-garden/SKILL.md +9 -0
- package/templates/fastapi/.claude/skills/10-db-optimize/SKILL.md +53 -0
- package/templates/fastapi/.claude/skills/11-load-test/SKILL.md +81 -0
- package/templates/fastapi/.dockerignore +30 -0
- package/templates/fastapi/.github/dependabot.yml +24 -0
- package/templates/fastapi/AGENTS.md +87 -0
- package/templates/fastapi/ARCHITECTURE.md +141 -0
- package/templates/fastapi/Makefile +42 -0
- package/templates/fastapi/README.md +179 -0
- package/templates/fastapi/docs/PLANS.md +26 -0
- package/templates/fastapi/docs/QUALITY_SCORE.md +26 -0
- package/templates/fastapi/docs/design-docs/core-beliefs.md +46 -0
- package/templates/fastapi/docs/design-docs/decisions/ADR-000-infrastructure.md +79 -0
- package/templates/fastapi/docs/design-docs/decisions/index.md +7 -0
- package/templates/fastapi/docs/design-docs/index.md +9 -0
- package/templates/fastapi/docs/exec-plans/active/.gitkeep +0 -0
- package/templates/fastapi/docs/exec-plans/completed/.gitkeep +0 -0
- package/templates/fastapi/docs/exec-plans/tech-debt-tracker.md +19 -0
- package/templates/fastapi/docs/generated/.gitkeep +0 -0
- package/templates/fastapi/docs/product-specs/ROADMAP.md +49 -0
- package/templates/fastapi/docs/product-specs/draft/.gitkeep +0 -0
- package/templates/fastapi/docs/product-specs/index.md +18 -0
- package/templates/fastapi/docs/product-specs/ready/.gitkeep +0 -0
- package/templates/fastapi/gitignore +46 -0
- package/templates/fastapi/scripts/gate.sh +68 -0
- package/templates/nestjs/.claude/CLAUDE.md +121 -0
- package/templates/nestjs/.claude/agents/db-optimizer.md +51 -0
- package/templates/nestjs/.claude/agents/doc-gardener.md +45 -0
- package/templates/nestjs/.claude/agents/garbage-collector.md +50 -0
- package/templates/nestjs/.claude/agents/gate-checker.md +127 -0
- package/templates/nestjs/.claude/agents/reviewer.md +68 -0
- package/templates/nestjs/.claude/agents/security-auditor.md +72 -0
- package/templates/nestjs/.claude/hooks/post-write.sh +91 -0
- package/templates/nestjs/.claude/rules/api.md +87 -0
- package/templates/nestjs/.claude/rules/architecture.md +104 -0
- package/templates/nestjs/.claude/rules/database.md +111 -0
- package/templates/nestjs/.claude/rules/security.md +110 -0
- package/templates/nestjs/.claude/rules/testing.md +145 -0
- package/templates/nestjs/.claude/settings.json +33 -0
- package/templates/nestjs/.claude/skills/00-infra-setup/SKILL.md +222 -0
- package/templates/nestjs/.claude/skills/01-write-spec/SKILL.md +25 -0
- package/templates/nestjs/.claude/skills/02-write-plan/SKILL.md +18 -0
- package/templates/nestjs/.claude/skills/03-build-layer/SKILL.md +242 -0
- package/templates/nestjs/.claude/skills/04-validate-layer/SKILL.md +4 -0
- package/templates/nestjs/.claude/skills/05-push-layer/SKILL.md +6 -0
- package/templates/nestjs/.claude/skills/06-layer-check/SKILL.md +12 -0
- package/templates/nestjs/.claude/skills/07-garbage-collect/SKILL.md +2 -0
- package/templates/nestjs/.claude/skills/08-doc-garden/SKILL.md +4 -0
- package/templates/nestjs/.claude/skills/09-api-sync/SKILL.md +36 -0
- package/templates/nestjs/.claude/skills/10-load-test/SKILL.md +29 -0
- package/templates/nestjs/.env.example +11 -0
- package/templates/nestjs/AGENTS.md +88 -0
- package/templates/nestjs/ARCHITECTURE.md +192 -0
- package/templates/nestjs/docs/PLANS.md +22 -0
- package/templates/nestjs/docs/QUALITY_SCORE.md +27 -0
- package/templates/nestjs/docs/design-docs/core-beliefs.md +18 -0
- package/templates/nestjs/docs/design-docs/decisions/ADR-000-infrastructure.md +58 -0
- package/templates/nestjs/docs/design-docs/decisions/index.md +7 -0
- package/templates/nestjs/docs/design-docs/index.md +7 -0
- package/templates/nestjs/docs/exec-plans/active/.gitkeep +0 -0
- package/templates/nestjs/docs/exec-plans/completed/.gitkeep +0 -0
- package/templates/nestjs/docs/exec-plans/tech-debt-tracker.md +16 -0
- package/templates/nestjs/docs/generated/.gitkeep +0 -0
- package/templates/nestjs/docs/product-specs/draft/.gitkeep +0 -0
- package/templates/nestjs/docs/product-specs/index.md +7 -0
- package/templates/nestjs/docs/product-specs/ready/.gitkeep +0 -0
- package/templates/nestjs/gitignore +8 -0
- package/templates/nextjs/.claude/CLAUDE.md +139 -0
- package/templates/nextjs/.claude/agents/arch-validator.md +66 -0
- package/templates/nextjs/.claude/agents/contract-checker.md +78 -0
- package/templates/nextjs/.claude/agents/doc-gardener.md +52 -0
- package/templates/nextjs/.claude/agents/garbage-collector.md +61 -0
- package/templates/nextjs/.claude/agents/gate-checker.md +129 -0
- package/templates/nextjs/.claude/agents/perf-auditor.md +109 -0
- package/templates/nextjs/.claude/agents/reviewer.md +79 -0
- package/templates/nextjs/.claude/agents/security-auditor.md +50 -0
- package/templates/nextjs/.claude/hooks/post-write.sh +121 -0
- package/templates/nextjs/.claude/rules/api-contract.md +166 -0
- package/templates/nextjs/.claude/rules/architecture.md +99 -0
- package/templates/nextjs/.claude/rules/components.md +122 -0
- package/templates/nextjs/.claude/rules/security.md +122 -0
- package/templates/nextjs/.claude/rules/testing.md +187 -0
- package/templates/nextjs/.claude/scripts/infra-setup.sh +550 -0
- package/templates/nextjs/.claude/settings.json +33 -0
- package/templates/nextjs/.claude/skills/00-infra-setup/SKILL.md +471 -0
- package/templates/nextjs/.claude/skills/01-write-roadmap/SKILL.md +77 -0
- package/templates/nextjs/.claude/skills/02-write-spec/SKILL.md +78 -0
- package/templates/nextjs/.claude/skills/03-write-plan/SKILL.md +91 -0
- package/templates/nextjs/.claude/skills/04-build-layer/SKILL.md +141 -0
- package/templates/nextjs/.claude/skills/05-validate-layer/SKILL.md +13 -0
- package/templates/nextjs/.claude/skills/06-push-layer/SKILL.md +20 -0
- package/templates/nextjs/.claude/skills/07-layer-check/SKILL.md +28 -0
- package/templates/nextjs/.claude/skills/08-garbage-collect/SKILL.md +14 -0
- package/templates/nextjs/.claude/skills/09-doc-garden/SKILL.md +12 -0
- package/templates/nextjs/.claude/skills/10-api-sync/SKILL.md +50 -0
- package/templates/nextjs/.claude/skills/11-perf-budget/SKILL.md +51 -0
- package/templates/nextjs/.dockerignore +25 -0
- package/templates/nextjs/.gitattributes +10 -0
- package/templates/nextjs/.github/CODEOWNERS +24 -0
- package/templates/nextjs/.github/dependabot.yml +31 -0
- package/templates/nextjs/.github/pull_request_template.md +59 -0
- package/templates/nextjs/.github/workflows/ci.yml +143 -0
- package/templates/nextjs/.github/workflows/lighthouse.yml +54 -0
- package/templates/nextjs/.github/workflows/load-test.yml +54 -0
- package/templates/nextjs/.husky/pre-commit +1 -0
- package/templates/nextjs/.lintstagedrc.json +4 -0
- package/templates/nextjs/.mcp.json +8 -0
- package/templates/nextjs/.prettierignore +13 -0
- package/templates/nextjs/.prettierrc +9 -0
- package/templates/nextjs/AGENTS.md +88 -0
- package/templates/nextjs/ARCHITECTURE.md +165 -0
- package/templates/nextjs/Dockerfile +38 -0
- package/templates/nextjs/Makefile +50 -0
- package/templates/nextjs/QUICKSTART.md +128 -0
- package/templates/nextjs/docs/PLANS.md +23 -0
- package/templates/nextjs/docs/QUALITY_SCORE.md +27 -0
- package/templates/nextjs/docs/design-docs/core-beliefs.md +43 -0
- package/templates/nextjs/docs/design-docs/decisions/ADR-000-infrastructure.md +72 -0
- package/templates/nextjs/docs/design-docs/decisions/index.md +7 -0
- package/templates/nextjs/docs/design-docs/index.md +7 -0
- package/templates/nextjs/docs/design-docs/team-workflow.md +66 -0
- package/templates/nextjs/docs/exec-plans/active/.gitkeep +0 -0
- package/templates/nextjs/docs/exec-plans/completed/.gitkeep +0 -0
- package/templates/nextjs/docs/exec-plans/tech-debt-tracker.md +19 -0
- package/templates/nextjs/docs/generated/.gitkeep +0 -0
- package/templates/nextjs/docs/product-specs/ROADMAP.md +59 -0
- package/templates/nextjs/docs/product-specs/draft/.gitkeep +0 -0
- package/templates/nextjs/docs/product-specs/index.md +18 -0
- package/templates/nextjs/docs/product-specs/ready/.gitkeep +0 -0
- package/templates/nextjs/eslint.config.mjs +150 -0
- package/templates/nextjs/gitignore +35 -0
- package/templates/nextjs/lighthouserc.js +37 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "tests/**/*.py"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Testing Rules — Auto-injected on test file edits
|
|
7
|
+
|
|
8
|
+
## Coverage Thresholds (enforced in CI)
|
|
9
|
+
|
|
10
|
+
| Layer | Threshold | What to test |
|
|
11
|
+
|---|---|---|
|
|
12
|
+
| `src/utils/` | **100%** | Every branch, every edge case |
|
|
13
|
+
| `src/services/` | **90%** | Happy path + all error paths + state machines |
|
|
14
|
+
| `src/repo/` | **80%** | Query results, model_validate, ownership, cursor pagination |
|
|
15
|
+
| `src/runtime/routers/` | **75%** | Auth required, 422 validation, 404, 422 |
|
|
16
|
+
| `src/providers/` | **70%** | Core functionality |
|
|
17
|
+
|
|
18
|
+
## Required Test Patterns
|
|
19
|
+
|
|
20
|
+
### Service Tests — error paths mandatory
|
|
21
|
+
```python
|
|
22
|
+
import pytest
|
|
23
|
+
from src.services.application_service import ApplicationService
|
|
24
|
+
from src.types.exceptions import StageTransitionError, TerminalStageError
|
|
25
|
+
|
|
26
|
+
@pytest.mark.asyncio
|
|
27
|
+
async def test_transition_stage_invalid_raises(session, user_id, app_id):
|
|
28
|
+
with pytest.raises(StageTransitionError):
|
|
29
|
+
await ApplicationService.transition_stage(session, user_id, app_id, "ACCEPTED")
|
|
30
|
+
|
|
31
|
+
@pytest.mark.asyncio
|
|
32
|
+
async def test_transition_from_terminal_raises(session, user_id, accepted_app_id):
|
|
33
|
+
with pytest.raises(TerminalStageError):
|
|
34
|
+
await ApplicationService.transition_stage(session, user_id, accepted_app_id, "APPLIED")
|
|
35
|
+
|
|
36
|
+
@pytest.mark.asyncio
|
|
37
|
+
async def test_ownership_enforced(session, other_user_id, app_id):
|
|
38
|
+
with pytest.raises(ApplicationNotFoundError):
|
|
39
|
+
await ApplicationService.get(session, other_user_id, app_id)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Route Tests — test HTTP contract
|
|
43
|
+
```python
|
|
44
|
+
@pytest.mark.asyncio
|
|
45
|
+
async def test_create_application_requires_auth(async_client: AsyncClient):
|
|
46
|
+
response = await async_client.post("/api/v1/applications", json={...})
|
|
47
|
+
assert response.status_code == 401
|
|
48
|
+
|
|
49
|
+
@pytest.mark.asyncio
|
|
50
|
+
async def test_create_application_validates_body(auth_client: AsyncClient):
|
|
51
|
+
response = await auth_client.post("/api/v1/applications", json={"role": "Engineer"})
|
|
52
|
+
assert response.status_code == 422
|
|
53
|
+
|
|
54
|
+
@pytest.mark.asyncio
|
|
55
|
+
async def test_create_application_returns_envelope(auth_client: AsyncClient):
|
|
56
|
+
response = await auth_client.post("/api/v1/applications", json=valid_body)
|
|
57
|
+
assert response.status_code == 201
|
|
58
|
+
body = response.json()
|
|
59
|
+
# All responses are enveloped: { ok, data, meta } — see .claude/rules/api.md
|
|
60
|
+
assert body["ok"] is True
|
|
61
|
+
assert "request_id" in body["meta"]
|
|
62
|
+
data = body["data"]
|
|
63
|
+
assert "id" in data
|
|
64
|
+
assert "stage" in data
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Pydantic Schema Tests
|
|
68
|
+
```python
|
|
69
|
+
def test_create_input_rejects_missing_company():
|
|
70
|
+
with pytest.raises(ValidationError):
|
|
71
|
+
CreateApplicationInput(role="Engineer") # missing company
|
|
72
|
+
|
|
73
|
+
def test_stage_enum_rejects_invalid():
|
|
74
|
+
with pytest.raises(ValidationError):
|
|
75
|
+
ApplicationResponse(stage="PENDING", ...) # not a valid stage
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Architecture Tests
|
|
79
|
+
```python
|
|
80
|
+
# tests/architecture/test_layers.py
|
|
81
|
+
import ast, pathlib
|
|
82
|
+
|
|
83
|
+
def get_imports(filepath: pathlib.Path) -> list[str]:
|
|
84
|
+
# NOTE: ast.Import has NO .module attribute (only ast.ImportFrom does).
|
|
85
|
+
# Handling them together as `node.module` raises AttributeError on any
|
|
86
|
+
# plain `import x` statement — the test would crash, not enforce. Split them.
|
|
87
|
+
tree = ast.parse(filepath.read_text())
|
|
88
|
+
out: list[str] = []
|
|
89
|
+
for node in ast.walk(tree):
|
|
90
|
+
if isinstance(node, ast.ImportFrom):
|
|
91
|
+
out.append(node.module or "")
|
|
92
|
+
elif isinstance(node, ast.Import):
|
|
93
|
+
out.extend(alias.name for alias in node.names)
|
|
94
|
+
return out
|
|
95
|
+
|
|
96
|
+
def test_service_does_not_import_fastapi():
|
|
97
|
+
services = pathlib.Path("src/services").rglob("*.py")
|
|
98
|
+
for f in services:
|
|
99
|
+
imports = get_imports(f)
|
|
100
|
+
assert not any("fastapi" in i for i in imports), \
|
|
101
|
+
f"{f} imports fastapi — services must not depend on HTTP layer"
|
|
102
|
+
|
|
103
|
+
def test_utils_has_no_domain_imports():
|
|
104
|
+
utils = pathlib.Path("src/utils").rglob("*.py")
|
|
105
|
+
domain = ["src.types", "src.config", "src.models", "src.repo", "src.services", "src.runtime"]
|
|
106
|
+
for f in utils:
|
|
107
|
+
imports = get_imports(f)
|
|
108
|
+
for imp in imports:
|
|
109
|
+
assert not any(d in imp for d in domain), \
|
|
110
|
+
f"{f} imports domain code: {imp}"
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## conftest.py Fixtures Required
|
|
114
|
+
|
|
115
|
+
- `session` — async SQLAlchemy session (test DB, auto-rollback)
|
|
116
|
+
- `async_client` — httpx `AsyncClient` unauthenticated
|
|
117
|
+
- `auth_client` — httpx `AsyncClient` with valid Bearer token
|
|
118
|
+
- `user_id` — seeded test user UUID
|
|
119
|
+
- `login_as(user_id: UUID)` — helper that returns AsyncClient with token for specific user
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## Required Integration Tests
|
|
124
|
+
|
|
125
|
+
### Cross-User Isolation Test (CRITICAL — security boundary)
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
# tests/integration/test_cross_user_isolation.py
|
|
129
|
+
import pytest
|
|
130
|
+
from uuid import uuid4
|
|
131
|
+
from httpx import AsyncClient
|
|
132
|
+
|
|
133
|
+
@pytest.mark.asyncio
|
|
134
|
+
async def test_user_cannot_access_other_user_resource(
|
|
135
|
+
session,
|
|
136
|
+
login_as, # fixture from conftest.py
|
|
137
|
+
):
|
|
138
|
+
"""User B attempting to access User A's resource returns 404 (not 403)."""
|
|
139
|
+
user_a_id = uuid4()
|
|
140
|
+
user_b_id = uuid4()
|
|
141
|
+
|
|
142
|
+
# User A creates a resource
|
|
143
|
+
client_a = await login_as(user_a_id)
|
|
144
|
+
response = await client_a.post("/api/v1/applications", json={
|
|
145
|
+
"company": "Acme Corp",
|
|
146
|
+
"role": "Engineer",
|
|
147
|
+
})
|
|
148
|
+
assert response.status_code == 201
|
|
149
|
+
resource_id = response.json()["data"]["id"]
|
|
150
|
+
|
|
151
|
+
# User B attempts to GET User A's resource
|
|
152
|
+
client_b = await login_as(user_b_id)
|
|
153
|
+
response = await client_b.get(f"/api/v1/applications/{resource_id}")
|
|
154
|
+
assert response.status_code == 404 # NOT 403 (don't leak existence)
|
|
155
|
+
assert response.json()["error"]["code"] == "NOT_FOUND"
|
|
156
|
+
|
|
157
|
+
# User B attempts to UPDATE User A's resource
|
|
158
|
+
response = await client_b.patch(f"/api/v1/applications/{resource_id}", json={
|
|
159
|
+
"stage": "INTERVIEWING",
|
|
160
|
+
})
|
|
161
|
+
assert response.status_code == 404
|
|
162
|
+
|
|
163
|
+
# User B attempts to DELETE User A's resource
|
|
164
|
+
response = await client_b.delete(f"/api/v1/applications/{resource_id}")
|
|
165
|
+
assert response.status_code == 404
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
**Why 404 not 403?** Returning 403 leaks the existence of the resource (user can enumerate UUIDs to discover other users' data). 404 reveals nothing.
|
|
169
|
+
|
|
170
|
+
### GDPR Deletion Test
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
# tests/integration/test_gdpr_deletion.py
|
|
174
|
+
import pytest
|
|
175
|
+
from src.utils.gdpr_delete_util import gdpr_delete_user
|
|
176
|
+
|
|
177
|
+
@pytest.mark.asyncio
|
|
178
|
+
async def test_gdpr_delete_anonymizes_pii_keeps_audit(session, user_id):
|
|
179
|
+
"""GDPR deletion anonymizes PII, hard-deletes sensitive rows, keeps audit logs."""
|
|
180
|
+
# Setup: User has applications, payments, audit logs
|
|
181
|
+
# (create test data here)
|
|
182
|
+
|
|
183
|
+
# Execute GDPR deletion
|
|
184
|
+
await gdpr_delete_user(session, user_id)
|
|
185
|
+
await session.commit()
|
|
186
|
+
|
|
187
|
+
# Assertions:
|
|
188
|
+
# 1. User PII anonymized
|
|
189
|
+
user = await session.get(User, user_id)
|
|
190
|
+
assert user.email == f"deleted-{user_id}@gdpr.local"
|
|
191
|
+
assert user.name == "Deleted User"
|
|
192
|
+
assert user.deleted_at is not None
|
|
193
|
+
|
|
194
|
+
# 2. Sensitive data hard-deleted (payments)
|
|
195
|
+
from src.models.payment import Payment
|
|
196
|
+
payments = await session.execute(
|
|
197
|
+
select(Payment).where(Payment.user_id == user_id)
|
|
198
|
+
)
|
|
199
|
+
assert payments.scalars().all() == []
|
|
200
|
+
|
|
201
|
+
# 3. Audit logs KEPT (compliance requirement)
|
|
202
|
+
from src.models.audit_log import AuditLog
|
|
203
|
+
logs = await session.execute(
|
|
204
|
+
select(AuditLog).where(AuditLog.user_id == user_id)
|
|
205
|
+
)
|
|
206
|
+
assert len(logs.scalars().all()) > 0 # audit trail preserved
|
|
207
|
+
```
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"allow": [
|
|
4
|
+
"Bash(uv *)",
|
|
5
|
+
"Bash(python *)",
|
|
6
|
+
"Bash(python3 *)",
|
|
7
|
+
"Bash(pytest *)",
|
|
8
|
+
"Bash(ruff *)",
|
|
9
|
+
"Bash(bandit *)",
|
|
10
|
+
"Bash(mypy *)",
|
|
11
|
+
"Bash(alembic *)",
|
|
12
|
+
"Bash(git *)",
|
|
13
|
+
"Bash(bash *)",
|
|
14
|
+
"Bash(sh *)",
|
|
15
|
+
"Bash(cat *)",
|
|
16
|
+
"Bash(ls *)",
|
|
17
|
+
"Bash(grep *)",
|
|
18
|
+
"Bash(find *)",
|
|
19
|
+
"Bash(wc *)",
|
|
20
|
+
"Bash(k6 *)",
|
|
21
|
+
"Bash(pre-commit *)",
|
|
22
|
+
"Read(//Users/shreyasss15/Projects-Clean/shreyas/voice-guard/harness-template-fastapi/**)",
|
|
23
|
+
"Read(//Users/shreyasss15/Projects-Clean/shreyas/voice-guard/**)",
|
|
24
|
+
"Bash(git add *)"
|
|
25
|
+
],
|
|
26
|
+
"deny": [],
|
|
27
|
+
"additionalDirectories": [
|
|
28
|
+
"/Users/shreyasss15/Projects-Clean/shreyas/harness-template/harness-nextjs/.claude/skills/01-write-spec",
|
|
29
|
+
"/Users/shreyasss15/Projects-Clean/shreyas/harness-template/harness-nextjs/.claude/skills/02-write-plan",
|
|
30
|
+
"/Users/shreyasss15/Projects-Clean/shreyas/harness-template/harness-nextjs/.claude/skills/01-write-roadmap",
|
|
31
|
+
"/Users/shreyasss15/Projects-Clean/shreyas/harness-template/harness-nextjs/docs/product-specs",
|
|
32
|
+
"/Users/shreyasss15/Projects-Clean/shreyas/harness-template/harness-nextjs/.claude",
|
|
33
|
+
"/Users/shreyasss15/Projects-Clean/shreyas/harness-template/harness-nextjs"
|
|
34
|
+
]
|
|
35
|
+
},
|
|
36
|
+
"model": "claude-opus-4-8",
|
|
37
|
+
"hooks": {
|
|
38
|
+
"PostToolUse": [
|
|
39
|
+
{
|
|
40
|
+
"matcher": "Write|Edit|MultiEdit",
|
|
41
|
+
"hooks": [
|
|
42
|
+
{
|
|
43
|
+
"type": "command",
|
|
44
|
+
"command": "bash .claude/hooks/post-write.sh"
|
|
45
|
+
}
|
|
46
|
+
]
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
# /infra-setup — Phase 0 Infrastructure Setup
|
|
2
|
+
|
|
3
|
+
**Verified:** 2026-06-18 · **Staleness threshold:** 60 days
|
|
4
|
+
**Libraries:** FastAPI 0.115, SQLAlchemy 2.0, PyJWT 2.9, pwdlib 0.2, pytest 8, ruff 0.8, mypy 1.13, uv 0.4
|
|
5
|
+
**Sources:** [FastAPI](https://fastapi.tiangolo.com/), [PyJWT](https://pypi.org/project/PyJWT/), [pwdlib](https://pypi.org/project/pwdlib/), [SQLAlchemy](https://www.sqlalchemy.org/), [uv](https://docs.astral.sh/uv/)
|
|
6
|
+
|
|
7
|
+
Triggered by: `/infra-setup`
|
|
8
|
+
Run ONCE when starting a new project. Check `pyproject.toml` exists first — if yes, abort.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Step 1 — Python + uv Init
|
|
13
|
+
```bash
|
|
14
|
+
uv init --python 3.14
|
|
15
|
+
echo "3.14" > .python-version
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Create `pyproject.toml` with all dependencies:
|
|
19
|
+
|
|
20
|
+
```toml
|
|
21
|
+
[project]
|
|
22
|
+
name = "your-service-name"
|
|
23
|
+
version = "0.0.1"
|
|
24
|
+
requires-python = ">=3.14"
|
|
25
|
+
|
|
26
|
+
# PEP 621: dependencies is an ARRAY of PEP 508 strings (NOT a Poetry-style table).
|
|
27
|
+
# Extras go in brackets: "uvicorn[standard]>=0.32". `uv sync` rejects the table form.
|
|
28
|
+
dependencies = [
|
|
29
|
+
"fastapi>=0.115",
|
|
30
|
+
"uvicorn[standard]>=0.32",
|
|
31
|
+
"sqlalchemy[asyncio]>=2.0",
|
|
32
|
+
"alembic>=1.14",
|
|
33
|
+
"asyncpg>=0.30",
|
|
34
|
+
"pydantic>=2.0",
|
|
35
|
+
"pydantic-settings>=2.0",
|
|
36
|
+
"email-validator>=2.0",
|
|
37
|
+
"PyJWT[crypto]>=2.9",
|
|
38
|
+
"pwdlib[argon2]>=0.2",
|
|
39
|
+
"argon2-cffi>=23.0",
|
|
40
|
+
"python-multipart>=0.0.9",
|
|
41
|
+
"celery[redis]>=5.0",
|
|
42
|
+
"redis>=5.0",
|
|
43
|
+
"httpx>=0.28",
|
|
44
|
+
"tenacity>=9.0",
|
|
45
|
+
"structlog>=24.0",
|
|
46
|
+
"opentelemetry-sdk>=1.0",
|
|
47
|
+
"opentelemetry-instrumentation-fastapi>=0.49",
|
|
48
|
+
"opentelemetry-instrumentation-sqlalchemy>=0.49",
|
|
49
|
+
"opentelemetry-exporter-otlp>=1.0",
|
|
50
|
+
"prometheus-fastapi-instrumentator>=7.0",
|
|
51
|
+
"slowapi>=0.1",
|
|
52
|
+
"limits>=3.0",
|
|
53
|
+
"uuid-utils>=0.9",
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
# Dev tooling — PEP 735 dependency group (installed by `uv sync` by default).
|
|
57
|
+
[dependency-groups]
|
|
58
|
+
dev = [
|
|
59
|
+
"pytest>=8.0",
|
|
60
|
+
"pytest-asyncio>=0.24",
|
|
61
|
+
"pytest-cov>=6.0",
|
|
62
|
+
"factory-boy>=3.0",
|
|
63
|
+
"faker>=30.0",
|
|
64
|
+
"hypothesis>=6.0", # property-based tests
|
|
65
|
+
"schemathesis>=3.19,<4", # OpenAPI contract/fuzz tests (v4 moved the from_asgi loader; pin <4)
|
|
66
|
+
"mutmut>=3.0", # mutation testing (run on critical service modules)
|
|
67
|
+
"ruff>=0.8",
|
|
68
|
+
"bandit>=1.7", # full security scan in CI (ruff `S` is a fast subset for pre-commit)
|
|
69
|
+
"mypy>=1.13",
|
|
70
|
+
"pre-commit>=4.0",
|
|
71
|
+
"sqlalchemy[mypy]>=2.0",
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
# Application (not a library) — don't try to build/install the project itself.
|
|
75
|
+
[tool.uv]
|
|
76
|
+
package = false
|
|
77
|
+
|
|
78
|
+
[tool.ruff]
|
|
79
|
+
line-length = 100
|
|
80
|
+
target-version = "py314"
|
|
81
|
+
|
|
82
|
+
[tool.ruff.lint]
|
|
83
|
+
# `S` = flake8-bandit fast subset (pre-commit); full `bandit -ll` runs in CI (see dev-deps).
|
|
84
|
+
select = ["E", "F", "I", "T20", "ANN", "S", "UP"]
|
|
85
|
+
# ANN101/ANN102 were REMOVED from ruff (>=0.2) — do not re-add. S101 = allow assert.
|
|
86
|
+
# UP046/UP047 (PEP 695 generic rewrites) are OFF: `class X(BaseModel, Generic[T])` and
|
|
87
|
+
# `def f(x: T)` stay valid — pydantic supports both styles and the docs use Generic[T].
|
|
88
|
+
# (PEP 695 `class X[T]` is allowed too; it just isn't forced.)
|
|
89
|
+
ignore = ["S101", "UP046", "UP047"]
|
|
90
|
+
|
|
91
|
+
[tool.ruff.lint.per-file-ignores]
|
|
92
|
+
"tests/**" = ["ANN", "S"]
|
|
93
|
+
"src/config/settings.py" = ["S105"]
|
|
94
|
+
|
|
95
|
+
[tool.mypy]
|
|
96
|
+
python_version = "3.14"
|
|
97
|
+
strict = true
|
|
98
|
+
ignore_missing_imports = true
|
|
99
|
+
plugins = ["pydantic.mypy", "sqlalchemy.ext.mypy.plugin"]
|
|
100
|
+
|
|
101
|
+
[tool.pytest.ini_options]
|
|
102
|
+
asyncio_mode = "auto"
|
|
103
|
+
testpaths = ["tests"]
|
|
104
|
+
# REQUIRED: the app is `package = false` (not installed), so `from src... import` in
|
|
105
|
+
# tests raises ModuleNotFoundError without this. (Verified by dogfood: collection failed.)
|
|
106
|
+
pythonpath = ["."]
|
|
107
|
+
# Do NOT put --cov here: a global addopts coverage gate makes EVERY partial run fail
|
|
108
|
+
# (e.g. `pytest tests/architecture/` → 0% coverage → fail). Coverage is invoked
|
|
109
|
+
# explicitly — per-layer by the gate-checker, aggregate in CI. (Verified by dogfood:
|
|
110
|
+
# `pytest tests/architecture/` failed only on the coverage gate, not the tests.)
|
|
111
|
+
addopts = "-ra -q"
|
|
112
|
+
|
|
113
|
+
[tool.coverage.run]
|
|
114
|
+
source = ["src"]
|
|
115
|
+
omit = ["src/runtime/main.py", "src/providers/telemetry.py"]
|
|
116
|
+
|
|
117
|
+
[tool.coverage.report]
|
|
118
|
+
# Global floor = lowest per-layer threshold (providers 70). Per-layer thresholds
|
|
119
|
+
# (utils 100 / services 90 / repo 80 / runtime 75 / providers 70) are enforced by the
|
|
120
|
+
# gate-checker agent. Keep this ≤ the lowest per-layer floor so a full `pytest` run does
|
|
121
|
+
# not fail spuriously when a low-threshold layer drags the aggregate down.
|
|
122
|
+
fail_under = 70
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Step 2 — Install Dependencies
|
|
126
|
+
```bash
|
|
127
|
+
uv sync
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Step 3 — Directory Structure
|
|
131
|
+
```bash
|
|
132
|
+
mkdir -p \
|
|
133
|
+
src/{types,config,models,repo,services,utils} \
|
|
134
|
+
src/runtime/{routers/v1,workers,exception_handlers} \
|
|
135
|
+
src/providers/auth \
|
|
136
|
+
tests/{architecture,unit/{services,repo,utils},integration,contract,load} \
|
|
137
|
+
alembic/versions \
|
|
138
|
+
docs/{product-specs/{draft,ready},exec-plans/{active,completed},design-docs/decisions,generated} \
|
|
139
|
+
infra/monitoring \
|
|
140
|
+
.github/workflows
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Step 4 — Environment Setup
|
|
144
|
+
Create `.env.example`:
|
|
145
|
+
```
|
|
146
|
+
APP_ENV=development
|
|
147
|
+
PORT=8000
|
|
148
|
+
DATABASE_URL=postgresql+asyncpg://app:app@localhost:5432/app
|
|
149
|
+
DATABASE_POOL_SIZE=10
|
|
150
|
+
DATABASE_POOL_MIN=2
|
|
151
|
+
REDIS_URL=redis://localhost:6379
|
|
152
|
+
JWT_SECRET=
|
|
153
|
+
JWT_ACCESS_EXPIRY_SECONDS=900
|
|
154
|
+
JWT_REFRESH_EXPIRY_SECONDS=604800
|
|
155
|
+
CORS_ORIGINS=http://localhost:3000
|
|
156
|
+
LOG_LEVEL=INFO
|
|
157
|
+
SERVICE_NAME=your-service-name
|
|
158
|
+
APP_VERSION=0.0.1
|
|
159
|
+
OTEL_EXPORTER_OTLP_ENDPOINT=
|
|
160
|
+
CELERY_BROKER_URL=redis://localhost:6379/0
|
|
161
|
+
CELERY_RESULT_BACKEND=redis://localhost:6379/0
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Create `src/config/settings.py` — pydantic-settings BaseSettings, `process.exit` equivalent on failure.
|
|
165
|
+
Create `src/config/constants.py` — app-wide constants.
|
|
166
|
+
Create `src/config/timeouts.py` — `DB_QUERY_MS=5000`, `EXTERNAL_API_MS=10000`.
|
|
167
|
+
|
|
168
|
+
## Step 5 — Providers
|
|
169
|
+
Create `src/providers/logger.py` — structlog JSON renderer with redaction.
|
|
170
|
+
Create `src/providers/database.py` — async SQLAlchemy engine + session factory + `get_session` Depends.
|
|
171
|
+
Create `src/providers/redis_client.py` — redis-py async client singleton.
|
|
172
|
+
Create `src/providers/telemetry.py` — OTel SDK init.
|
|
173
|
+
Create `src/providers/auth/jwt.py` — PyJWT sign/verify/decode with RS256 support.
|
|
174
|
+
Create `src/providers/auth/password.py` — pwdlib PasswordHash for argon2id hashing.
|
|
175
|
+
Create `src/providers/auth/middleware.py` — `require_auth` Depends, `AuthContext`.
|
|
176
|
+
- The `Authorization` header MUST be OPTIONAL in the signature (`Header(default=None)`); raise `AuthError` (→401) when missing/invalid. A required `Header(...)` makes FastAPI return **422 + a non-enveloped body** for the common no-token case — wrong status AND breaks the envelope contract. (Verified by dogfood: required Header → 422.)
|
|
177
|
+
Create `src/providers/rate_limit.py` — slowapi limiter with Redis store (3 tiers).
|
|
178
|
+
|
|
179
|
+
## Step 6 — Runtime Base
|
|
180
|
+
Create `src/runtime/exception_handlers.py` — maps DomainError subclasses → HTTPException, emits ErrorEnvelope.
|
|
181
|
+
- ALSO register a `RequestValidationError` handler that returns an **enveloped** 422 (`{ok:false, error:{code:"VALIDATION_ERROR",...}, meta}`). Without it, every 422 returns FastAPI's default `{detail:[...]}`, violating "envelope always".
|
|
182
|
+
Create `src/runtime/middleware.py` — SecurityHeadersMiddleware (7 headers), request ID.
|
|
183
|
+
Create `src/runtime/routers/v1/health.py` — `/v1/health` + `/v1/ready`.
|
|
184
|
+
Create `src/runtime/main.py` — FastAPI app, all middleware, all routers, lifespan handler.
|
|
185
|
+
Create `src/runtime/response_util.py` — `ok()`, `err()` builders (import envelope types from `src.types.common`).
|
|
186
|
+
- MUST live in `runtime/`, NOT `utils/` — `utils` may not import domain types, and `ok()` returns `SuccessEnvelope`. Putting it in utils fails `tests/architecture/test_layers.py::test_utils_has_no_domain_imports`. (Verified by dogfood: it fails.)
|
|
187
|
+
Create `src/utils/cursor_util.py` — base64url encode/decode for opaque cursor pagination.
|
|
188
|
+
- `decode_cursor` MUST catch malformed input (`binascii.Error`, `json.JSONDecodeError`, `ValueError`, `UnicodeDecodeError`) and raise `InvalidCursorError` (→400) — NEVER let a bad `?cursor=` value 500. (Verified by dogfood: schemathesis crashed the list endpoint with `?cursor=null`.)
|
|
189
|
+
Create `src/utils/audit_log_util.py` — audit log writer (who, what, resource, timestamp).
|
|
190
|
+
Create `src/utils/gdpr_delete_util.py` — cascade GDPR deletion (anonymize PII, hard-delete sensitive, keep audit).
|
|
191
|
+
Create `src/utils/uuid_util.py` — `new_id()` → uuid7 (use `uuid_utils.compat.uuid7` → returns stdlib `uuid.UUID`).
|
|
192
|
+
Create `src/utils/retry_util.py` — tenacity retry decorator + timeout wrapper.
|
|
193
|
+
Create `src/types/exceptions.py` — `DomainError` base + subclasses: `NotFoundError` (404), `AuthError` (401), `InvalidCursorError` (400), `ValidationDomainError` (422).
|
|
194
|
+
Create `src/types/common.py` — `PageResult[T]`, `AuthContext`, `Meta`, `SuccessEnvelope[T]`, `ErrorEnvelope`.
|
|
195
|
+
|
|
196
|
+
## Step 7 — Alembic
|
|
197
|
+
```bash
|
|
198
|
+
uv run alembic init alembic
|
|
199
|
+
```
|
|
200
|
+
Update `alembic/env.py` to use async engine + read `DATABASE_URL` from settings.
|
|
201
|
+
Update `alembic.ini` to read URL from settings.
|
|
202
|
+
|
|
203
|
+
## Step 8 — Testing Base
|
|
204
|
+
Create `tests/conftest.py` — async session fixture (auto-rollback), async_client, auth_client, loginAs helper.
|
|
205
|
+
Create `tests/architecture/test_layers.py` — ast-based import graph structural tests.
|
|
206
|
+
Create `tests/integration/test_cross_user_isolation.py` — User A creates resource, User B attempts access (404 not 403).
|
|
207
|
+
Create `tests/integration/test_gdpr_deletion.py` — cascade deletion test (PII anonymized, audit kept).
|
|
208
|
+
Create `tests/contract/test_openapi.py` — schemathesis: load `/openapi.json`, fuzz every endpoint (`schemathesis.from_asgi`).
|
|
209
|
+
- FastAPI 0.115 emits **OpenAPI 3.1**, which schemathesis 3.x only supports experimentally. Enable it in-code: `schemathesis.experimental.OPEN_API_3_1.enable()` (or pass `--experimental=openapi-3.1` to the CLI). Without it: "Open API 3.1.0 ... not fully supported". (Verified by dogfood.)
|
|
210
|
+
- This catches real bugs: fuzzing `?cursor=` with garbage MUST return 400 (`InvalidCursorError`), never 500. (Dogfood found exactly this 500.)
|
|
211
|
+
Create `tests/load/smoke.js` — k6 smoke test scaffold (1 VU, 30s).
|
|
212
|
+
Create `tests/load/stress.js` — k6 stress test (ramping VUs, P95/P99 thresholds).
|
|
213
|
+
|
|
214
|
+
## Step 9 — Makefile + gate script (single-command DX)
|
|
215
|
+
Create `Makefile` with daily-driver targets — every doc command goes through these:
|
|
216
|
+
```make
|
|
217
|
+
.PHONY: bootstrap dev lint format typecheck test gate migrate openapi up down
|
|
218
|
+
bootstrap: ; uv sync && uv run pre-commit install # one-command setup
|
|
219
|
+
dev: ; uv run uvicorn src.runtime.main:app --reload
|
|
220
|
+
lint: ; uv run ruff check src/ tests/
|
|
221
|
+
format: ; uv run ruff format src/ tests/
|
|
222
|
+
typecheck: ; uv run mypy src/
|
|
223
|
+
test: ; uv run pytest
|
|
224
|
+
gate: ; bash scripts/gate.sh # deterministic, same checks as gate-checker agent
|
|
225
|
+
migrate: ; uv run alembic upgrade head
|
|
226
|
+
openapi: ; set -a; [ -f .env ] && . ./.env; set +a; uv run python -c "import json,sys; from src.runtime.main import app; json.dump(app.openapi(), sys.stdout)" > openapi.json
|
|
227
|
+
up: ; docker compose up -d
|
|
228
|
+
down: ; docker compose down
|
|
229
|
+
```
|
|
230
|
+
Create `scripts/gate.sh` — runnable mirror of the gate-checker checks (file-size, print, os.environ, ruff, mypy, bandit, architecture tests, coverage). Exits non-zero on any failure so pre-commit and humans can run the SAME gate the agent runs.
|
|
231
|
+
|
|
232
|
+
## Step 10 — Docker
|
|
233
|
+
Create `Dockerfile` — multi-stage (deps → runner), non-root user, HEALTHCHECK.
|
|
234
|
+
Create `.dockerignore` — exclude `.git`, `.venv`, `__pycache__`, `.env*`, `tests/`, `docs/`, `*.md`, `.github/` (prevents secret leaks + bloated build context).
|
|
235
|
+
Create `docker-compose.yml` — app + postgres:16 + redis:7 + pgbouncer (commented out).
|
|
236
|
+
Create `infra/monitoring/docker-compose.lgtm.yml` — local LGTM stack (Grafana + Loki + Tempo + Prometheus) wired to the app's OTLP endpoint, so "observability is not optional" is true locally too.
|
|
237
|
+
|
|
238
|
+
## Step 11 — GitHub Actions + supply chain
|
|
239
|
+
Create `.github/workflows/ci.yml` with INDEPENDENT parallel jobs (security must not be gated behind lint):
|
|
240
|
+
- `quality`: ruff check + ruff format --check + mypy
|
|
241
|
+
- `test`: `pytest --cov=src --cov-report=term-missing --cov-fail-under=70` (coverage is explicit here — NOT in addopts) + schemathesis contract tests (`--experimental=openapi-3.1`)
|
|
242
|
+
- `security`: bandit -ll + pip-audit + gitleaks (secret scan) — runs even if `quality` fails
|
|
243
|
+
- `image`: build Dockerfile → Trivy scan (fail on HIGH/CRITICAL) → generate SBOM (Syft, CycloneDX) → upload artifact
|
|
244
|
+
Use `actions/cache` / `astral-sh/setup-uv` cache. Pin action SHAs.
|
|
245
|
+
Create `.github/workflows/load-test.yml` — k6 smoke on PR against ephemeral env + on staging deploy (P95/error budgets fail the job, not just report).
|
|
246
|
+
Create `.github/dependabot.yml` — weekly `uv`/pip + GitHub Actions + Docker update PRs.
|
|
247
|
+
Create `.gitleaks.toml` and add `gitleaks` + `trivy` notes; wire gitleaks as a pre-commit hook too (Step 12).
|
|
248
|
+
|
|
249
|
+
## Step 12 — ruff + pre-commit
|
|
250
|
+
Create `ruff.toml` (or in pyproject.toml — already done in Step 1).
|
|
251
|
+
Create `.pre-commit-config.yaml` — ruff-pre-commit + mypy + gitleaks + `bash scripts/gate.sh` (so the gate runs locally before every commit).
|
|
252
|
+
```bash
|
|
253
|
+
uv run pre-commit install
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
## Step 13 — Scaffold Docs
|
|
257
|
+
Write `ADR-000-infrastructure.md` with today's date. Include sections:
|
|
258
|
+
- **Auth Libraries**: PyJWT (JWT), pwdlib (password hashing)
|
|
259
|
+
- Reasoning: python-jose abandoned (last commit 2023, CVEs), passlib broken on Python 3.13+
|
|
260
|
+
- PyJWT: FastAPI official recommendation, actively maintained
|
|
261
|
+
- pwdlib: created by FastAPI Users maintainer, Python 3.13+ compatible
|
|
262
|
+
- Alternatives considered: python-jose (rejected: abandoned), passlib (rejected: broken), bcrypt directly (rejected: argon2id is OWASP recommended)
|
|
263
|
+
- **Task Queue**: Celery vs arq evaluation
|
|
264
|
+
- Celery: sync-first, beat scheduling, chord/chain primitives, larger ecosystem
|
|
265
|
+
- arq: async-native (by Pydantic team), Redis-backed, type-safe, simpler API
|
|
266
|
+
- Current choice: Celery (mature, feature-complete)
|
|
267
|
+
- Trade-off: async tasks require asyncio.run() wrapper in Celery
|
|
268
|
+
- Revisit: if beat scheduling or chord/chain not needed, consider arq for async-first simplicity
|
|
269
|
+
- **OpenAPI → Frontend Sync**: GET /openapi.json feeds frontend /api-sync command
|
|
270
|
+
- Workflow: run backend → copy /openapi.json → run /api-sync in frontend repo
|
|
271
|
+
- **Version Bounds**: `>=` strategy for all dependencies (no pinning)
|
|
272
|
+
- Lock file (uv.lock) pins transitive deps automatically
|
|
273
|
+
- Upper bounds cause dependency hell in libraries
|
|
274
|
+
- Security updates apply immediately via `uv sync --upgrade`
|
|
275
|
+
|
|
276
|
+
Write `docs/design-docs/index.md`, `decisions/index.md`.
|
|
277
|
+
Write `docs/product-specs/index.md`.
|
|
278
|
+
Write `docs/exec-plans/tech-debt-tracker.md`.
|
|
279
|
+
|
|
280
|
+
## Gate Check
|
|
281
|
+
```bash
|
|
282
|
+
make gate # deterministic — runs the same checks as the gate-checker agent
|
|
283
|
+
# (equivalently:)
|
|
284
|
+
uv run ruff check src/
|
|
285
|
+
uv run bandit -r src/ -ll
|
|
286
|
+
uv run mypy src/
|
|
287
|
+
uv run pytest tests/architecture/ -v
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
## Commit
|
|
291
|
+
```bash
|
|
292
|
+
git add -A
|
|
293
|
+
git commit -m "chore(infra): phase 0 infrastructure setup
|
|
294
|
+
|
|
295
|
+
- FastAPI 0.115 + Python 3.14 + SQLAlchemy 2 (async)
|
|
296
|
+
- pydantic-settings validated config
|
|
297
|
+
- structlog JSON logging with redaction
|
|
298
|
+
- OpenTelemetry + prometheus-fastapi-instrumentator
|
|
299
|
+
- Celery + Redis queue
|
|
300
|
+
- slowapi rate limiting (Redis store)
|
|
301
|
+
- Health checks: /v1/health + /v1/ready
|
|
302
|
+
- Alembic migrations configured
|
|
303
|
+
- Dockerfile multi-stage, non-root user + .dockerignore
|
|
304
|
+
- docker-compose: app + postgres:16 + redis:7; local LGTM observability stack
|
|
305
|
+
- Makefile (bootstrap/dev/lint/test/gate/migrate/openapi) + scripts/gate.sh
|
|
306
|
+
- CI: parallel quality/test/security/image jobs (ruff, mypy, pytest, schemathesis, bandit, pip-audit, gitleaks, Trivy, SBOM)
|
|
307
|
+
- Dependabot (uv + actions + docker)
|
|
308
|
+
- Architecture structural tests (AST-based)
|
|
309
|
+
- ADR-000: infrastructure decisions"
|
|
310
|
+
git push origin main
|
|
311
|
+
```
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# /write-roadmap — Decompose a Product into an Ordered Spec Roadmap
|
|
2
|
+
|
|
3
|
+
**Verified:** 2026-06-19 · **Staleness threshold:** 60 days
|
|
4
|
+
**Libraries:** harness toolchain (uv, ruff, mypy, pytest) — process skill, no external library pins
|
|
5
|
+
|
|
6
|
+
Triggered by: `/write-roadmap` (optional arg: path to a brief file; else ask the human)
|
|
7
|
+
|
|
8
|
+
This is the altitude ABOVE specs. A real product is many specs, ordered by dependency — this
|
|
9
|
+
skill plans that SET. It writes ONLY `docs/product-specs/ROADMAP.md`; it NEVER writes spec files
|
|
10
|
+
(that is `/write-spec`'s job, which owns spec numbering).
|
|
11
|
+
|
|
12
|
+
## Step 1 — Get the Product Brief
|
|
13
|
+
If invoked with a file path argument, read that file. Otherwise ask the human:
|
|
14
|
+
"Paste the product brief — what we're building, for whom, and the core capabilities."
|
|
15
|
+
Do NOT proceed without a brief.
|
|
16
|
+
|
|
17
|
+
## Step 2 — Check for an Existing Roadmap (refresh, don't clobber)
|
|
18
|
+
```bash
|
|
19
|
+
ls docs/product-specs/ROADMAP.md 2>/dev/null && echo "EXISTS — refresh: keep created specs"
|
|
20
|
+
ls docs/product-specs/{draft,ready}/ 2>/dev/null | grep "SPEC-" | sort
|
|
21
|
+
```
|
|
22
|
+
If a roadmap exists you are REFRESHING: preserve every row that already has a real Spec ID and
|
|
23
|
+
its Status; only add/reorder `NOT_STARTED` rows. Never renumber a created spec.
|
|
24
|
+
|
|
25
|
+
## Step 3 — Identify Bounded Contexts (Epics)
|
|
26
|
+
From the brief, list the distinct bounded contexts — each owns a cohesive set of entities and
|
|
27
|
+
rules (e.g. Identity, Billing, Catalog). Only the contexts the brief implies; no speculative ones.
|
|
28
|
+
|
|
29
|
+
## Step 4 — Propose an Ordered Spec List with Dependencies
|
|
30
|
+
Break each epic into shippable specs (one feature each — the `/write-spec` altitude). For each
|
|
31
|
+
spec record: name, epic, and which specs it depends on (an entity / auth / contract another spec
|
|
32
|
+
must establish first). Order the whole list topologically — every spec appears after its
|
|
33
|
+
dependencies. Keep each spec small enough to become ONE execution plan (split if not).
|
|
34
|
+
|
|
35
|
+
## Step 5 — Pick the Walking-Skeleton First Slice
|
|
36
|
+
Choose the single thinnest spec that exercises the system end-to-end — typically auth + one
|
|
37
|
+
entity + one persisted endpoint with a test — and that depends on nothing. Mark exactly one spec
|
|
38
|
+
as the skeleton. This is what the human builds first to prove every layer's path works.
|
|
39
|
+
|
|
40
|
+
## Step 6 — Write / Refresh the Roadmap
|
|
41
|
+
Save to `docs/product-specs/ROADMAP.md` using the template already in that file's header
|
|
42
|
+
(self-documenting). Fill: Bounded Contexts, the dependency-ordered Spec Roadmap table, and the
|
|
43
|
+
Build Sequence Rationale.
|
|
44
|
+
- New specs get Spec ID `—` and Status `NOT_STARTED`.
|
|
45
|
+
- On refresh, keep existing Spec IDs and Statuses untouched; never renumber a created spec.
|
|
46
|
+
- Set the **Walking skeleton** header to the chosen first slice.
|
|
47
|
+
|
|
48
|
+
## Step 7 — Tell the Human
|
|
49
|
+
```
|
|
50
|
+
Roadmap written: docs/product-specs/ROADMAP.md
|
|
51
|
+
{N} specs across {M} epics, dependency-ordered.
|
|
52
|
+
Walking skeleton (build first): {skeleton spec name}
|
|
53
|
+
|
|
54
|
+
Review the roadmap. When the order looks right:
|
|
55
|
+
1. Run /write-spec — it reads ROADMAP.md and authors the next NOT_STARTED spec
|
|
56
|
+
(starting with the walking skeleton), pre-filling its epic + dependencies.
|
|
57
|
+
2. Repeat /write-spec down the list as you go.
|
|
58
|
+
|
|
59
|
+
/write-roadmap does NOT write spec files. It only plans the set. Each spec is authored
|
|
60
|
+
individually by /write-spec, now guided by this roadmap.
|
|
61
|
+
```
|