feat: закрыть остаточные P1/P2 по профилю, когорте и ведомости

- профиль приведён к docs/09: variant/level, updatedBy/name, лимит попыток
- V14: NOT NULL и полная уникальность когорты, preflight legacy-групп
- возврат группы из архива и история членства на странице группы
- пагинация групп и плоского списка материалов
- пакетные сдачи ведомости одним запросом
- XLSX-preflight импорта и audit-тесты ростера
- LDD: 66 claims, 398 тестов, release PASS
This commit is contained in:
2026-10-29 20:51:27 +03:00
parent 7ef5c866a3
commit 82394cc688
48 changed files with 1196 additions and 156 deletions

View File

@@ -435,12 +435,12 @@
"criticality": "P1",
"status": "active",
"requirement": "docs/16-stage6-report.md, §2.2",
"claim": "Схема master + синтетические данные → миграции identity V4–V13, academic V6–V12, progress V13; V10 audit, V11 refresh, V12 absolute-expiry и V13 keyset-индексы создаются, V13 идемпотентна.",
"oracle": "Успешное применение и postflight-наличие таблиц/колонок/индексов",
"claim": "Схема master + синтетические данные → миграции identity V4–V14, academic V6–V12, progress V13; V10 audit, V11 refresh, V12 absolute-expiry, V13 keyset-индексы, V14 NOT NULL/unique когорты; V13/V14 идемпотентны.",
"oracle": "postflight checks = required; повторные V13/V14 не падают",
"runtime": [
"migration.master_upgrade"
],
"limitations": "Запускается отдельным скриптом на одноразовой БД; не входит в unit-CI."
"limitations": "V14 намеренно блокирует legacy-группы с неразобранной когортой."
},
{
"id": "LDD-RUNTIME-001",
@@ -872,6 +872,108 @@
"ru.ysu.lms.web.profile.StudentProfileTemplateTest#renders student profile with labs, comments and downloads()"
],
"limitations": "Внутренние REST-контракты остаются UUID; form payload создания/копирования лабораторной тоже использует UUID."
},
{
"id": "LDD-PROFILE-002",
"title": "Профиль соответствует контракту docs/09 и ограничивает попытки",
"criticality": "P1",
"status": "active",
"requirement": "docs/09-teacher-student-profile.md, §3, §7",
"claim": "Профиль отдаёт variant.id, level.id/code, grade.updatedBy и author.name; число попыток отражается полностью, а список ограничивается параметром submissionLimit; профиль не делает запрос на каждую лабу.",
"oracle": "JSON-поля контракта; attemptsCount/submissionsTotal; число SQL-запросов не растёт с числом лабораторных",
"evidence": [
"ru.ysu.lms.progress.StudentProfileIntegrationTest#teacher sees unpublished grade, authors and all submission attempts()",
"ru.ysu.lms.progress.StudentProfileIntegrationTest#profile limits submission attempts but reports total()",
"ru.ysu.lms.progress.StudentProfileIntegrationTest#profile uses one query per data kind instead of per lab()",
"ru.ysu.lms.web.profile.ProfileDtoJsonTest#deserializes profile contract()",
"ru.ysu.lms.web.profile.StudentProfileTemplateTest#renders student profile with labs, comments and downloads()",
"ru.ysu.lms.identity.internal.InternalControllerIntegrationTest#lookup includes student full name and leaves staff name empty()"
],
"limitations": "По умолчанию возвращаются последние 50 попыток; UI показывает, сколько скрыто."
},
{
"id": "LDD-GROUP-001",
"title": "Группа возвращается из архива",
"criticality": "P1",
"status": "active",
"requirement": "docs/07-groups-courses.md, §8.3",
"claim": "Архивную группу можно вернуть в ACTIVE; повторный возврат активной группы отклоняется, состав и история не меняются.",
"oracle": "status меняется ARCHIVED→ACTIVE; второй restore — 400; число студентов и история сохраняются",
"evidence": [
"ru.ysu.lms.identity.roster.GroupServiceIntegrationTest#restores archived group and rejects repeated restore()",
"ru.ysu.lms.identity.audit.AuditIntegrationTest#roster operations record group and student audit events()"
],
"limitations": "Возврат выполняется со страницы группы; отдельного массового восстановления нет."
},
{
"id": "LDD-GROUP-002",
"title": "Финальные ограничения когорты",
"criticality": "P1",
"status": "active",
"requirement": "docs/07-groups-courses.md, §8.2; docs/08-groups-courses-plan.md, этап 1",
"claim": "Все поля когорты NOT NULL, уникальность полная (без partial WHERE), legacy-строки блокируют миграцию с явной ошибкой.",
"oracle": "information_schema.is_nullable=NO для 5 полей; groups_cohort_unique без WHERE; master→branch migration PASS",
"evidence": [],
"runtime": [
"migration.master_upgrade"
],
"limitations": "Перед деплоем на реальных данных нужен ручной разбор legacy-групп; запрос для проверки есть в V14."
},
{
"id": "LDD-IMPORT-004",
"title": "XLSX-импорт отчитывается по строкам до записи",
"criticality": "P1",
"status": "active",
"requirement": "docs/08-groups-courses-plan.md, этап 1; docs/18 §3",
"claim": "Для XLSX занятый логин и неактивная целевая группа возвращают applied=false, номер строки и не создают частичных записей.",
"oracle": "ImportResult.errors.row=2, applied=false; студент не создан",
"evidence": [
"ru.ysu.lms.identity.roster.GroupServiceIntegrationTest#xlsx import reports occupied login before writing()",
"ru.ysu.lms.identity.roster.GroupServiceIntegrationTest#xlsx import reports inactive target group before writing()"
],
"limitations": "Гонка с параллельной вставкой логина вне preflight всё ещё может дать общий 409."
},
{
"id": "LDD-AUDIT-006",
"title": "Операции с группами и студентами пишут аудит",
"criticality": "P1",
"status": "active",
"requirement": "docs/07-groups-courses.md, §9; docs/08-groups-courses-plan.md, этап 1",
"claim": "GROUP_CREATE, STUDENT_CREATE, GROUP_ENROLL, GROUP_IMPORT, GROUP_ARCHIVE и GROUP_RESTORE попадают в журнал с актором и деталями.",
"oracle": "audit_events содержит все шесть action; actorAccountId/actorLogin заполнены",
"evidence": [
"ru.ysu.lms.identity.audit.AuditIntegrationTest#roster operations record group and student audit events()"
],
"limitations": "Проверяются успешные операции; отказы валидации аудит не пишут."
},
{
"id": "LDD-PERF-003",
"title": "Пакетные сдачи и пагинация списков",
"criticality": "P1",
"status": "active",
"requirement": "docs/07-groups-courses.md, §10; docs/18 §3",
"claim": "Ведомость получает последние сдачи по всем выбранным лабам одним запросом; группы и плоский список материалов поддерживают limit/offset.",
"oracle": "одна выборка submissions на набор labId; страницы групп/материалов не пересекаются и не теряют записи",
"evidence": [
"ru.ysu.lms.progress.SubmissionBatchIntegrationTest#latest submissions are grouped by lab in one call()",
"ru.ysu.lms.identity.roster.GroupServiceIntegrationTest#lists groups page by page and returns all without limit()",
"ru.ysu.lms.academic.content.CourseContentIntegrationTest#admin article list supports offset and limit()"
],
"limitations": "Дерево курса по-прежнему загружается целиком для навигации; пагинируются плоские списки."
},
{
"id": "LDD-GROUP-003",
"title": "История членства видна на странице группы",
"criticality": "P1",
"status": "active",
"requirement": "docs/07-groups-courses.md, §3; docs/18 §3",
"claim": "Админ видит последние события membership_history по группе: студент, действие, откуда/куда, причина и время МСК.",
"oracle": "GET /api/v1/groups/{id}/history отдаёт studentName/fromGroupName/toGroupName; шаблон показывает блок истории",
"evidence": [
"ru.ysu.lms.identity.roster.GroupServiceIntegrationTest#imports students and records membership history()",
"ru.ysu.lms.web.admin.AdminStage4TemplateTest#renders membership history and restore action for archived group()"
],
"limitations": "Показаны последние 200 событий; фильтра/пагинации по истории нет."
}
]
}