- teacher/admin course pages, content, gradebook, profile, downloads - courseNo -> UUID только на границе REST API - обновлены e2e, шаблоны, тесты и docs - LDD-UI-002; baseline 59 claims / 385 тестов
387 lines
29 KiB
Markdown
387 lines
29 KiB
Markdown
# 08. План реализации: группы, курсы, преподаватели
|
||
|
||
**Статус:** план согласован, этапы 0–6 выполнены. Этап 6 закрыт: права
|
||
преподавателя на запись, аудит и CI добавлены; продуктовые риски (legacy-активы,
|
||
срок жизни JWT/сессии, пагинация аудита) закрыты 2026-10-28. Оставшиеся
|
||
ограничения — § 9.1 и [16-stage6-report.md](16-stage6-report.md).
|
||
**Основа:** [07-groups-courses.md](07-groups-courses.md).
|
||
**Дата решений:** 2026-10-23.
|
||
|
||
Документ фиксирует решения, принятые при разборе ТЗ, и порядок реализации.
|
||
При противоречии с `07-groups-courses.md` приоритет у раздела 1 этого плана;
|
||
само ТЗ приводится в соответствие на этапе 0.
|
||
|
||
---
|
||
|
||
## 1. Зафиксированные решения
|
||
|
||
| № | Вопрос | Решение |
|
||
|---|---|---|
|
||
| 1 | Лабораторные вне курса | Не нужны. Все лабораторные принадлежат курсу. Текущие лабы переносятся в курс «Промышленное программирование на языке C». Все текущие активные группы привязываются к этому курсу. `labs.course_id` сначала nullable, после переноса — `NOT NULL`. Перед миграцией проверяются дубли номеров. |
|
||
| 2 | Идентификатор курса в URL | UUID остаётся первичным ключом. Добавляется целочисленный `courses.course_no` (`BIGINT GENERATED ALWAYS AS IDENTITY`, уникальный, неизменяемый). Курсовые материалы открываются по `/learn/course/{course_no}/...`. Все текущие материалы переносятся в курс C и доступны только студентам его групп, преподавателям курса и админу. Старые ссылки не поддерживаются. `audience` используется только для будущих глобальных материалов (`course_id IS NULL`). |
|
||
| 3 | Архив и вход | `accounts.status` (`ACTIVE/ARCHIVED`) — для всех пользователей; `students.status` (`ACTIVE/ACADEMIC_LEAVE/EXPELLED/GRADUATED/ARCHIVED`) — академический статус. Вход запрещён для `EXPELLED`, `GRADUATED`, `ARCHIVED` и архивных аккаунтов. `ACADEMIC_LEAVE` — вход в режиме только чтения. Web перепроверяет статус с кэшем 30–60 секунд; сервисы проверяют доступ к курсу при каждом запросе. |
|
||
| 4 | Журнал повышения | `promotion_runs` + `promotion_run_items` + `membership_history`. Dry-run сохраняется как план; применение идемпотентно; откат — по элементам журнала с проверкой конфликтов. |
|
||
| 5 | Объём | Полный ТЗ, этапы 0–6, один релиз с внутренними merge gate. |
|
||
| 6 | Запуск повышения | 31 августа в 00:00 МСК автоматически строится dry-run. Применение — только вручную админом после проверки плана. При пропуске даты dry-run строится при следующем старте. |
|
||
| 7 | Профиль студента курса | Преподаватель своего курса может открыть профиль студента по клику на ФИО или отдельной кнопке: статистика по лабам, комментарии преподавателей, скачивание файлов любой попытки сдачи. |
|
||
| 8 | Резервное копирование | Обязательный backup перед каждым деплоем/миграцией, ежедневный backup по расписанию, retention, проверка целостности и restore на staging. Без успешного backup деплой не продолжается. |
|
||
|
||
---
|
||
|
||
## 2. Новое требование: профиль студента курса
|
||
|
||
### 2.1. Доступ
|
||
|
||
- **Преподаватель** — только если он активен в `course_teachers` для этого курса
|
||
(`removed_at IS NULL`), а студент входит в группу, привязанную к этому курсу
|
||
(`course_groups.removed_at IS NULL`).
|
||
- **Админ** — любой студент любого курса.
|
||
- **Студент** — свой профиль и только опубликованные оценки/комментарии
|
||
(опционально; в обязательный объём текущего требования не входит).
|
||
- **Аноним** — доступа нет.
|
||
- Проверка выполняется на сервисах, а не только в web-BFF. Чужой курс/студент
|
||
должен давать `403` или `404`, а не данные.
|
||
|
||
### 2.2. Состав профиля
|
||
|
||
Карточка студента:
|
||
|
||
- ФИО, № студента, группа, статус, год набора;
|
||
- контактные/служебные поля, если появятся позже.
|
||
|
||
Сводка по курсу:
|
||
|
||
- количество лабораторных курса;
|
||
- сколько сдано, сколько проверено;
|
||
- средний балл;
|
||
- количество защит и их статусы.
|
||
|
||
Таблица по каждой лабораторной курса:
|
||
|
||
- номер и название лабы;
|
||
- вариант и уровень (если назначены);
|
||
- количество попыток сдачи;
|
||
- дата последней попытки;
|
||
- оценка;
|
||
- статус публикации оценки;
|
||
- статус защиты;
|
||
- общий комментарий к оценке;
|
||
- количество комментариев.
|
||
|
||
Комментарии преподавателей по каждой лабе:
|
||
|
||
- общий комментарий оценки (`lab_grades.general_comment`);
|
||
- отдельные комментарии (`grade_comments`) с текстом, тоном, автором и датой;
|
||
- для старых комментариев без автора — «автор не указан»;
|
||
- новые комментарии сохраняют `author_account_id`.
|
||
|
||
Все попытки сдачи по каждой лабе:
|
||
|
||
- номер попытки, дата, список файлов;
|
||
- скачивание любой попытки, а не только последней;
|
||
- скачивание в рамках своего курса; чужой `submissionId` запрещён;
|
||
- для преподавателя — ZIP попытки и, при необходимости, отдельные файлы;
|
||
- для студента — только свои попытки.
|
||
|
||
### 2.3. API (предварительно)
|
||
|
||
- `GET /api/v1/courses/{courseId}/students/{studentId}/profile` — агрегированный
|
||
профиль студента курса.
|
||
- `GET /api/v1/courses/{courseId}/submissions/{submissionId}/download` — ZIP попытки
|
||
для преподавателя/админа.
|
||
- `GET /api/v1/courses/{courseId}/submissions/{submissionId}/files/{name}` —
|
||
отдельный файл попытки (если включаем отдельные файлы).
|
||
- `GET /api/v1/me/courses/{courseId}/profile` — собственный профиль студента
|
||
(только опубликованное), опционально; в обязательный объём не входит.
|
||
|
||
`courseId` в API — UUID. В браузерных URL используется `course_no`.
|
||
|
||
### 2.4. UI
|
||
|
||
- В ведомости группы ФИО студента становится ссылкой на профиль.
|
||
- В колонке действий появляется отдельная кнопка «Профиль».
|
||
- Страница преподавателя: `/teacher/courses/{courseNo}/students/{studentId}`.
|
||
- Админ открывает тот же профиль по `/admin/courses/{courseNo}/students/{studentId}`
|
||
или через общий маршрут с проверкой роли.
|
||
- Профиль не показывает чужие курсы и глобальные материалы.
|
||
- Для архивных/выпущенных студентов профиль открывается в режиме истории, если
|
||
студент был в курсе; иначе — `404`.
|
||
|
||
### 2.5. Данные и миграции
|
||
|
||
- `grade_comments` добавить `author_account_id uuid NULL`; индекс
|
||
`(student_id, lab_id)` уже существует.
|
||
- Для общего комментария оценки добавить `lab_grades.updated_by_account_id`
|
||
(автор последнего изменения) и индекс `lab_grades(lab_id)`.
|
||
- Все попытки уже хранятся в `submissions` (`student_id`, `lab_id`, `seq`);
|
||
отдельная таблица не нужна.
|
||
- Для производительности: один агрегирующий запрос/эндпоинт, пакетная выборка
|
||
комментариев, попыток, защит и публикаций по списку `lab_id`; без N+1.
|
||
- Аудит скачиваний реализован на этапе 6 в `identity.audit_events`.
|
||
|
||
### 2.6. Ограничения
|
||
|
||
- Преподаватель видит все лабы своего курса, включая черновики, но не может
|
||
видеть лабы других курсов.
|
||
- Студент видит только опубликованные оценки и комментарии.
|
||
- Скачивание файлов не даёт доступа к чужим сдачам.
|
||
- Профиль не заменяет ведомость: оценки по-прежнему выставляются в ведомости,
|
||
профиль — просмотр и скачивание.
|
||
|
||
---
|
||
|
||
## 3. Этапы
|
||
|
||
### Этап 0. Контракты и схема
|
||
|
||
- Обновить `07-groups-courses.md` и ADR под решения раздела 1.
|
||
- Описать API профиля студента и формат ответа.
|
||
- Миграции identity/academic/progress.
|
||
- Preflight: группы, не подходящие под шаблон имени; дубли номеров лаб;
|
||
материалы без курса; комментарии без автора.
|
||
- Проверить миграции на пустой БД и на копии текущей.
|
||
- Определить механизм `labs.course_id NOT NULL`: обычный Flyway-старт не может
|
||
дождаться bootstrap-курса. Варианты — seed курса C в миграции с временно
|
||
nullable владельцем либо отдельный post-bootstrap шаг. Решение — в ADR.
|
||
|
||
**Выход:** схема, контракты, отчёт о рисках; код бизнес-логики ещё не пишем.
|
||
|
||
### Этап 1. Identity
|
||
|
||
- Роли `ADMIN/TEACHER/STUDENT`.
|
||
- Поля групп и парсер имени.
|
||
- Статусы студентов и аккаунтов.
|
||
- `membership_history`.
|
||
- Импорт Excel v2 с построчным отчётом.
|
||
- Админ-UI групп и пользователей.
|
||
|
||
### Этап 2. Academic
|
||
|
||
- `courses`, `course_teachers`, `course_groups`.
|
||
- `course_no` и URL курса.
|
||
- `labs.course_id`, перенос лаб в курс C, копирование лаб.
|
||
- `chapters.course_id/audience`, перенос материалов в курс C.
|
||
- Частичные уникальные индексы slug.
|
||
- Активы: связь с главой/статьёй и наследуемая видимость.
|
||
|
||
### Этап 3. Доступ и профиль (backend)
|
||
|
||
- `TEACHER` в JWT и security-конфигах.
|
||
- Внутренние access-эндпоинты academic/identity.
|
||
- Проверки курса, группы, студента и публикации на сервисах.
|
||
- Профиль студента курса: агрегирующий endpoint, все попытки, комментарии,
|
||
скачивание файлов.
|
||
- Negative-тесты: чужой курс, чужой студент, чужая сдача, аноним.
|
||
|
||
### Этап 4. Lifecycle
|
||
|
||
- Повышение: dry-run, журнал, применение, откат, выпуск.
|
||
- Академический отпуск и возврат.
|
||
- Отчисление, выпуск, архив.
|
||
- Усиленное удаление и аудит.
|
||
|
||
### Этап 5. UX
|
||
|
||
- Кабинет преподавателя.
|
||
- Статус защиты в ведомости: «готова» / «кончилось время» (timeout),
|
||
с разным цветом и подсказкой; сейчас в матрице всегда показывается «готова».
|
||
- Страница профиля студента: ссылка с ФИО и отдельная кнопка.
|
||
- Студенческие страницы курсов.
|
||
- Админ-страницы курсов, повышения, отпусков, архива, импорта и копирования лаб.
|
||
- Публичные глобальные материалы `ALL`.
|
||
|
||
### Этап 6. Hardening
|
||
|
||
- Права преподавателя на запись: лабораторные/уровни/варианты, ведомость,
|
||
привязка групп, авторские материалы курса.
|
||
- Полная security-матрица и negative-тесты «свой/чужой курс».
|
||
- Upgrade-тест миграций.
|
||
- Индексы; keyset-пагинация аудита, bulk-выдача групп и пакетные детали
|
||
лабораторных — выполнены.
|
||
- Аудит скачиваний и чувствительных действий (`identity.audit_events`,
|
||
`/admin/audit`).
|
||
- CI на Gitea + Woodpecker: `check` (`test ktlintCheck detekt evidenceCheck`)
|
||
и `smoke` на каждом push/MR; полный `release` — для тегов/вручную.
|
||
- Проверка backup: создание, retention, `verify`, restore на staging.
|
||
- Документация, deploy/runbook, smoke.
|
||
|
||
---
|
||
|
||
## 4. Ориентировочные миграции
|
||
|
||
Номера уточняются при реализации, чтобы не конфликтовать с максимумами master
|
||
(identity V3, **academic V5**, progress V12). Максимумы этой ветки:
|
||
identity V13, **academic V12**, progress V13.
|
||
|
||
| Сервис | Миграции | Содержание |
|
||
|---|---|---|
|
||
| identity | V4–V13 | роли, поля групп, статусы студентов, `membership_history`, `promotion_runs/items`, `accounts.status`; `V10` — `audit_events`; `V11` — `refresh_tokens` (хеш, срок, отзыв, цепочка замен); `V12` — `absolute_expires_at`; `V13` — keyset-индексы аудита |
|
||
| academic | V6–V12 + post-bootstrap step | курсы, преподаватели, группы курса, `labs.course_id` (nullable), материалы курса/аудитории, частичные индексы, активы; `V10` — уникальный индекс `lower(title)` по курсам; `V11` — backfill `audience='ALL'` для глобальных глав; `V12` — backfill связей `article_assets` из markdown-ссылок; `SET NOT NULL` — post-bootstrap шаг вне Flyway |
|
||
| progress | V13 | `grade_comments.author_account_id`, `lab_grades.updated_by_account_id`, индекс `lab_grades(lab_id)`; аудит реализован в identity `V10` |
|
||
|
||
### Переход со старой feature-миграции V10 на V13
|
||
|
||
Старая `progress V10__profile_authors.sql` — временная feature-миграция, её нет
|
||
в master.
|
||
|
||
- **Production / master:** есть только V11 и V12, записи V10 нет — V13 применяется
|
||
обычным порядком, ничего делать не нужно.
|
||
- **Dev/feature-БД, где `V10__profile_authors.sql` уже применена и имеет
|
||
`success=true`:** удалить только эту запись истории, ничего не удаляя из схемы;
|
||
V13 идемпотентна и сама создаст недостающие колонки и индекс. Используйте
|
||
`flyway repair` или точечно:
|
||
`DELETE FROM progress.flyway_schema_history WHERE version = '10' AND script = 'V10__profile_authors.sql' AND success;`.
|
||
- **Staging-БД**, которые ненадолго запускали master-коммит `bfdae41` (out-of-order
|
||
+ V10), тоже содержат запись V10 — для них действует тот же порядок.
|
||
- **Если V10 осталась с `success=false`:** сначала убедитесь, что её частичные
|
||
изменения откатились на уровне объектов, затем `flyway repair` или удаление
|
||
неуспешной строки; V13 досоздаст недостающее.
|
||
- Перед очисткой и деплоем проверьте состояние через
|
||
`deploy/preflight/progress-v10-v13.sql`.
|
||
- Переход **forward-only**: после V13 откат на сборку без V13 не пройдёт валидацию
|
||
Flyway. Не оставляйте `out-of-order` включённым.
|
||
- **Нельзя** переставлять V11/V12.
|
||
|
||
---
|
||
|
||
## 5. Агенты
|
||
|
||
| Агент | Зона | Границы |
|
||
|---|---|---|
|
||
| `lms-orchestrator` | контракты, ADR, merge, review | не пишет массово код |
|
||
| `lms-db` | миграции и тесты миграций | только `db/migration/**` и фикстуры |
|
||
| `lms-identity` | identity-service | только identity и его тесты |
|
||
| `lms-academic` | academic-service | только academic и его тесты |
|
||
| `lms-progress` | progress-service, профиль, сдачи | только progress и его тесты |
|
||
| `lms-web` | web, UI профиля, кабинеты | только `web/**` |
|
||
| `lms-security` | `libs/security-jwt`, gateway, security-review | без бизнес-логики |
|
||
| `lms-qa` | матрица ролей, upgrade, smoke | production-код не правит |
|
||
| `lms-docs` | docs, deploy/runbook | только документация |
|
||
|
||
---
|
||
|
||
## 6. Skills
|
||
|
||
- `lms-context` — инварианты и карта модулей.
|
||
- `lms-migrations` — Flyway, двухфазные миграции, preflight, legacy-БД.
|
||
- `lms-authz` — ролевая матрица, internal access, negative-тесты.
|
||
- `lms-identity-domain` — группы, статусы, история, импорт, повышение.
|
||
- `lms-academic-domain` — курсы, лабы, материалы, аудитории, активы.
|
||
- `lms-web-ui` — Thymeleaf/Bootstrap/JS, формы, навигация.
|
||
- `lms-testing` — Testcontainers, MockMvc с JWT, фикстуры.
|
||
- `lms-release` — lint/detekt/test, bootJar, `run.sh`, smoke.
|
||
|
||
---
|
||
|
||
## 7. Контроль качества
|
||
|
||
1. Миграции на пустой БД и на копии текущей; preflight-отчёт.
|
||
2. Security-матрица: аноним, студент свой/чужой, преподаватель свой/чужой,
|
||
админ, архивный/выпущенный.
|
||
3. Профиль: чужой курс/студент/сдача → `403/404`; все попытки скачиваются;
|
||
комментарии с автором; нет N+1.
|
||
4. `./gradlew ktlintCheck detekt test` после каждого этапа.
|
||
5. UI-smoke для четырёх ролей, старые и новые URL, публичные/приватные активы.
|
||
6. Аудит скачиваний и чувствительных действий.
|
||
7. Backup: обязательный перед деплоем, ежедневный, с проверкой restore.
|
||
8. Документация и runbook миграций.
|
||
|
||
---
|
||
|
||
## 8. Риски и открытые вопросы
|
||
|
||
- **Большой объём одного релиза.** Снижаем контрактами, merge gate и независимым QA.
|
||
- **Межсервисная авторизация.** Самый сложный участок; ADR до кода.
|
||
- **Перенос данных.** Нужны preflight и копия БД; группы с нестандартными именами
|
||
и дубли номеров лаб могут заблокировать `NOT NULL`.
|
||
- **Legacy-активы.** Закрыто: связанные активы наследуют видимость статьи
|
||
(миграция `V12`), а файлы без markdown-ссылки по умолчанию отдают `404`
|
||
анониму/студенту/преподавателю. Админ перечисляет их через
|
||
`GET /api/v1/content/legacy-assets`, страницу `/admin/legacy-assets` или
|
||
`scripts/legacy-assets.sh` для привязки или удаления.
|
||
- **JWT и сессии.** Закрыто: access-токен 15 минут, refresh 7 дней с ротацией
|
||
и отзывом, web-сессия 7 дней sliding с абсолютным потолком 30 дней.
|
||
Прямые API-клиенты используют `POST /api/v1/auth/refresh`; logout и смена
|
||
пароля отзывают refresh; истёкшие токены чистит ночная задача.
|
||
- **Производительность.** Аудит продолжается keyset-курсором, детали всех
|
||
лабораторных курса и группы курса отдаются пакетно. Отдельный запрос сдач
|
||
по выбранной лабораторной в ведомости остаётся, но это не N+1 по студентам.
|
||
- **Комментарии без автора.** Старые записи останутся с «автор не указан».
|
||
- **Архивные студенты.** Открывать ли профиль после отвязки группы — уточнить
|
||
на этапе 3; по умолчанию история курса доступна, если студент был в курсе.
|
||
|
||
---
|
||
|
||
## 9. Статус реализации
|
||
|
||
- **Этап 0 — выполнен.** Схема, contract API профиля, preflight, ADR,
|
||
проверка на копии БД. Отчёт: [10-stage0-report.md](10-stage0-report.md).
|
||
- **Этап 1 — выполнен.** Роли, группы-когорты, статусы, история, импорт v2,
|
||
админ-UI. Отчёт: [11-stage1-report.md](11-stage1-report.md).
|
||
- **Этап 2 — выполнен.** Курсы и `course_no`, история преподавателей и групп,
|
||
bootstrap курса C, привязка лабораторных к курсу, копирование лаб,
|
||
админ-UI курсов и лабораторных. Отчёт: [12-stage2-report.md](12-stage2-report.md).
|
||
- **Этап 3 — выполнен.** Аккаунты преподавателей, курсовая область видимости,
|
||
внутренние API identity с `LMS_INTERNAL_TOKEN`, кабинет и состав групп
|
||
преподавателя, профиль студента курса и скачивание всех попыток.
|
||
Отчёт: [13-stage3-report.md](13-stage3-report.md).
|
||
- **Этап 4 — выполнен.** Повышение (dry-run/apply/rollback, планировщик
|
||
31 августа МСК и догон), жизненный цикл студента, архив группы,
|
||
усиленное подтверждение удаления, блокировка входа для архивных
|
||
и выпущенных групп. Отчёт: [14-stage4-report.md](14-stage4-report.md).
|
||
- **Этап 5 — выполнен.** Студенческие страницы курсов, материалы по
|
||
`/learn/course/{course_no}/...` и аудитории, публичные материалы `ALL`,
|
||
скрытые материалы для преподавателя, перепроверка статуса web-сессии.
|
||
Отчёт: [15-stage5-course-materials.md](15-stage5-course-materials.md).
|
||
- **Этап 6 — выполнен.** Права преподавателя на запись (лабораторные,
|
||
ведомость, группы, материалы), центральный аудит `identity.audit_events`
|
||
с `/admin/audit`, CI merge gate и полный hardening. Проверены прогон
|
||
тестов/линтеров (**385**, 0 failures), upgrade миграций с master-схемы
|
||
(identity `V4–V13`, academic `V6–V12`, progress `V13`), security-матрица,
|
||
gateway, видимость активов и deploy-runbook. LDD baseline: **59 claims
|
||
(27 P0 / 32 P1), P0 gaps = 0**, browser E2E 18/18 (3 браузера). Отчёт:
|
||
[16-stage6-report.md](16-stage6-report.md).
|
||
|
||
### 9.1. Известные пробелы после этапа 6
|
||
|
||
- **Импорт.** Старый формат не поддерживается, принимается только шаблон v2.
|
||
Номер группы — числовой; отдельного требования к ведущим нулям нет.
|
||
- **Пагинация и N+1.** Аудит продолжается keyset-курсором `(occurred_at, id)`
|
||
с offset-fallback для старых ссылок; `GET /api/v1/labs/details` отдаёт все
|
||
лабораторные курса с уровнями и вариантами одним запросом; группы курса —
|
||
одним вызовом. Ведомость по-прежнему запрашивает сдачи по выбранной
|
||
лабораторной — это не N+1 по студентам.
|
||
- **Legacy-активы.** Файлы без статьи уже не публичны; админская страница
|
||
`/admin/legacy-assets`, API и `scripts/legacy-assets.sh` готовы. Остаётся
|
||
прогнать инвентаризацию на production/staging и привязать или удалить файлы.
|
||
- **Сессии.** Web-сессия скользящая 7 дней, абсолютный потолок 30 дней
|
||
(`LMS_SESSION_ABSOLUTE_TIMEOUT`); истёкшие refresh-токены чистит ночная
|
||
задача (`LMS_REFRESH_TOKEN_RETENTION`).
|
||
- **CI.** Конфигурация `.woodpecker/` и `ci-preflight.sh` готовы; Woodpecker
|
||
server/agent и первый реальный pipeline ещё нужно развернуть и подтвердить
|
||
(`deploy/WOODPECKER.md`).
|
||
- **Внешняя интеграция «Контингент»/`next.uniyar.ac.ru` исключена из плана**
|
||
до появления нового ТЗ и доступа к API.
|
||
|
||
## 10. Definition of Done
|
||
|
||
- Все решения раздела 1 реализованы.
|
||
- Текущие лабы и материалы работают в курсе C.
|
||
- Преподаватель видит только свои курсы и профили своих студентов, пишет
|
||
в лабы/ведомость/группы/материалы только своего курса.
|
||
- Профиль показывает статистику, комментарии и все попытки; файлы скачиваются.
|
||
- Повышение, академотпуск, выпуск и архив работают по ТЗ.
|
||
- Аудит чувствительных действий пишется в `identity.audit_events` и доступен
|
||
админу на `/admin/audit` с keyset-пагинацией.
|
||
- Активы наследуют видимость материала; файлы без привязки не публичны и
|
||
доступны только админу через страницу `/admin/legacy-assets`.
|
||
- Access/refresh-токены и web-сессия работают по срокам 15 минут / 7 дней /
|
||
7 дней sliding с абсолютным потолком 30 дней; logout, смена пароля и
|
||
закрытие доступа отзывают refresh.
|
||
- Security-матрица и миграционный upgrade-тест зелёные (identity до `V13`).
|
||
- CI (Gitea + Woodpecker) на каждом push/MR прогоняет
|
||
`test ktlintCheck detekt evidenceCheck` и smoke полного стека; полный
|
||
`release` обязателен для тегов.
|
||
- Backup перед деплоем обязателен; restore проверен на staging.
|
||
- Документация, deploy/runbook и smoke обновлены.
|