diff --git a/.opencode/scripts/spec-status.py b/.opencode/scripts/spec-status.py index cbfe03c..1f17c70 100644 --- a/.opencode/scripts/spec-status.py +++ b/.opencode/scripts/spec-status.py @@ -70,12 +70,22 @@ PHASE_FILES: dict[int, str] = { VALID_TYPES = {"backend", "fullstack", "mcp-server", "cli", "bot", "worker"} STACK_REQUIRED: dict[str, list[str]] = { - "backend": ["fastapi", "tortoise", "uv", "pytest", "ruff", "mypy"], - "fullstack": ["fastapi", "tortoise", "react", "vite", "biome", "uv"], + "backend": ["fastapi", "tortoise", "uv", "pytest", "ruff", "mypy", "loguru", "pydantic"], + "fullstack": [ + "fastapi", + "tortoise", + "svelte", + "sveltekit", + "biome", + "uv", + "ruff", + "mypy", + "pytest", + ], "mcp-server": ["fastapi", "mcp", "patchright", "uv"], - "cli": ["typer", "uv", "hatchling"], - "bot": ["aiogram", "fastapi", "uv"], - "worker": ["prefect", "uv"], + "cli": ["typer", "uv", "hatchling", "ruff", "mypy", "pytest"], + "bot": ["aiogram", "fastapi", "uv", "ruff", "mypy", "pytest"], + "worker": ["prefect", "uv", "ruff", "mypy", "pytest"], } PHASE_NAMES = [ diff --git a/.opencode/skills/spec/SKILL.md b/.opencode/skills/spec/SKILL.md index 742b10f..ccc3707 100644 --- a/.opencode/skills/spec/SKILL.md +++ b/.opencode/skills/spec/SKILL.md @@ -41,13 +41,17 @@ truth для порядка и действий — `spec-status` tool. На в Общий для всех типов: Python 3.12+, uv, hatchling, ruff, mypy strict, pytest 90% cov, xenon, pre-commit, .editorconfig, .gitignore, LICENSE MIT, dependabot, CI. -- **backend**: FastAPI + uvicorn, Tortoise ORM + Aerich, pydantic-settings, Loguru -- **fullstack**: backend + frontend/ (React 19 + Vite + Biome + TS strict + Vitest + Knip + happy-dom) +- **backend**: FastAPI + uvicorn, Tortoise ORM (встроенные миграции `tortoise makemigrations`, НЕ Aerich — legacy), Pydantic v2 + pydantic-settings, Loguru, опц. JWT-auth (`passlib[bcrypt]` + `pyjwt`) +- **fullstack**: backend + frontend/ (SvelteKit + Svelte 5 runes (JS) + Biome + Vitest + Knip) - **mcp-server**: FastAPI + MCP SDK, Patchright/Playwright over CDP, X-API-Key - **cli**: Typer (default) / click / argparse, hatchling build - **bot**: aiogram 3.x, FastAPI webhook/polling, Tortoise (опц.), Pydantic AI (опц.) - **worker**: Prefect flows + tasks, prefect.yaml, docker-compose worker profile +### Миграционная заметка (stack.md) + +Если существующий `docs/spec/stack.md` содержит `react` или `aerich` (устаревшие значения до issue #231), spec-status пометит Phase 2 как NOT_DONE (`STACK_REQUIRED` теперь требует `svelte`/`sveltekit` для fullstack и не требует `aerich`). Обнови stack.md: замени `react` → `svelte`/`sveltekit`, удали `aerich` (Tortoise 1.0+ имеет встроенные миграции `tortoise makemigrations`). + ## 9 фаз ### Phase 0: DETECT (subagent, без вопроса юзеру) @@ -61,7 +65,7 @@ Prompt template A (см. ниже). ``` Выбери тип проекта: [1] backend — FastAPI + Tortoise, REST API, без frontend -[2] fullstack — backend + React 19/Vite dashboard (monorepo) +[2] fullstack — backend + SvelteKit/Svelte 5 dashboard (monorepo) [3] mcp-server — MCP + REST сервер (Patchright/Playwright over CDP) [4] cli — Python CLI tool (Typer) [5] bot — Telegram bot (aiogram 3) @@ -84,7 +88,7 @@ backend: - Auth: [1] none v1 / [2] JWT / [3] X-API-Key fullstack: -- frontend: [1] React 19 (default) / [2] SvelteKit / [3] add later +- frontend: [1] SvelteKit + Svelte 5 (default) / [2] add later - DB: (same as backend) - Auth: (same as backend) @@ -231,8 +235,8 @@ Spec complete. Issues: #N1, #N2, ... Тип проекта: (из frontmatter meta.md). Default stack для типа (хардкод, добавить всегда): - Общий: Python 3.12+, uv, hatchling, ruff, mypy strict, pytest 90% cov, xenon, pre-commit, .editorconfig, .gitignore, LICENSE MIT, dependabot, CI -- backend: FastAPI + uvicorn, Tortoise ORM + Aerich, pydantic-settings, Loguru -- fullstack: + frontend/ (React 19 + Vite + Biome + TS strict + Vitest + Knip + happy-dom) +- backend: FastAPI + uvicorn, Tortoise ORM (встроенные миграции `tortoise makemigrations`, НЕ Aerich), Pydantic v2 + pydantic-settings, Loguru, опц. JWT-auth (`passlib[bcrypt]` + `pyjwt`) +- fullstack: + frontend/ (SvelteKit + Svelte 5 runes (JS) + Biome + Vitest + Knip) - mcp-server: FastAPI + MCP SDK, Patchright/Playwright over CDP, X-API-Key - cli: Typer (default) / click / argparse, hatchling build - bot: aiogram 3.x, FastAPI webhook/polling, Tortoise (опц.), Pydantic AI (опц.) diff --git a/.opencode/templates/backend/{{cookiecutter.project_name}}/src/{{cookiecutter.project_name}}/api/v1/dependencies.py b/.opencode/templates/backend/{{cookiecutter.project_name}}/src/{{cookiecutter.project_name}}/api/v1/dependencies.py index 84b7a8d..54d7283 100644 --- a/.opencode/templates/backend/{{cookiecutter.project_name}}/src/{{cookiecutter.project_name}}/api/v1/dependencies.py +++ b/.opencode/templates/backend/{{cookiecutter.project_name}}/src/{{cookiecutter.project_name}}/api/v1/dependencies.py @@ -45,5 +45,5 @@ async def get_current_user( Returns the username/subject of the token. Only present when ``use_auth == "yes"``. """ - return await auth.get_current_user(credentials.credentials) + return await auth.decode_access_token(credentials.credentials) {% endif %} \ No newline at end of file diff --git a/.opencode/templates/backend/{{cookiecutter.project_name}}/src/{{cookiecutter.project_name}}/db/models/user.py b/.opencode/templates/backend/{{cookiecutter.project_name}}/src/{{cookiecutter.project_name}}/db/models/user.py index 4e095c1..e09d972 100644 --- a/.opencode/templates/backend/{{cookiecutter.project_name}}/src/{{cookiecutter.project_name}}/db/models/user.py +++ b/.opencode/templates/backend/{{cookiecutter.project_name}}/src/{{cookiecutter.project_name}}/db/models/user.py @@ -15,7 +15,7 @@ class User(Model): username = fields.CharField(max_length=64, unique=True) email = fields.CharField(max_length=128, unique=True) {% if cookiecutter.use_auth == "yes" %} - hashed_password = fields.CharField(max_length=128) + hashed_password = fields.CharField(max_length=255) {% endif %} class Meta: diff --git a/.opencode/templates/backend/{{cookiecutter.project_name}}/src/{{cookiecutter.project_name}}/services/auth_service.py b/.opencode/templates/backend/{{cookiecutter.project_name}}/src/{{cookiecutter.project_name}}/services/auth_service.py index e56d77c..509b327 100644 --- a/.opencode/templates/backend/{{cookiecutter.project_name}}/src/{{cookiecutter.project_name}}/services/auth_service.py +++ b/.opencode/templates/backend/{{cookiecutter.project_name}}/src/{{cookiecutter.project_name}}/services/auth_service.py @@ -30,7 +30,7 @@ class AuthService: return _pwd.verify(plain, hashed) @staticmethod - def create_token(username: str) -> str: + def create_access_token(username: str) -> str: expire = datetime.now(timezone.utc) + timedelta( minutes=settings.jwt.expire_minutes, ) @@ -52,10 +52,10 @@ class AuthService: status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials", ) - return AuthService.create_token(username) + return AuthService.create_access_token(username) @staticmethod - async def get_current_user(token: str) -> str: + async def decode_access_token(token: str) -> str: try: payload = jwt.decode( token, settings.jwt.secret, algorithms=[settings.jwt.algorithm], diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/backend/src/{{cookiecutter.project_name}}/api/v1/dependencies.py b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/backend/src/{{cookiecutter.project_name}}/api/v1/dependencies.py index 84b7a8d..54d7283 100644 --- a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/backend/src/{{cookiecutter.project_name}}/api/v1/dependencies.py +++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/backend/src/{{cookiecutter.project_name}}/api/v1/dependencies.py @@ -45,5 +45,5 @@ async def get_current_user( Returns the username/subject of the token. Only present when ``use_auth == "yes"``. """ - return await auth.get_current_user(credentials.credentials) + return await auth.decode_access_token(credentials.credentials) {% endif %} \ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/backend/src/{{cookiecutter.project_name}}/db/models/user.py b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/backend/src/{{cookiecutter.project_name}}/db/models/user.py index 4e095c1..e09d972 100644 --- a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/backend/src/{{cookiecutter.project_name}}/db/models/user.py +++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/backend/src/{{cookiecutter.project_name}}/db/models/user.py @@ -15,7 +15,7 @@ class User(Model): username = fields.CharField(max_length=64, unique=True) email = fields.CharField(max_length=128, unique=True) {% if cookiecutter.use_auth == "yes" %} - hashed_password = fields.CharField(max_length=128) + hashed_password = fields.CharField(max_length=255) {% endif %} class Meta: diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/backend/src/{{cookiecutter.project_name}}/services/auth_service.py b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/backend/src/{{cookiecutter.project_name}}/services/auth_service.py index e56d77c..509b327 100644 --- a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/backend/src/{{cookiecutter.project_name}}/services/auth_service.py +++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/backend/src/{{cookiecutter.project_name}}/services/auth_service.py @@ -30,7 +30,7 @@ class AuthService: return _pwd.verify(plain, hashed) @staticmethod - def create_token(username: str) -> str: + def create_access_token(username: str) -> str: expire = datetime.now(timezone.utc) + timedelta( minutes=settings.jwt.expire_minutes, ) @@ -52,10 +52,10 @@ class AuthService: status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials", ) - return AuthService.create_token(username) + return AuthService.create_access_token(username) @staticmethod - async def get_current_user(token: str) -> str: + async def decode_access_token(token: str) -> str: try: payload = jwt.decode( token, settings.jwt.secret, algorithms=[settings.jwt.algorithm], diff --git a/tests/test_spec_status.py b/tests/test_spec_status.py index dfb5712..3754cfc 100644 --- a/tests/test_spec_status.py +++ b/tests/test_spec_status.py @@ -515,7 +515,7 @@ def test_check_stack_case_insensitive(monkeypatch, tmp_path): spec_dir = _set_spec_dir(monkeypatch, tmp_path) _write_meta(spec_dir, project="foo", type="backend") (spec_dir / "stack.md").write_text( - "- FastAPI\n- Tortoise ORM\n- UV\n- Pytest\n- Ruff\n- Mypy\n" + "- FastAPI\n- Tortoise ORM\n- UV\n- Pytest\n- Ruff\n- Mypy\n- Loguru\n- Pydantic\n" ) result = ss.check_stack() assert result.status == ss.PhaseStatus.DONE