{ "version": 1, "description": "LDD claims для текущей функциональности LMS. P0 без зелёного свидетельства валит ./gradlew evidenceCheck.", "claims": [ { "id": "LDD-ACCESS-001", "title": "Преподаватель не получает доступ к чужому курсу, лабе, студенту или группе", "criticality": "P0", "status": "active", "requirement": "docs/07-groups-courses.md, §5", "claim": "Для любого преподавателя T и ресурса R чужого курса сервис возвращает 403/404 и не отдаёт данные R.", "oracle": "HTTP-статус отказа и отсутствие данных в ответе", "evidence": [ "ru.ysu.lms.academic.course.CourseAccessServiceTest#teacher cannot access a foreign course()", "ru.ysu.lms.academic.course.CourseRosterServiceTest#foreign teacher is forbidden()", "ru.ysu.lms.progress.TeacherWriteAccessIntegrationTest#teacher is forbidden to grade foreign lab()", "ru.ysu.lms.progress.TeacherWriteAccessIntegrationTest#teacher is forbidden to grade student from another course()" ], "limitations": "Проверены основные API-точки; UI-навигация не является границей безопасности." }, { "id": "LDD-ACCESS-002", "title": "Студент видит только свои курсы, группу и опубликованные данные", "criticality": "P0", "status": "active", "requirement": "docs/07-groups-courses.md, §5", "claim": "Студент чужой группы не получает доступ к курсу, а в своём профиле видит только опубликованные оценки и комментарии.", "oracle": "403 для чужого курса; отсутствие неопубликованных данных в ответе", "evidence": [ "ru.ysu.lms.academic.course.CourseAccessServiceTest#student cannot access course of a foreign group()", "ru.ysu.lms.progress.StudentProfileIntegrationTest#student sees only published grade and comments of own profile()", "ru.ysu.lms.progress.StudentProfileIntegrationTest#teacher cannot read or download student from group outside course but admin can()" ], "limitations": "Студенческий BFF-профиль не является отдельной страницей; проверен профильный API." }, { "id": "LDD-ACCESS-003", "title": "Анонимный доступ ограничен глобальными материалами ALL", "criticality": "P0", "status": "active", "requirement": "docs/07-groups-courses.md, §6", "claim": "Аноним не читает курсовые и приватные материалы/активы; публичное дерево содержит только глобальные главы audience=ALL.", "oracle": "401/404 для приватного; 200 только для ALL; состав публичного дерева", "evidence": [ "ru.ysu.lms.academic.content.CourseContentIntegrationTest#anonymous cannot read course content()", "ru.ysu.lms.academic.content.CourseContentIntegrationTest#public tree contains only global chapters with ALL audience()", "ru.ysu.lms.academic.content.ContentAssetAccessIntegrationTest#course asset requires authentication()", "ru.ysu.lms.academic.content.ContentAssetAccessIntegrationTest#published global ALL asset stays public()" ], "limitations": "Legacy-активы без article_assets остаются публичными и вынесены в отдельный риск." }, { "id": "LDD-ACCESS-004", "title": "Администратор сохраняет полный доступ", "criticality": "P0", "status": "active", "requirement": "docs/07-groups-courses.md, §5", "claim": "Администратор не ограничен курсовой областью и может читать/писать любой курс и профиль.", "oracle": "Успешный доступ к чужому курсу/лабе/студенту для ADMIN", "evidence": [ "ru.ysu.lms.academic.course.CourseAccessServiceTest#admin bypasses checks and has no course restriction()", "ru.ysu.lms.academic.course.CourseAccessServiceTest#admin my courses contains every course()", "ru.ysu.lms.progress.TeacherWriteAccessIntegrationTest#admin keeps access without course scope checks()" ], "limitations": "Проверены сервисные проверки, не все административные UI-маршруты." }, { "id": "LDD-ACCESS-005", "title": "Внутренние API identity защищены сервисным токеном и не раскрывают учётные данные", "criticality": "P0", "status": "active", "requirement": "docs/13-stage3-report.md, §3", "claim": "Без корректного X-Internal-Token внутренние API возвращают 403; ответы не содержат паролей, tempPassword и лишних данных.", "oracle": "403 на неверный/пустой токен; отсутствие чувствительных полей в JSON", "evidence": [ "ru.ysu.lms.identity.internal.InternalControllerIntegrationTest#invalid token is forbidden()", "ru.ysu.lms.identity.internal.InternalControllerIntegrationTest#groups response exposes no credentials or students()", "ru.ysu.lms.identity.audit.AuditIntegrationTest#invalid or missing internal token is forbidden and writes nothing()" ], "limitations": "Проверены контекст студента, состав группы, список групп и аудит." }, { "id": "LDD-ACCESS-006", "title": "Преподаватель управляет лабораторными только своего курса", "criticality": "P0", "status": "active", "requirement": "docs/07-groups-courses.md, §5", "claim": "TEACHER может создавать и изменять лабы/уровни/варианты своего курса, но не чужого; STUDENT не может писать.", "oracle": "200 для своего курса; 403 для чужого; 403 для студента", "evidence": [ "ru.ysu.lms.academic.lab.LabTeacherAccessIntegrationTest#teacher cannot update lab of foreign course()", "ru.ysu.lms.academic.lab.LabTeacherAccessIntegrationTest#teacher can create lab in own course()", "ru.ysu.lms.academic.lab.LabTeacherAccessIntegrationTest#student cannot update or create labs()" ], "limitations": "Импорт ZIP проверен отдельными тестами парсера/копирования." }, { "id": "LDD-PROM-001", "title": "Dry-run повышения не меняет данные и дедуплицируется по году", "criticality": "P0", "status": "active", "requirement": "docs/07-groups-courses.md, §7", "claim": "Построение dry-run создаёт только план; данные групп/студентов не меняются; повторный запуск за год не создаёт дубль.", "oracle": "Снимок данных до/после и количество запусков", "evidence": [ "ru.ysu.lms.identity.promotion.PromotionServiceIntegrationTest#dry run only plans and does not change data()", "ru.ysu.lms.identity.promotion.PromotionServiceIntegrationTest#scheduled dry run guard respects date and deduplicates by year()" ], "limitations": "Проверено на синтетических данных Testcontainers." }, { "id": "LDD-PROM-002", "title": "Применение повышения переводит группы и студентов по правилам", "criticality": "P0", "status": "active", "requirement": "docs/07-groups-courses.md, §7", "claim": "Apply повышает курс групп, выпускает на пределе и переводит академических студентов в целевую группу.", "oracle": "Группы, статусы, зачисления и membership_history после apply", "evidence": [ "ru.ysu.lms.identity.promotion.PromotionServiceIntegrationTest#apply promotes groups graduates and moves academic students()" ], "limitations": "Проверен один сценарий; граничные сочетания требуют property-based тестов." }, { "id": "LDD-PROM-003", "title": "Повторное применение повышения идемпотентно", "criticality": "P0", "status": "active", "requirement": "docs/07-groups-courses.md, §7", "claim": "Повторный apply того же run не создаёт повторных изменений и истории.", "oracle": "Состояние и количество history-записей после первого и второго apply", "evidence": [ "ru.ysu.lms.identity.promotion.PromotionServiceIntegrationTest#repeated apply is idempotent()" ], "limitations": "Конкурентный apply проверяется advisory lock, но отдельного нагрузочного теста нет." }, { "id": "LDD-PROM-004", "title": "Откат повышения восстанавливает состояние и обнаруживает конфликт", "criticality": "P0", "status": "active", "requirement": "docs/07-groups-courses.md, §7", "claim": "Rollback возвращает before-state; при ручной правке после apply сообщает конфликт и не затирает данные.", "oracle": "Снимок before/after и 409 при конфликте", "evidence": [ "ru.ysu.lms.identity.promotion.PromotionServiceIntegrationTest#rollback restores data and removes created target group()", "ru.ysu.lms.identity.promotion.PromotionServiceIntegrationTest#rollback reports manual conflict()" ], "limitations": "Проверены группы и студенты; rollback файлов/аудита не применим." }, { "id": "LDD-LIFE-001", "title": "Жизненный цикл студента пишет историю и переводит зачисление", "criticality": "P0", "status": "active", "requirement": "docs/07-groups-courses.md, §8", "claim": "Академ/возврат/отчисление/архив/восстановление меняют статусы, зачисление и membership_history по правилам.", "oracle": "Статус student/account, группа зачисления, membership_history", "evidence": [ "ru.ysu.lms.identity.roster.StudentLifecycleIntegrationTest#academic leave creates target cohort group and records history()", "ru.ysu.lms.identity.roster.StudentLifecycleIntegrationTest#return from academic leave moves to requested active group()", "ru.ysu.lms.identity.roster.StudentLifecycleIntegrationTest#expel blocks login and keeps current enrollment for history()", "ru.ysu.lms.identity.roster.StudentLifecycleIntegrationTest#archive blocks login and restore returns student to active group()", "ru.ysu.lms.identity.roster.StudentLifecycleIntegrationTest#restore returns an expelled student to active group()" ], "limitations": "Проверены основные переходы; массовые операции не автоматизированы." }, { "id": "LDD-LIFE-002", "title": "Вход закрыт для отчисленных, архивных и закрытых групп", "criticality": "P0", "status": "active", "requirement": "docs/07-groups-courses.md, §1.3", "claim": "EXPELLED/GRADUATED/ARCHIVED студенты и архивные аккаунты не проходят login; активный студент проходит.", "oracle": "Unauthorized/403 при login и active=false в /auth/me", "evidence": [ "ru.ysu.lms.identity.roster.GroupServiceIntegrationTest#expelled student cannot login()", "ru.ysu.lms.identity.roster.StudentLifecycleIntegrationTest#student in archived group cannot login()", "ru.ysu.lms.identity.roster.StudentLifecycleIntegrationTest#student in active group can login()", "ru.ysu.lms.identity.AuthServiceMeActiveTest#me is inactive for archived account()" ], "limitations": "JWT с прежним сроком жизни остаётся остаточным риском." }, { "id": "LDD-PROFILE-001", "title": "Профиль студента курса различает staff/student и публикацию", "criticality": "P0", "status": "active", "requirement": "docs/07-groups-courses.md, §5.1", "claim": "Преподаватель своего курса видит все лабы и неопубликованные данные; студент — только опубликованное; чужой преподаватель получает 403.", "oracle": "Состав DTO профиля и HTTP-статус", "evidence": [ "ru.ysu.lms.progress.StudentProfileIntegrationTest#teacher sees unpublished grade, authors and all submission attempts()", "ru.ysu.lms.progress.StudentProfileIntegrationTest#student sees only published grade and comments of own profile()", "ru.ysu.lms.progress.StudentProfileIntegrationTest#teacher of another course gets forbidden()" ], "limitations": "Проверен API; UI-шаблон проверяется отдельно." }, { "id": "LDD-DOWNLOAD-001", "title": "Скачивание сдачи ограничено курсом и студентом", "criticality": "P0", "status": "active", "requirement": "docs/07-groups-courses.md, §5.1", "claim": "Преподаватель скачивает только сдачи своего курса/своих студентов; сдача лабы другого курса отклоняется.", "oracle": "ZIP для разрешённой сдачи; 403 для чужой лабы/студента", "evidence": [ "ru.ysu.lms.progress.StudentProfileIntegrationTest#download rejects submission from another course lab and allows current course lab()", "ru.ysu.lms.progress.TeacherLabPageAccessIntegrationTest#teacher downloads submission of own lab()", "ru.ysu.lms.progress.TeacherLabPageAccessIntegrationTest#teacher is forbidden for foreign lab()" ], "limitations": "Аудит скачивания проверяется отдельным claim." }, { "id": "LDD-CONTENT-001", "title": "Курсовые материалы не утекают в публичное дерево", "criticality": "P0", "status": "active", "requirement": "docs/07-groups-courses.md, §6", "claim": "Курсовая глава недоступна через публичный article endpoint; студент видит только published; преподаватель видит hidden.", "oracle": "404/отсутствие курсовой главы в public tree; состав courseTree", "evidence": [ "ru.ysu.lms.academic.content.CourseContentIntegrationTest#course chapter is not exposed through public article endpoint()", "ru.ysu.lms.academic.content.CourseContentIntegrationTest#student of course sees only published materials()", "ru.ysu.lms.academic.content.CourseContentIntegrationTest#teacher of course sees published and hidden materials()" ], "limitations": "Импорт и авторство проверяются отдельными тестами." }, { "id": "LDD-ASSET-001", "title": "Видимость активов наследуется от статьи", "criticality": "P0", "status": "active", "requirement": "docs/07-groups-courses.md, §6", "claim": "Скрытый курсовой актив не отдаётся студенту, но доступен преподавателю; чужой курс получает 403; legacy без связи публичен.", "oracle": "HTTP-статус и содержимое файла", "evidence": [ "ru.ysu.lms.academic.content.ContentAssetAccessIntegrationTest#hidden course asset is not served to student but is served to teacher()", "ru.ysu.lms.academic.content.ContentAssetAccessIntegrationTest#foreign teacher and student are forbidden for course asset()", "ru.ysu.lms.academic.content.ContentAssetAccessIntegrationTest#legacy asset without article association stays public()" ], "limitations": "Legacy-файлы без article_assets остаются публичными — отдельный остаточный риск." }, { "id": "LDD-MIG-001", "title": "Миграция V13 идемпотентна", "criticality": "P0", "status": "active", "requirement": "docs/16-stage6-report.md, §2.2", "claim": "Повторное применение V13 не меняет схему и не падает.", "oracle": "Повторный Flyway migrate и проверка колонок/индекса", "evidence": [ "ru.ysu.lms.progress.migration.ProfileAuthorsMigrationTest#V13 profile authors migration is idempotent()" ], "limitations": "Полный upgrade с master-схемы проверяется runtime-скриптом." }, { "id": "LDD-AUDIT-001", "title": "Аудит защищён и доступен только администратору", "criticality": "P0", "status": "active", "requirement": "docs/16-stage6-report.md, §2.3", "claim": "Внутренняя запись требует X-Internal-Token; чтение /api/v1/audit требует ADMIN.", "oracle": "403 без токена/для не-админа; 200 для админа", "evidence": [ "ru.ysu.lms.identity.audit.AuditIntegrationTest#admin list requires admin role()", "ru.ysu.lms.identity.audit.AuditIntegrationTest#invalid or missing internal token is forbidden and writes nothing()" ], "limitations": "Проверены API; UI-страница проверяется отдельным шаблонным тестом." }, { "id": "LDD-AUDIT-002", "title": "Аудит переживает удаление сущности и сохраняет поля", "criticality": "P0", "status": "active", "requirement": "docs/07-groups-courses.md, §5.2", "claim": "Событие аудита сохраняется после удаления студента/преподавателя и корректно отдаётся в списке.", "oracle": "Наличие строки после delete; поля id/action/entity/occurredAt", "evidence": [ "ru.ysu.lms.identity.audit.AuditIntegrationTest#identity hooks record teacher create and student delete that survives deletion()", "ru.ysu.lms.identity.audit.AuditIntegrationTest#internal write is listed newest first with all fields()" ], "limitations": "Проверены identity-хуки; cross-service хуки проверяются отдельными тестами." }, { "id": "LDD-ACCESS-007", "title": "Преподаватель управляет группами своего курса", "criticality": "P1", "status": "active", "requirement": "docs/07-groups-courses.md, §5", "claim": "TEACHER привязывает/отвязывает активные группы только своего курса; чужой преподаватель и студент получают 403.", "oracle": "200 и history-запись для своего курса; 403 для чужого", "evidence": [ "ru.ysu.lms.academic.course.CourseGroupTeacherAccessTest#teacher sees available groups, attaches and detaches on own course()", "ru.ysu.lms.academic.course.CourseGroupTeacherAccessTest#foreign teacher cannot list attach or detach groups()" ], "limitations": "Проверено API; UI-форма — шаблонным тестом." }, { "id": "LDD-ACCESS-008", "title": "Преподаватель авторствует материалы только своего курса", "criticality": "P1", "status": "active", "requirement": "docs/07-groups-courses.md, §5", "claim": "TEACHER создаёт/меняет/публикует главы, разделы и статьи своего курса; глобальные материалы — только ADMIN; чужой курс — 403.", "oracle": "200 для своего курса; 403 для чужого; global routes только ADMIN", "evidence": [ "ru.ysu.lms.academic.content.CourseContentAuthoringIntegrationTest#teacher creates chapter section and article in own course()", "ru.ysu.lms.academic.content.CourseContentAuthoringIntegrationTest#foreign teacher cannot author course content()", "ru.ysu.lms.academic.content.CourseContentAuthoringIntegrationTest#global chapter authoring stays admin only()" ], "limitations": "Импорт ZIP остаётся админским; это осознанное ограничение." }, { "id": "LDD-ACCESS-009", "title": "Ведомость преподавателя не показывает админские операции", "criticality": "P1", "status": "active", "requirement": "docs/13-stage3-report.md, §4", "claim": "В teacher-view скрыты пароли, аттестации и жизненный цикл; админский вид не меняется; админские URL закрыты интерсептором.", "oracle": "Состав HTML и redirect для teacher", "evidence": [ "ru.ysu.lms.web.admin.TeacherGradebookTemplateTest#teacher gradebook has no admin-only controls()", "ru.ysu.lms.web.admin.TeacherGradebookTemplateTest#admin view is used when teacherView attribute is missing()", "ru.ysu.lms.web.session.AdminInterceptorTest#teacher is redirected from admin-only group operations()" ], "limitations": "Это UI-защита; серверные проверки — в LDD-ACCESS-001." }, { "id": "LDD-LIFE-003", "title": "Удаление требует усиленного подтверждения", "criticality": "P1", "status": "active", "requirement": "docs/07-groups-courses.md, §11", "claim": "DELETE студента без совпадения confirm с login/ФИО отклоняется; корректный confirm удаляет.", "oracle": "400 при неверном confirm; успешное удаление при верном", "evidence": [ "ru.ysu.lms.identity.roster.StudentLifecycleIntegrationTest#delete requires confirm equal to login or full name()" ], "limitations": "Проверен сервис; UI-диалог — шаблонным тестом." }, { "id": "LDD-PROM-005", "title": "Отмена устаревшего dry-run разрешает перестроить план", "criticality": "P1", "status": "active", "requirement": "docs/16-stage6-report.md, §2.3", "claim": "Cancel переводит PLANNED dry-run в FAILED/CANCELLED; после этого можно построить новый план за год.", "oracle": "Статус run и создание нового dry-run", "evidence": [ "ru.ysu.lms.identity.promotion.PromotionServiceIntegrationTest#cancel marks planned dry run and allows rebuild()" ], "limitations": "Конкурентный cancel/apply защищён advisory lock, отдельного теста нет." }, { "id": "LDD-IMPORT-001", "title": "Импорт групп валидирует файл и не применяется частично", "criticality": "P1", "status": "active", "requirement": "docs/07-groups-courses.md, §9", "claim": "Ошибочные строки блокируют импорт целиком; корректный шаблон и дубли № распознаются.", "oracle": "applied=false и отсутствие изменений при ошибках; отчёт парсера", "evidence": [ "ru.ysu.lms.identity.roster.GroupServiceIntegrationTest#does not apply import when there are row errors()", "ru.ysu.lms.identity.roster.StudentImportParserTest#parses new template and splits full name()", "ru.ysu.lms.identity.roster.StudentImportParserTest#reports duplicate student number()" ], "limitations": "Старый формат не поддерживается; ведущие нули нормализуются." }, { "id": "LDD-LAB-002", "title": "Копирование лабораторной не переносит прогресс и требует другой курс", "criticality": "P1", "status": "active", "requirement": "docs/07-groups-courses.md, §10", "claim": "Копия создаёт новые уровни/варианты в целевом курсе, не копирует оценки/сдачи; тот же/архивный курс отклоняется.", "oracle": "Состав копии и отсутствие progress-строк; 400 для same/archived", "evidence": [ "ru.ysu.lms.academic.lab.LabCopyIntegrationTest#copies lab with levels and variants into another course()", "ru.ysu.lms.academic.lab.LabCopyIntegrationTest#copy rejects the source course as target()", "ru.ysu.lms.academic.lab.LabCopyIntegrationTest#copy rejects an archived target course()" ], "limitations": "Проверено на Testcontainers; UI-копирование — шаблонным тестом." }, { "id": "LDD-CONTRACT-001", "title": "DTO-контракты web и сервисов совпадают", "criticality": "P1", "status": "active", "requirement": "docs/09-teacher-student-profile.md", "claim": "Ключевые JSON-контракты профиля, состава группы, аудита и повышения десериализуются web-слоем без потери полей.", "oracle": "JsonIgnoreProperties + значения полей после десериализации", "evidence": [ "ru.ysu.lms.web.profile.ProfileDtoJsonTest#deserializes profile contract()", "ru.ysu.lms.web.api.CourseGroupRosterDtoTest#deserializes course group roster contract()", "ru.ysu.lms.web.api.AuditDtoTest#deserializes audit event list with nulls()", "ru.ysu.lms.web.api.PromotionDtoTest#deserializes promotion detail with item states and result()", "ru.ysu.lms.identity.roster.TeacherIntegrationTest#student creation accepts omitted optional json fields()" ], "limitations": "Это smoke-контракты, не consumer-driven contracts." }, { "id": "LDD-UI-001", "title": "Ключевые страницы содержат нужные действия и не раскрывают лишнего", "criticality": "P1", "status": "active", "requirement": "docs/16-stage6-report.md, §3.3", "claim": "Шаблоны ведомости, материалов и аудита рендерят ожидаемые формы/ссылки и экранируют данные.", "oracle": "Содержимое HTML и отсутствие запрещённых элементов", "evidence": [ "ru.ysu.lms.web.teacher.TeacherCourseContentTemplateTest#content page renders create forms and full tree()", "ru.ysu.lms.web.profile.StudentProfileTemplateTest#renders student profile with labs, comments and downloads()", "ru.ysu.lms.web.admin.AdminAuditTemplateTest#renders audit events with labels and filter()" ], "limitations": "Thymeleaf-тесты не заменяют браузерный E2E." }, { "id": "LDD-AUDIT-003", "title": "Критичные действия progress пишут аудит", "criticality": "P1", "status": "active", "requirement": "docs/07-groups-courses.md, §5.2", "claim": "Оценка и публикация оценки создают audit-событие с actor из JWT и корректными идентификаторами.", "oracle": "Вызов AuditClient с ожидаемыми action/entity/student/course", "evidence": [ "ru.ysu.lms.progress.ProgressControllerAuditTest#grade write records audit entry for actor from jwt()", "ru.ysu.lms.progress.ProgressControllerAuditTest#publication records student count()", "ru.ysu.lms.identity.audit.AuditIntegrationTest#action filter and limit are applied()" ], "limitations": "Проверены representative-хуки; остальные actions проверяются кодом и smoke." }, { "id": "LDD-MIG-002", "title": "Upgrade с master-схемы применяет миграции ветки", "criticality": "P1", "status": "active", "requirement": "docs/16-stage6-report.md, §2.2", "claim": "Схема master + синтетические данные → миграции identity V4–V10, academic V6–V12, progress V13; V10 audit создаётся, V13 идемпотентна.", "oracle": "Успешное применение и postflight-наличие таблиц/колонок/индексов", "runtime": [ "migration.master_upgrade" ], "limitations": "Запускается отдельным скриптом на одноразовой БД; не входит в unit-CI." }, { "id": "LDD-RUNTIME-001", "title": "Полный стек поднимается и проходит E2E smoke", "criticality": "P1", "status": "active", "requirement": "docs/16-stage6-report.md, §3.3", "claim": "На чистой БД стартуют все сервисы, health=UP, admin setup/login работает, создаются группа/курс/преподаватель/лаба, чужой преподаватель получает 403, аудит доступен.", "oracle": "HTTP health/status/JSON на реальном стеке", "runtime": [ "runtime.stack_health", "runtime.admin_setup_login", "runtime.course_flow", "runtime.foreign_teacher_denied", "runtime.audit_visible", "runtime.web_login_page" ], "limitations": "Smoke не заменяет полный UI E2E и нагрузочные тесты." }, { "id": "LDD-MUTATION-001", "title": "Мутационная прочность матрицы доступа", "criticality": "P1", "status": "active", "requirement": "docs/evidence/README.md", "claim": "PIT по CourseAccessService убивает не менее 70% покрытых мутаций.", "oracle": "PIT mutations.xml: killed / covered", "runtime": [ "mutation.course_access" ], "limitations": "PIT 1.20 + JUnit4 harness; Kotlin/Spring-специфичные мутации и эквивалентные мутанты не входят в цель." }, { "id": "LDD-MUTATION-002", "title": "Мутационная прочность планировщика повышения", "criticality": "P1", "status": "active", "requirement": "docs/evidence/README.md", "claim": "PIT по PromotionPlanner убивает не менее 60% покрытых мутаций.", "oracle": "PIT mutations.xml: killed / covered", "runtime": [ "mutation.promotion_planner" ], "limitations": "Проверяется планировщик, не транзакционный apply/rollback (они покрыты интеграционными тестами)." }, { "id": "LDD-BROWSER-001", "title": "Браузерный E2E админа, преподавателя и студента, включая сдачу и mobile", "criticality": "P1", "status": "active", "requirement": "docs/evidence/README.md", "claim": "Playwright на реальном стеке: admin видит курс и аудит; teacher открывает курс/ведомость/профиль, создаёт главу и видит mobile-вид; student сдаёт файл; аноним и студент не попадают в админку.", "oracle": "Playwright assertions на DOM/URL", "runtime": [ "browser.e2e" ], "limitations": "4 сценария, один Chromium; нет визуальной регрессии и Safari/Firefox." }, { "id": "LDD-RUNTIME-002", "title": "Нагрузочный smoke чтения курса, лаб, профиля и аудита", "criticality": "P1", "status": "active", "requirement": "docs/evidence/README.md", "claim": "100 параллельных чтений (10 workers) через gateway дают 0 ошибок и p95 < 2 c.", "oracle": "HTTP status + latency percentiles", "runtime": [ "runtime.load_gradebook_profile" ], "limitations": "Smoke, не benchmark; одна машина и синтетические данные, без SLA production." }, { "id": "LDD-PROM-006", "title": "Конкурентные apply/rollback одного запуска повышения", "criticality": "P0", "status": "active", "requirement": "docs/07-groups-courses.md, §7", "claim": "Два одновременных apply не дублируют историю; два одновременных rollback дают ровно один успех и один конфликт, состояние восстанавливается один раз.", "oracle": "Результаты двух потоков, количество APPLY/ROLLBACK и history", "evidence": [ "ru.ysu.lms.identity.promotion.PromotionConcurrencyIntegrationTest#concurrent apply of one run is idempotent and writes history once()", "ru.ysu.lms.identity.promotion.PromotionConcurrencyIntegrationTest#concurrent rollback allows exactly one rollback and restores once()" ], "limitations": "Два потока в одном JVM; распределённая нагрузка и несколько инстансов проверяются advisory lock-ом, но не chaos-тестом." }, { "id": "LDD-PROM-007", "title": "Транзакционный apply/rollback на сгенерированных когортах", "criticality": "P1", "status": "active", "requirement": "docs/07-groups-courses.md, §7", "claim": "Для 10 сгенерированных когорт apply приводит группу к плану, а rollback возвращает точный before-state и активный статус студента.", "oracle": "Сравнение состояния группы/студента до, после apply и после rollback", "evidence": [ "ru.ysu.lms.identity.promotion.PromotionApplyRollbackPropertyTest#generated cohorts apply to plan and rollback to before-state()" ], "limitations": "10 детерминированных сценариев в одной транзакции; не заменяет property-генератор по всем полям." } ] }