feat(spec): update stack required and jwt auth template (#236)

* feat(spec): update STACK_REQUIRED for svelte and quality tools

* fix(templates): align jwt auth with issue contracts

* docs(spec): update template c stack and migration note

* fix(ci): format tests/test_spec_status.py

---------

Co-authored-by: opencode-agent <agent@opencode.local>
This commit is contained in:
Sergey 2026-08-03 17:21:43 +03:00 committed by GitHub
parent 35b6c6e542
commit 51a5300423
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 36 additions and 22 deletions

View file

@ -70,12 +70,22 @@ PHASE_FILES: dict[int, str] = {
VALID_TYPES = {"backend", "fullstack", "mcp-server", "cli", "bot", "worker"} VALID_TYPES = {"backend", "fullstack", "mcp-server", "cli", "bot", "worker"}
STACK_REQUIRED: dict[str, list[str]] = { STACK_REQUIRED: dict[str, list[str]] = {
"backend": ["fastapi", "tortoise", "uv", "pytest", "ruff", "mypy"], "backend": ["fastapi", "tortoise", "uv", "pytest", "ruff", "mypy", "loguru", "pydantic"],
"fullstack": ["fastapi", "tortoise", "react", "vite", "biome", "uv"], "fullstack": [
"fastapi",
"tortoise",
"svelte",
"sveltekit",
"biome",
"uv",
"ruff",
"mypy",
"pytest",
],
"mcp-server": ["fastapi", "mcp", "patchright", "uv"], "mcp-server": ["fastapi", "mcp", "patchright", "uv"],
"cli": ["typer", "uv", "hatchling"], "cli": ["typer", "uv", "hatchling", "ruff", "mypy", "pytest"],
"bot": ["aiogram", "fastapi", "uv"], "bot": ["aiogram", "fastapi", "uv", "ruff", "mypy", "pytest"],
"worker": ["prefect", "uv"], "worker": ["prefect", "uv", "ruff", "mypy", "pytest"],
} }
PHASE_NAMES = [ PHASE_NAMES = [

View file

@ -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. Общий для всех типов: 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 - **backend**: FastAPI + uvicorn, Tortoise ORM (встроенные миграции `tortoise makemigrations`, НЕ Aerich — legacy), Pydantic v2 + pydantic-settings, Loguru, опц. JWT-auth (`passlib[bcrypt]` + `pyjwt`)
- **fullstack**: backend + frontend/ (React 19 + Vite + Biome + TS strict + Vitest + Knip + happy-dom) - **fullstack**: backend + frontend/ (SvelteKit + Svelte 5 runes (JS) + Biome + Vitest + Knip)
- **mcp-server**: FastAPI + MCP SDK, Patchright/Playwright over CDP, X-API-Key - **mcp-server**: FastAPI + MCP SDK, Patchright/Playwright over CDP, X-API-Key
- **cli**: Typer (default) / click / argparse, hatchling build - **cli**: Typer (default) / click / argparse, hatchling build
- **bot**: aiogram 3.x, FastAPI webhook/polling, Tortoise (опц.), Pydantic AI (опц.) - **bot**: aiogram 3.x, FastAPI webhook/polling, Tortoise (опц.), Pydantic AI (опц.)
- **worker**: Prefect flows + tasks, prefect.yaml, docker-compose worker profile - **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 фаз ## 9 фаз
### Phase 0: DETECT (subagent, без вопроса юзеру) ### Phase 0: DETECT (subagent, без вопроса юзеру)
@ -61,7 +65,7 @@ Prompt template A (см. ниже).
``` ```
Выбери тип проекта: Выбери тип проекта:
[1] backend — FastAPI + Tortoise, REST API, без frontend [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) [3] mcp-server — MCP + REST сервер (Patchright/Playwright over CDP)
[4] cli — Python CLI tool (Typer) [4] cli — Python CLI tool (Typer)
[5] bot — Telegram bot (aiogram 3) [5] bot — Telegram bot (aiogram 3)
@ -84,7 +88,7 @@ backend:
- Auth: [1] none v1 / [2] JWT / [3] X-API-Key - Auth: [1] none v1 / [2] JWT / [3] X-API-Key
fullstack: fullstack:
- frontend: [1] React 19 (default) / [2] SvelteKit / [3] add later - frontend: [1] SvelteKit + Svelte 5 (default) / [2] add later
- DB: (same as backend) - DB: (same as backend)
- Auth: (same as backend) - Auth: (same as backend)
@ -231,8 +235,8 @@ Spec complete. Issues: #N1, #N2, ...
Тип проекта: <type> (из frontmatter meta.md). Тип проекта: <type> (из frontmatter meta.md).
Default stack для типа (хардкод, добавить всегда): Default stack для типа (хардкод, добавить всегда):
- Общий: Python 3.12+, uv, hatchling, ruff, mypy strict, pytest 90% cov, xenon, pre-commit, .editorconfig, .gitignore, LICENSE MIT, dependabot, CI - Общий: 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 - backend: FastAPI + uvicorn, Tortoise ORM (встроенные миграции `tortoise makemigrations`, НЕ Aerich), Pydantic v2 + pydantic-settings, Loguru, опц. JWT-auth (`passlib[bcrypt]` + `pyjwt`)
- fullstack: + frontend/ (React 19 + Vite + Biome + TS strict + Vitest + Knip + happy-dom) - fullstack: + frontend/ (SvelteKit + Svelte 5 runes (JS) + Biome + Vitest + Knip)
- mcp-server: FastAPI + MCP SDK, Patchright/Playwright over CDP, X-API-Key - mcp-server: FastAPI + MCP SDK, Patchright/Playwright over CDP, X-API-Key
- cli: Typer (default) / click / argparse, hatchling build - cli: Typer (default) / click / argparse, hatchling build
- bot: aiogram 3.x, FastAPI webhook/polling, Tortoise (опц.), Pydantic AI (опц.) - bot: aiogram 3.x, FastAPI webhook/polling, Tortoise (опц.), Pydantic AI (опц.)

View file

@ -45,5 +45,5 @@ async def get_current_user(
Returns the username/subject of the token. Only present when Returns the username/subject of the token. Only present when
``use_auth == "yes"``. ``use_auth == "yes"``.
""" """
return await auth.get_current_user(credentials.credentials) return await auth.decode_access_token(credentials.credentials)
{% endif %} {% endif %}

View file

@ -15,7 +15,7 @@ class User(Model):
username = fields.CharField(max_length=64, unique=True) username = fields.CharField(max_length=64, unique=True)
email = fields.CharField(max_length=128, unique=True) email = fields.CharField(max_length=128, unique=True)
{% if cookiecutter.use_auth == "yes" %} {% if cookiecutter.use_auth == "yes" %}
hashed_password = fields.CharField(max_length=128) hashed_password = fields.CharField(max_length=255)
{% endif %} {% endif %}
class Meta: class Meta:

View file

@ -30,7 +30,7 @@ class AuthService:
return _pwd.verify(plain, hashed) return _pwd.verify(plain, hashed)
@staticmethod @staticmethod
def create_token(username: str) -> str: def create_access_token(username: str) -> str:
expire = datetime.now(timezone.utc) + timedelta( expire = datetime.now(timezone.utc) + timedelta(
minutes=settings.jwt.expire_minutes, minutes=settings.jwt.expire_minutes,
) )
@ -52,10 +52,10 @@ class AuthService:
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid credentials", detail="Invalid credentials",
) )
return AuthService.create_token(username) return AuthService.create_access_token(username)
@staticmethod @staticmethod
async def get_current_user(token: str) -> str: async def decode_access_token(token: str) -> str:
try: try:
payload = jwt.decode( payload = jwt.decode(
token, settings.jwt.secret, algorithms=[settings.jwt.algorithm], token, settings.jwt.secret, algorithms=[settings.jwt.algorithm],

View file

@ -45,5 +45,5 @@ async def get_current_user(
Returns the username/subject of the token. Only present when Returns the username/subject of the token. Only present when
``use_auth == "yes"``. ``use_auth == "yes"``.
""" """
return await auth.get_current_user(credentials.credentials) return await auth.decode_access_token(credentials.credentials)
{% endif %} {% endif %}

View file

@ -15,7 +15,7 @@ class User(Model):
username = fields.CharField(max_length=64, unique=True) username = fields.CharField(max_length=64, unique=True)
email = fields.CharField(max_length=128, unique=True) email = fields.CharField(max_length=128, unique=True)
{% if cookiecutter.use_auth == "yes" %} {% if cookiecutter.use_auth == "yes" %}
hashed_password = fields.CharField(max_length=128) hashed_password = fields.CharField(max_length=255)
{% endif %} {% endif %}
class Meta: class Meta:

View file

@ -30,7 +30,7 @@ class AuthService:
return _pwd.verify(plain, hashed) return _pwd.verify(plain, hashed)
@staticmethod @staticmethod
def create_token(username: str) -> str: def create_access_token(username: str) -> str:
expire = datetime.now(timezone.utc) + timedelta( expire = datetime.now(timezone.utc) + timedelta(
minutes=settings.jwt.expire_minutes, minutes=settings.jwt.expire_minutes,
) )
@ -52,10 +52,10 @@ class AuthService:
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid credentials", detail="Invalid credentials",
) )
return AuthService.create_token(username) return AuthService.create_access_token(username)
@staticmethod @staticmethod
async def get_current_user(token: str) -> str: async def decode_access_token(token: str) -> str:
try: try:
payload = jwt.decode( payload = jwt.decode(
token, settings.jwt.secret, algorithms=[settings.jwt.algorithm], token, settings.jwt.secret, algorithms=[settings.jwt.algorithm],

View file

@ -515,7 +515,7 @@ def test_check_stack_case_insensitive(monkeypatch, tmp_path):
spec_dir = _set_spec_dir(monkeypatch, tmp_path) spec_dir = _set_spec_dir(monkeypatch, tmp_path)
_write_meta(spec_dir, project="foo", type="backend") _write_meta(spec_dir, project="foo", type="backend")
(spec_dir / "stack.md").write_text( (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() result = ss.check_stack()
assert result.status == ss.PhaseStatus.DONE assert result.status == ss.PhaseStatus.DONE