docs: add full project documentation

- Architecture: overview, 7 ADR, tech stack
- Principles: code-style, git-workflow, security
- API contracts: auth, profile, tests, admin endpoints
- Database schema: tables, relationships, indexes
- LLM strategy: prompts, fallback, validation, Qwen 2.5 14B
- Onboarding: setup, Docker, .env template
- Progress: roadmap, changelog
- Agents: context, backend instructions

Made-with: Cursor
This commit is contained in:
Anton
2026-03-04 12:07:17 +03:00
commit 99cd8ae727
21 changed files with 3763 additions and 0 deletions

229
agents/backend.md Normal file
View File

@@ -0,0 +1,229 @@
# Инструкции для AI-агента: Backend
Документ для агентов при создании и доработке `samreshu-backend`. Общий контекст проекта — в [context.md](context.md).
## Ссылки на документацию
| Документ | Что взять |
| - | - |
| [api/contracts.md](../api/contracts.md) | Все endpoints, request/response, коды ошибок |
| [database/schema.md](../database/schema.md) | Таблицы, колонки, связи, индексы |
| [principles/security.md](../principles/security.md) | Auth (argon2, JWT, refresh), rate limiting, Helmet, CORS |
| [llm/strategy.md](../llm/strategy.md) | Промпты, JSON Schema валидации, fallback, LlmService интерфейс |
| [principles/code-style.md](../principles/code-style.md) | ESLint, Prettier, TS config, Vitest |
| [onboarding/setup.md](../onboarding/setup.md) | Docker Compose, .env шаблон |
---
## Структура репозитория
```text
samreshu-backend/
├── src/
│ ├── index.ts # Entry point
│ ├── app.ts # Fastify app, регистрация плагинов
│ ├── config/
│ │ └── env.ts # Валидация и экспорт env переменных
│ ├── db/
│ │ ├── index.ts # Drizzle client, экспорт drizzle
│ │ ├── schema/ # Drizzle schema (users, sessions, tests, ...)
│ │ └── migrations/ # SQL миграции (drizzle-kit)
│ ├── plugins/
│ │ ├── database.ts # Подключение к PostgreSQL
│ │ ├── redis.ts # Подключение к Redis
│ │ ├── auth.ts # JWT verify, decode, refreshToken logic
│ │ ├── subscription.ts # Загрузка user.plan из subscriptions
│ │ ├── rateLimit.ts # @fastify/rate-limit + Redis
│ │ ├── helmet.ts # @fastify/helmet
│ │ └── cors.ts # @fastify/cors
│ ├── routes/
│ │ ├── auth.ts # POST /auth/register, login, logout, refresh, ...
│ │ ├── profile.ts # GET/PATCH /profile, GET /profile/:username
│ │ ├── tests.ts # POST/GET /tests, answer, finish, results, history
│ │ └── admin/
│ │ └── questions.ts # GET queue, PATCH /admin/questions/:id
│ ├── services/
│ │ ├── auth.service.ts # register, login, verifyPassword, createSession
│ │ ├── user.service.ts # getProfile, updateProfile
│ │ ├── test.service.ts # createTest, getTest, answerQuestion, finishTest
│ │ ├── question.service.ts # getFromBank, fallback logic
│ │ └── llm.service.ts # generateQuestions, вызов Ollama API, валидация
│ ├── lib/
│ │ ├── errors.ts # Кастомные ошибки (AppError)
│ │ ├── pagination.ts # Cursor-based helper
│ │ └── uuid.ts # UUID v7 генерация (если нет встроенной)
│ └── types/
│ └── index.ts # Общие типы (Stack, Level, QuestionType)
├── docker-compose.dev.yml # PostgreSQL + Redis + Ollama
├── .env.example
├── package.json
├── tsconfig.json
├── vitest.config.ts
└── drizzle.config.ts
```
Префикс `/api/v1` задаётся при регистрации роутов: `fastify.register(routes, { prefix: '/api/v1' })`.
---
## Порядок разработки
Рекомендуемая последовательность (каждый шаг — отдельный PR):
### 1. Каркас
- Инициализация проекта (npm init, зависимости)
- `config/env.ts` — загрузка и валидация env (zod или аналог)
- `app.ts` — Fastify, Pino logger, базовые плагины (helmet, cors)
- `db/schema` — Drizzle schema для MVP 0: users, sessions, subscriptions, tests, test_questions, question_bank
- `db/index.ts` — Drizzle client
- Миграции: `drizzle-kit generate` + `migrate`
- Docker Compose (PostgreSQL, Redis)
### 2. Auth
- `plugins/auth.ts` — JWT verify, refresh token из cookie
- `services/auth.service.ts` — argon2, создание sessions, ротация refresh
- `routes/auth.ts` — register, login, logout, refresh, verify-email, forgot-password, reset-password
- `plugins/rateLimit.ts` — прогрессивный lockout на login
- Email-мок для dev (например, вывод в консоль или mailpit)
### 3. Profile
- `plugins/subscription.ts` — middleware, подгружающий user.plan
- `services/user.service.ts`
- `routes/profile.ts` — GET/PATCH /profile, GET /profile/:username
- Защита роутов: `preHandler` с проверкой JWT
### 4. Tests (ядро)
- `services/llm.service.ts` — вызов Ollama, JSON Schema валидация, fallback на банк
- `services/question.service.ts` — выбор из банка, дедупликация
- `services/test.service.ts` — создание теста, снепшот в test_questions, answer, finish
- `routes/tests.ts` — все endpoints для тестов
- Проверка лимита Free (5 тестов/день) в subscription middleware
### 5. Admin
- `routes/admin/questions.ts` — GET queue, PATCH approve/reject
- Проверка `user.role === 'admin'` в preHandler
### 6. Seed и прогон
- Скрипт `npm run seed:questions` — наполнение question_bank через LLM
- Скрипт `npm run db:seed` — тестовый пользователь (опционально)
- Проверка полного flow вручную
---
## Ключевые паттерны
### Fastify plugins
Каждый плагин — отдельный файл, регистрируется через `fastify.register(plugin)`:
```ts
// plugins/auth.ts
import type { FastifyPluginAsync } from 'fastify'
const authPlugin: FastifyPluginAsync = async (fastify) => {
fastify.decorate('verifyJwt', async (request, reply) => { ... })
}
export default authPlugin
```
Декорators: `fastify.verifyJwt`, `fastify.db`, `fastify.redis`, `fastify.llm` — доступны после регистрации плагинов.
### Обработка ошибок
Единый `setErrorHandler`:
```ts
fastify.setErrorHandler((error, request, reply) => {
if (error instanceof AppError) {
return reply.status(error.statusCode).send({
error: { code: error.code, message: error.message }
})
}
fastify.log.error(error)
return reply.status(500).send({
error: { code: 'INTERNAL_ERROR', message: 'Internal server error' }
})
})
```
`AppError` — класс с полями `statusCode`, `code`, `message`. Коды ошибок — из [api/contracts.md](../api/contracts.md).
### Drizzle
- Схема в `db/schema/` — отдельный файл на домен (users.ts, tests.ts, question_bank.ts)
- Все запросы через `db.select()`, `db.insert()`, `db.update()` — параметризованные
- Даты: `new Date()` в UTC, в БД — `timestamptz`
- UUID: `uuidv7()` или аналог для PK
### LlmService
- Один класс/объект в `services/llm.service.ts`
- Конфиг из env: `LLM_BASE_URL`, `LLM_MODEL`, `LLM_TIMEOUT_MS`
- Вызов: `POST ${baseUrl}/chat/completions` с телом OpenAI-формата
- Ответ: извлечь `content` из `choices[0].message`, парсить JSON (см. llm/strategy.md — извлечение из markdown)
- Валидация: JSON Schema (ajv или zod) перед использованием
- Retry: 1 раз при таймауте или невалидном ответе
- Fallback: `question.service.getFromBank()` если LLM недоступен
### Валидация request body
Fastify schema на каждом route:
```ts
schema: {
body: {
type: 'object',
required: ['email', 'password'],
properties: {
email: { type: 'string', format: 'email' },
password: { type: 'string', minLength: 8 }
},
additionalProperties: false
}
}
```
### Пагинация cursor-based
Использовать `limit` и `cursor` (UUID). Запрос:
```sql
SELECT * FROM tests
WHERE user_id = :userId
AND (cursor IS NULL OR id < :cursor)
ORDER BY id DESC
LIMIT :limit + 1
```
Если вернулось `limit + 1` записей — `hasMore = true`, `nextCursor = last.id`. Иначе `hasMore = false`, `nextCursor = null`.
---
## Тестирование
- **Vitest** для unit и integration тестов
- Покрытие: минимум 70% на сервисном слое (см. code-style.md)
- Моки:
- LlmService — не вызывать реальный Ollama в тестах, возвращать фиксированный JSON
- Email — мок, не отправлять реальные письма
- Redis — можно in-memory или реальный Redis в Docker (integration)
- Тесты сервисов: мок Drizzle (или тестовая БД с migrater)
- Тесты роутов: `fastify.inject()` с моками сервисов
---
## Чеклист перед коммитом
- [ ] ESLint и Prettier проходят
- [ ] `npm run test` — все тесты зелёные
- [ ] Нет `console.log` (только `fastify.log` / `logger`)
- [ ] Нет `any` без явного обоснования
- [ ] Секреты не в коде (только env)
- [ ] Новые endpoints соответствуют [api/contracts.md](../api/contracts.md)