Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
46 KiB
Ledger Среза 1 (справочники) — SDD фазы 1 и 2, финальное ревью
Скопирован из .superpowers/sdd/ перед удалением рабочих каталогов. Ветка feat/5-slice-1-directories, база Среза 44806c8, HEAD на момент копирования 7f891dd (+ этот коммит).
Маркеры: T. — находки ревью задач; C/I/M-n — финальное ревью; R-n — рулинги (design #n+11 для R-12..., см. design.md); O-n — наблюдения.
Фаза 1
SDD ledger — plan: docs/superpowers/plans/2026-10-04-slice-1-directories-phase1.md
Spec: openspec/changes/slice-1-directories/ (reachable). Branch feat/5-slice-1-directories, worktree .wt/slice-1/arm-core. Base at start: 44806c8.
Нумерация: R-1 уже в design.md (#12). Рулинги контроллера с R-2; decisions в design.md с №13.
Модели: T1,T2,T4,T6,T8 + ревью — opus; T3,T5,T7 + ревью — sonnet.
Pre-flight: таблица конфликтов
| Пара/задача | produces vs consumes | находка |
|---|---|---|
| T1 -> всё | RelatedGroupMember.kind, *_CODE_SEQ, AuditAction.* |
T5/T8 импортируют COMPANY_GROUP_CODE_SEQ/RELATED_GROUP_CODE_SEQ из src/models/directories.py — совпадает |
| T2 -> T3,T5..T8 | get_async_session (commit), DomainError, RequestAudit, Page/PageParams |
T3 override get_async_session через begin_nested — совместимо с R-1. tasks.md 1.2 говорит get_uow_session — расхождение уже снято R-1 (design #12). dev_auth/auth.current_user используют get_async_session, коммитов не делают (проверено grep) |
| T2 | существующий common/exceptions.py (NotFoundError(Exception), CouldntCreateError) |
NotFoundError перепишется в подкласс DomainError; CouldntCreateError нигде не используется (grep) — удалить безопасно |
| T3 -> T5..T8 | фабрики make_*, auth_client(email), app |
сигнатуры совпадают с использованием в T5/T6/T8 тестах (office=, group=, related_group=) |
| T4 -> T5,T6,T8 | VisibilityScope, *_visible, office_for_create |
legal_entity_is_visible/company_group_is_visible в репозиториях не нужны (фильтр в SQL) — остаются как plan-mandated pure helpers; не дефект |
| T5 -> T6,T7,T8 | repositories/directories.py, get_company_group(scope), get_scope, next_code |
T6 и T8 добавляют в тот же файл/роутер: последовательно, конфликтов нет |
| T6 -> T7,T8 | get_legal_entity(scope), close_memberships |
T7 добавляет маршруты в legal_entities.py T6; T8 использует get_legal_entity |
| T5 vs spec | plan Task 5 тест 6: q по имени; spec directories-api: q по name/crm_id/code |
R-2: спека главнее — q ищет по name/crm_id/code |
| T5 vs spec | plan не указывает порядок history; spec: по убыванию valid_from |
R-3: history ГК (и ЮЛ) — по убыванию valid_from |
| T6 vs spec | spec q ЮЛ: подстрока имени/ИНН; plan: q без уточнения |
R-4: q ищет по name и inn |
| T7 vs spec | plan LEGAL_FORMS шире spec (ООО/АО/ПАО/ЗАО/ИП) |
§3 «ОПФ (ООО, АО, ПАО, ИП…)» — многоточие, расширенный список допустим; не конфликт |
| T8 vs spec | plan delete удаляет закрытые строки состава; spec молчит |
допустимо (OQ-34), FK иначе блокирует; без рулинга |
| внутренняя согласованность T1..T8 | тесты vs код vs файлы | T1: файлы перечислены полностью; тест уникальности ГСК+ГСЗ согласован с индексом. T2: интеграционный тест через scratch_db_url согласован с кодом. T3–T8: тесты соответствуют интерфейсам. Противоречий нет |
| Global Constraints vs rubric | verbatim-дубли/пустые тесты | не обнаружено |
Rulings (pre-flight):
- R-2:
qв списке ГК ищет по name/crm_id/code (спека) — план говорит только об имени — цена: нет, надмножество. (design #13) - R-3: история ГК/ЮЛ отдаётся по убыванию
valid_from(спека) — цена: нет. (design #14) - R-4:
qЮЛ ищет по name и inn (спека) — цена: нет. (design #15)
Task 1: dispatched (opus, implementer ab4cdc613d6744c3f), BASE=13d11c0
Task 1: implementer DONE_WITH_CONCERNS (commit 458392e). Ruling: R-5 (design #16) — миграция 0002 пересоздаёт related_group_member (copy→drop→create в порядке DBML→insert), а не ADD COLUMN: тест Среза 0 сверяет порядок колонок с create_all; цена ошибки — вернуть ADD COLUMN и ослабить сравнение. Также поправлены test_dbml_parity и test_offline_sql (offline UPDATE вместо INSERT) — вынужденно. Dispatching review (opus).
Task 1: review (opus) — spec OK, quality Approved; Critical/Important: none.
T1.1 minor: нет LOCK TABLE перед CTAS в 0002 (потеря параллельной записи; сейчас риск 0) — deferred
T1.2 minor: пересоздание теряет GRANT/RLS/триггеры вне Alembic; нет в docstring миграции — deferred (отражено в design #16)
T1.3 minor: docstring downgrade не упоминает падение при ЮЛ в ГСК и ГСЗ одновременно — deferred
T1.4 minor: тест сохранения данных не проверяет legal_entity_id/valid_from/created_by — deferred
T1.5 minor: в плане -k related_group_member не выбирает тестов (использовали -k create_all) — deferred
Task 1: complete (commits 13d11c0..458392e, review clean; R-5 = design #16 в 3a0ee80, D-1 уточнён c80e029)
Task 2: dispatched (opus, implementer aab564bc92cf06234), BASE=c80e029
Task 2: implementer DONE (commit 5976986). Ruling R-6 (design #17): SessionDep (scope="function") — обязателен для всех следующих задач (session: SessionDep из src/api/dependencies/db.py; guard-тест). Прочие решения T2 (№2-5 отчёта: extra с code/message → ValueError, статусы через fastapi.status, константы Page) — мелкие, приняты. Observation O-1: tasks.md 1.2 всё ещё называет get_uow_session/get_audit_service (устарело по R-1/R-6; править при архивации). O-2: индекс codegraph отстаёт — Read/Grep.
Task 2: review (opus) — spec OK, Approved; Critical/Important: none.
T2.1 minor: тест одной сессии не проверяет общность сессии current_user и маршрута (держится на ключе кэша FastAPI + guard-тесте) — deferred
T2.2 minor: guard-тест обходит только build_api_router, не src.app.app.routes — deferred
T2.3 minor: tasks.md 1.2 называет get_uow_session/get_audit_service (устарело) — deferred (правка текста tasks.md при финале фазы; O-1)
T2.4 minor: DomainError не переживает pickle/copy (args) — deferred
Task 2: complete (commits c80e029..5976986, review clean; R-6 = design #17 в cd6cca3)
Task 3: dispatched (sonnet, implementer ac5cf3be1f90b1c92), BASE=cd6cca3
Task 3: implementer DONE (8eb8592). Concern: warning build_verifier не подавлен вне test_me (O-3 кандидат). Review dispatched (sonnet).
Task 3: review (sonnet) — spec OK, Approved; Critical/Important: none.
T3.1 minor: warning build_verifier ("Включён dev-эмитент") не подавлен в общей фикстуре app (только в test_me autouse) — deferred → поручено Task 5 (конфтест app: подавить через monkeypatch src.core.auth.logger, убрать локальный autouse); статус fixed фиксируется в Task 5
T3.2 minor: pytest.raises(IntegrityError) в test_factories не проверяет конкретное ограничение — deferred
T3.3 minor: AuthClientFactory импортируется из tests.conftest — deferred
T3.4 minor: docstring conftest не упоминает новые фикстуры — deferred
Task 3: complete (commits cd6cca3..8eb8592, review clean)
Task 4: dispatched (opus, implementer afd5eb6c8cf3e7fb9), BASE=8eb8592
Task 4: implementer DONE (d142c6f). Решения: legal_entities_visible через подзапрос IN (а не outerjoin) — разрешено (семантика та же); company_groups_visible требует CompanyGroup во FROM (учесть в Task 8 get_related_group — join явно); O-4: tasks.md 2.1 называет apply_scope_/assert_office_allowed (устарело, брифы опираются на plan names). Review dispatched (opus).
Task 4: review (opus) — spec OK, Approved; Critical/Important: none. Ruling R-7 (design #18): подзапрос IN в legal_entities_visible; ГК/монозаёмщики без филиала видны только ГО.
T4.1 minor: company_groups_visible молча даёт декартово произведение, если CompanyGroup нет во FROM — deferred (Task 8 get_related_group обязан join'ить CompanyGroup явно; передано в бриф)
T4.2 minor: тест с JOIN вызывающего проверяет только FROM, не WHERE подзапроса — deferred
T4.3 minor: лишние проверки подстрок в тесте — deferred
T4.4 minor: tasks.md 2.1 называет apply_scope_/assert_office_allowed — deferred (синхронизировать текст tasks.md в конце фазы; O-4)
Task 4: complete (commits 8eb8592..d142c6f, review clean)
Task 5: dispatched (sonnet, implementer ae35074f0d4d0fe82), BASE=01187b5
Task 5: implementer DONE (d85ecd2, 80e8905). T3.1: fixed d85ecd2. Решения исполнителя приняты: R-8 (design #19).
Task 5: review (sonnet) — spec OK, Approved; Critical/Important: none.
T5.1 minor (informational): глобальный pre-check crm_id раскрывает существование строки crm_id — rejected как дефект (спека требует глобальную уникальность 409), записан в design #19
T5.2 minor: эндпоинт regional_offices вызывает repo напрямую (мимо services) — deferred
T5.3 minor: эндпоинт ГК пересобирает PageParams/CompanyGroupFilter из CompanyGroupListQuery — deferred
T5.4 minor: set_archived(bool) с noqa FBT вместо двух методов — deferred
T5.5 minor: OFFICE_NOT_FOUND не проверяет is_active (учтено в design #19) — deferred
T5.6 minor: пробелы тестов (multi-field PATCH, повторный archive без аудита, неизменность чужой ГК, collation в сортировке) — deferred
T5.7 minor: conftest заменяет logger заглушкой только с warning (logger.disable устойчивее) — deferred
Task 5: complete (commits 01187b5..80e8905, review clean)
O-5: RegionalOfficeOut дублируется в schemas/me.py и schemas/directories.py — OpenAPI-компоненты с длинными именами; решить в фазе 2 (фронт/OpenAPI).
Task 6: dispatched (opus, implementer a8549ee04ec10f373), BASE=69769cc
Task 6: implementer DONE (347b46e). Review (opus): spec OK, Needs fixes.
T6.1 IMPORTANT: office_for_create применяется к regional_office_id до сравнения с текущим — PATCH с тем же значением от пользователя филиала даёт 422 (нарушает Review Focus 1) — fix round 1 dispatched (resume implementer a8549ee04ec10f373); fix base 347b46e
T6.2 minor (M-1): тест отката не доказывает откат аудита (аудит пишется после close_memberships) — deferred
T6.3 minor (M-2): сообщение OFFICE_NOT_ALLOWED про ГК возвращается и для ЮЛ — deferred
T6.4 minor (M-3): дублирование _strip_required_name/_check_office_exists/_now между ГК и ЮЛ — deferred (вынести в Task 8 если третий потребитель)
T6.5 minor (M-4): pre-check INN/CRM_ID не защищён от гонки (IntegrityError → 409 общим обработчиком, текст общий) — deferred
Task 6: fix round 1/5 (1 addressed, 0 open — T6.1 fixed 462a30f; commits 347b46e..462a30f); re-review (sonnet): all addressed, no new Critical/Important.
T6.6 minor (re-review): явный null от пользователя филиала при текущем офисе NULL пропускается как no-op (до фикса — превращался в свой офис); не покрыто тестом — deferred
Task 6: complete (commits 69769cc..462a30f, review clean after 1 fix round). Решения исполнителя приняты: R-9 (design #20).
Task 7: dispatched (sonnet, implementer aa1f12b4cd4ecdaac), BASE=e03f979
Task 7: implementer DONE (7007c51, a19875d). Решения: ALIAS_TAKEN отдаёт только legal_entity_id владельца; чужой/несуществующий алиас — одинаковый 404; alias хранится дословно, пробельная строка → EMPTY_ALIAS. O-6: DIRECTORY_DELETE по seed только у ADMIN (DELETE алиасов/ГСК доступен только admin) — матрица Среза 0, не менялась; отметить человеку. Review dispatched (sonnet).
Task 7: review (sonnet) — spec OK, Approved; Critical/Important: none. Решения исполнителя приняты: R-10 (design #21).
T7.1 minor: гонка двух POST одного алиаса → IntegrityError → общий 409 без кода ALIAS_EXISTS (в отчёте исполнителя ошибочно указано 500) — deferred
T7.2 minor: find_alias_owner LIMIT 1 при теоретических дублях может дать ALIAS_TAKEN вместо ALIAS_EXISTS — rejected (сервис сам не допускает дублей; только гипотетические legacy-данные)
Task 7: complete (commits e03f979..a19875d, review clean)
Task 8: dispatched (opus, implementer a8179e82b2dfb35b5), BASE=69d641c
Task 8: implementer DONE (e3ea074, 88a4b6d, a04d9db). Решения: add_member возвращает (член, ЮЛ); список ГСК/ГСЗ массивом (не Page); DELETE members = DIRECTORY_EDIT по спеке; состав в before_value при удалении группы; новый src/common/clock.py (utc_now). Review dispatched (opus).
Task 8: review (opus) — spec OK, Approved; Critical/Important: none. Решения исполнителя приняты: R-11 (design #22).
T8.1 minor: удаление группы, на которую ссылаются consolidation/ex_snapshot (FK), даст 500 вместо 409 — deferred (к OQ-34, Срез 3/7: GROUP_IN_USE)
T8.2 minor: гонки pre-check → IntegrityError → общий 409/500 без доменного кода (общая проблема всех сервисов справочников; см. T6.5, T7.1) — deferred
T8.3 minor: список ГСК/ГСЗ — массив, а не Page (Global Constraints: списки {items,total,limit,offset}) — deferred (решить в фазе 2 до фронта)
T8.4 minor: нет теста DELETE чужой ГСК пользователем филиала — deferred
Task 8: complete (commits 69d641c..a04d9db, review clean)
O-1/O-4: tasks.md 1.2/2.1 приведены к фактическим именам (commit после design #22).
Фаза 2 и финальное ревью
SDD ledger — plan: docs/superpowers/plans/2026-10-04-slice-1-directories-phase2.md
Spec: openspec/changes/slice-1-directories/ (reachable). Branch feat/5-slice-1-directories, worktree .wt/slice-1/arm-core. Base фазы 2: b5a8324 (начало Среза 1: 44806c8).
Ledger фазы 1: ../2026-10-04-slice-1-directories-phase1/progress.md (T1.1–T8.4, O-1…O-5, R-2…R-11).
Нумерация: рулинги R-12 (design #23), R-13 (design #24) приняты сессией; далее R-14 (design #25+). Наблюдения O-7+ (O-6 фазы 1 — DIRECTORY_DELETE только у ADMIN). Находки T9.k…T15.k.
Модели: T9,T10,T12,T14 + ревью — opus; T11,T13,T15 + ревью — sonnet; финальное ревью — opus.
Pre-flight: таблица конфликтов
| Пара/задача | produces vs consumes | находка |
|---|---|---|
| T9 -> T10 | ядро plan_import, ImportReport.to_dict, read_rows, FileFormatError, операции |
T10 потребляет все операции и DirectorySnapshot; совпадает. Снимок строит T10 (load_snapshot) |
| T10 vs код фазы 1 | план требует apply_group_update(session, audit, group, *, name, office_id, now) и apply_entity_update(...) «вынесенные» из сервисов; в коде есть только классы CompanyGroupService/LegalEntityService (функций нет) |
R-14 (ниже): T10 выносит внутреннюю логику update в модульные функции в services/*.py, CompanyGroupService.update/LegalEntityService.update вызывают их; поведение API и тесты фазы 1 не меняются |
| T10 vs spec | план: dry_run «await session.rollback() … в тестах — откат SAVEPOINT»; фикстура app оборачивает запрос тестов в begin_nested |
rollback внутри SAVEPOINT-обёртки корректен; T10 обязан убедиться тестом подсчёта строк, что откат реально откатывает вставки (а не только expire) |
| T10 vs T9 (Review Focus 4) | повторный dry_run расходует nextval |
допустимо (дыры в кодах), тест сравнивает отчёты, не коды |
| T10 vs спека | план: «storage is None — S3Object не создаётся» |
согласовано с dev без S3; не конфликт |
| T11 -> T10 | fixtures-тест использует эндпоинт T10; пороги offices_created == 53, groups_created == 345, UNSUPPORTED_TYPE ≥ 500 |
цифры плана не проверены — ожидание исполнителю: сверить с фактом и при расхождении с планом описать (не подгонять код под числа без анализа) -> R-14b? нет: ruling не нужен, фиксируется в отчёте |
| T12 vs R-13 | UserOut.regional_office: RegionalOfficeOut |
R-13 применяется в T12 (одна модель) |
| T12 vs spec users-admin | план: LAST_ADMIN при снятии роли/права/деактивации; спека D-8 «защита от снятия USERS_ADMIN с себя/последней роли» |
план строже/равен спеке; смысл — «последнее право USERS_ADMIN у активных»; проверка перед записью. Конфликта нет |
| T12 vs Global Constraints | «пользователей не удалять; email lower unique» — в DBML уникальность email? | исполнитель проверяет DBML/индекс (lower(email)); если уникальный индекс только по email — проверка на уровне сервиса (план так и требует) |
| T12 -> T13/T14 | UserOut, RoleOut, PermissionOut, Page[UserOut], /directories/regional-offices |
T13 types.ts дублирует контракт вручную; T14 использует. Совпадение имён полей проверяет ревью T13/T14 по схемам T12 |
| T13 -> T14 | apiFetch, ApiError, useMe, AuthGate, DevLogin |
T14 использует useMe/api; Admin.tsx вызывает useMe() повторно (второй запрос /me) — допустимо; ревью T14 проверит отсутствие запросов /admin/* без права |
| T13 vs Global Constraints | web/src/utils.ts «удалить или не трогать» |
не трогать, если не мешает; не дефект |
| T14 vs spec | spec users-admin «минимальный экран»: sidebar-группа «Система» | совпадает с планом |
| T15 vs T9..T14 | README/CLAUDE.md документируют фактические имена; tasks.md все [x]; design.md — рулинги |
рулинги вносит контроллер по ходу (после каждой задачи), T15 лишь сверяет |
| T15 vs scope | make check-full требует ../fixtures/../docs, compose, docker |
доступны по постановке |
| внутренняя согласованность T9 | тест -k/ожидания: report.rows==21, groups_created==1, offices_created==1, entities_created==18, ГСК с 2 членами — план велит «зафиксировать по факту», не угадывать |
OK; type_raw для ФЛ: UNSUPPORTED_TYPE; 21 строка: 18 ЮЛ + ФЛ/дубль — по факту |
| внутренняя согласованность T10 | тест «срез РБ»: «в БД 1 ГК, 18 ЮЛ, 1 ГСК (2 члена)» vs Task 9 | совпадает |
| внутренняя согласованность T12 | Review Focus 5: самодеактивация | покрыт тестом и требованием |
| Global Constraints vs rubric | verbatim-дубли/пустые тесты | не обнаружено |
Rulings (pre-flight):
- R-12 (design #23), R-13 (design #24): приняты сессией.
- R-14: T10 выносит update-логику ГК/ЮЛ в модульные функции
apply_group_update/apply_entity_update(в плане названы как существующие, но их нет) — альтернатива «дублировать код update в сервисе импорта» нарушает DRY; цена ошибки: небольшой рефакторинг фазы 1 в T10, регрессия закрыта тестами фазы 1; откат — вернуть дублирование. (design #25 — будет внесён после T10 вместе с остальными решениями исполнителя)
Task 9: dispatched (opus, implementer a92fc138de6b6b79f), BASE=dd176a3
Task 9: implementer DONE_WITH_CONCERNS (df8a528). Concerns: фактически 20 ЮЛ/ИП (не 18), DUPLICATE_IN_FILE в файле РБ не возникает; полная выгрузка: groups_created=344 (ГК 1-5AL0CDM целиком DUPLICATE_IN_FILE), LE_IN_OTHER_GROUP невозможен на пустой БД. Пины openpyxl, et_xmlfile, types-openpyxl. 12 решений исполнителя (report).
Rulings: R-14 (design #25) apply_group_update/apply_entity_update выносятся в T10; R-15 (design #26) 20 ЮЛ/ИП, groups_created=344, LE_IN_OTHER_GROUP на пустой БД недостижим — спека/план/tasks.md исправлены; R-16 (design #27) решения исполнителя T9 приняты. Commit chore: см. git log (после df8a528).
Task 9: review (opus) — spec OK; Needs fixes.
T9.1 IMPORTANT (I-1): повреждённый xlsx (битый sheet/workbook/styles XML, неверная ссылка ячейки) → сырое ParseError/ValueError вместо FileFormatError UNSUPPORTED_FILE — fix round 1
T9.2 IMPORTANT (I-2): нет лимита строк/размера — xlsx 5 КБ с r="2000000" съедает 297 МБ (материализация пустых строк) — fix round 1 (стриминг, лимит строк, проверка суммарного zip file_size)
T9.3 minor (M-1): числовой ИНН теряет ведущий ноль (0274… → 9 цифр → INVALID_INN, видимый отказ) — deferred (follow-up/OQ-88)
T9.4 minor (M-2): csv row = номер записи, а не строки файла (reader.line_num) — включено в fix round 1 (дёшево)
T9.5 minor (M-3): CreateRelatedGroup возможна без последующего AddMember (пустая группа) — deferred, зафиксировано в design #27
T9.6 minor (M-4): пустые Организация/Группа компаний при непустом iD создают сущность с name="" — deferred (вопрос OQ; допущение в README Task 15)
T9.7 minor (M-5): при существующей ГК + skip новый РФ из файла всё равно создаётся (CreateOffice без ссылок) — deferred (буквальный порядок §13.2)
T9.8 minor (M-6): параметризованный тест не покрывает DUPLICATE_IN_FILE/LE_UPDATED/GK_UPDATED/in-file GK_NAME_CONFLICT (покрыты отдельными тестами); нет теста GK-обновление + LE_IN_OTHER_GROUP — deferred
Task 9: fix round 1/5 dispatched (resume a92fc138de6b6b79f); FIX_BASE=7f14e70
Task 9: fix round 1/5 (3 addressed, 0 open — T9.1 fixed 1364f43, T9.2 fixed 1364f43, T9.4 fixed 1364f43; commits 7f14e70..1364f43); re-review (sonnet): all addressed, no new Critical/Important.
T9.9 minor (re-review): workbook.close() в генераторе _xlsx_rows происходит при GC, не детерминированно (BytesIO, fd не течёт) — deferred
T9.10 minor (re-review): широкий кортеж исключений вокруг итерации может скрыть программную ошибку как «плохой файл» — deferred
T9.11 minor (re-review): MAX_UNCOMPRESSED_BYTES=200 МиБ щедрый; sharedStrings грузится целиком; не ограничены колонки/длина ячейки — deferred (кандидат на 50 МиБ)
O-7: MAX_IMPORT_ROWS=100_000 считает и пустые строки: xlsx с отформатированными пустыми хвостами >100k будет отвергнут (UX: пояснить в README Task 15).
Task 9: complete (commits dd176a3..1364f43, review clean after 1 fix round). Решения исполнителя: R-16 (design #27).
Task 10: dispatched (opus, implementer ad5722f28c02ecade), BASE=1364f43
Task 10: implementer DONE_WITH_CONCERNS (e373dfc refactor R-14, c9c3b69 feature). Concerns: rollback под SAVEPOINT-фикстурой сносит seed — dry_run-тест на отдельной БД; 413 после spool multipart Starlette; архивные ГК в снимке; HTTPException(413) из сервиса. 12 решений (report). Review dispatched (opus).
Task 10: review (opus) — spec OK, Approved; Critical/Important: none. Решения исполнителя приняты: R-17 (design #28).
T10.1 minor: dry_run-тест не проверяет FakeStorage.objects == {} («ни файла») — deferred → pre-final cleanup batch (дёшево, инвариант)
T10.2 minor: нет теста dry_run=true на файле РБ (спека «Реальная выгрузка принята») — deferred → cleanup batch
T10.3 minor: нет теста dry_run + on_existing=update (откат истории/аудита) — deferred → cleanup batch
T10.4 minor: safe_filename не ограничивает длину/управляющие символы → NUL или >1024 байт даст 500 вместо 422 — deferred → cleanup batch
T10.5 minor: HTTPException(413) в сервисном слое (по брифу) — deferred
T10.6 minor: committed_sessionmaker/committed_client в файле теста, дублируют фабрику conftest — deferred → cleanup batch (вынести в conftest)
T10.7 minor: архивные ГК в снимке, при update переименовываются без разархивации; OQ — deferred (design #28; вопрос человеку: добавить в 07-open-questions.md в ../docs)
T10.8 minor: update-режим на масштабе: ≈3 round-trip на изменённую строку; 1.1 с измерено только для skip — deferred (профилировать в Task 11 по необходимости)
T10.9 minor: LE_IN_OTHER_GROUP раскрывает имя ЮЛ другой ГК при выдаче DIRECTORY_ADMIN филиальному пользователю — deferred (design #28)
T10.10: решения исполнителя перенесены в design #28 — fixed (commit chore R-17)
Task 10: complete (commits 1364f43..c9c3b69, review clean)
Task 11: dispatched (sonnet, implementer aede0ffb3cd46d81b), BASE=58075e9; в диспетчеризацию включён cleanup T10.1–T10.4, T10.6 (commit A) + тест полной выгрузки (commit B)
Task 11: implementer DONE (c793b42 cleanup T10.1-4,6; c9cd839 test). dry_run 8400 строк = 1.14 с, тест не skip. Review dispatched (sonnet).
Task 11: review (sonnet) — spec OK, Approved; Critical/Important: none. Решения исполнителя приняты: R-18 (design #29). T10.1, T10.2, T10.3, T10.4, T10.6 — fixed c793b42.
T11.1 minor: print с замером времени (noqa T201) вместо record_property/logger — deferred
T11.2 minor: таймер охватывает только HTTP-запрос, в docstring не оговорено — deferred
T11.3 minor: safe_filename чистит только Cc; Cf (bidi U+202E, zero-width) остаются в имени/аудите/ключе S3 — deferred (design #29)
T11.4 minor: после усечения имя может кончаться пробелом перед расширением — deferred
T11.5 minor: кейс «nul» в HTTP-тесте проходит и на старом коде (регрессию ловит unit-тест) — deferred
O-8: полная выгрузка 8400 строк, dry_run = 1.14 с (лимит 60 с).
Task 11: complete (commits 58075e9..c9cd839, review clean)
Task 12: dispatched (opus, implementer a696b70ed168ae6f6), BASE=83746e6
Task 12: implementer DONE (4dd966b R-13 refactor, 800f3a4 feature). Отклонение от R-13: RegionalOfficeBrief (для /me) + RegionalOfficeOut(Brief) с is_active в schemas/directories.py (иначе /me получает is_active и ломает тест); 422 OFFICE_NOT_FOUND (R-8); FOR NO KEY UPDATE на строке права/роли (FOR UPDATE дедлочит на FK аудита); EmailStr отвергает .local; гонка email с разным регистром (open). Review dispatched (opus).
Task 12: review (opus) — spec OK, Approved; Critical/Important: none. Решения исполнителя приняты: R-19 (design #30), включая отклонения от R-13 (иерархия Brief/Out) и 422 OFFICE_NOT_FOUND.
T12.1 minor: не покрыта ветка kept_roles (замена ADMIN на другую активную роль с USERS_ADMIN → 200; на неактивную → 409) — deferred → cleanup batch
T12.2 minor: гонка проверена только set_roles×set_roles — deferred (опц.)
T12.3 minor: тест экранирования _ не доказывает экранирование (нет строки, различающейся только в позиции _) — deferred → cleanup batch
T12.4 minor: нет assert кода LAST_ADMIN в test_api_admin.py:403 — deferred → cleanup batch
T12.5 minor: USER_ROLE_CHANGE before сортирован, after в порядке запроса — deferred (задокументировано, design #30)
T12.6 minor: _check_office_exists теперь в трёх сервисах — deferred (вынести в repositories/directories)
T12.7 (concerns исполнителя): гонка создания email в разном регистре → оба создаются, логин → 403 fail-closed; одинаковый email-гонка → общий 409 без EMAIL_EXISTS (семейство T6.5/T7.1/T8.2); EmailStr отвергает .local; UserOut.roles показывает неактивные роли — deferred; общий маппинг IntegrityError пересмотреть в final
O-9: EmailStr не принимает домен .local — dev-пользователи вида x@armka.local через API/UI создать нельзя (seed-dev создаёт напрямую); упомянуть в README (Task 15).
Task 12: complete (commits 83746e6..800f3a4, review clean)
Task 13: dispatched (sonnet, implementer a3c5011bf4b5363d7), BASE=6217e4e
Task 13: implementer DONE_WITH_CONCERNS (498450b). biome/tsc/build/web-check зелёные; UI в браузере не проверялся (смоук клиента из Node против API); NODE_ENV литералом из-за подмены process.env в rsbuild.config; AuthGate вокруг Layout; MeContext. Review dispatched (sonnet).
Task 13: review (sonnet) — spec OK; Needs fixes.
T13.1 IMPORTANT: apiFetch не оборачивает сетевую ошибку (TypeError: Failed to fetch) — пользователь видит английский текст, будущие экраны получат необработанный reject — fix round 1
T13.2 IMPORTANT: apiFetch: response.json() на пустом/не-JSON 2xx теле бросает сырой SyntaxError — fix round 1
T13.3 IMPORTANT: DevLogin в prod-ветке теряет error (тупик при ошибке /me со старым токеном) — fix round 1 (показать Alert + «Повторить»)
T13.4 minor: void reloadCount; в useMe — deferred
T13.5 minor: на armka:unauthorized error не выставляется («Сессия истекла») — включено в round 1 (дёшево)
T13.6 minor: email не trim-ится, whitespace проходит required — включено в round 1
T13.7 minor: height:'100%' формы входа не работает (#root min-height) — включено в round 1
T13.8 minor: токен в памяти параллельно с sessionStorage — deferred (добавить комментарий о приоритете — включено)
T13.9 minor: Permission.object_type/action — string, а не union — включено в round 1
T13.10 minor: API_BASE при Config.API_URL='/' даст //api/v1 — включено в round 1 (trim trailing slash)
Task 13: fix round 1/5 dispatched (resume a3c5011bf4b5363d7); FIX_BASE=498450b
Task 13: fix round 1/5 (3 Important + 7 minors addressed, 0 open — T13.1, T13.2, T13.3, T13.5, T13.6, T13.7, T13.8, T13.9, T13.10 fixed d76ca57; commits 498450b..d76ca57); re-review (sonnet): all addressed, no new Critical/Important. T13.4 deferred.
O-10 (re-review): в prod-ветке «Повторить» без токена — no-op (useMe при отсутствии токена не сбрасывает error); сетевой сбой при чтении тела (response.text()) не оборачивается в NETWORK — deferred (редкие случаи)
Task 13: complete (commits 6217e4e..d76ca57, review clean after 1 fix round). Решения исполнителя: R-20 (design #31).
Task 14: dispatched (opus, implementer aee7f102e715b6674), BASE=a7502f6
Task 14: implementer DONE (d3c2f8a). biome/tsc/build/web-check зелёные; UI прогнан в headless Chrome через DevTools (смена ролей, LAST_ADMIN, создание, матрица, partial failure, 403 без запросов /admin/). Concerns: Redux scaffold 'Store does not have a valid reducer' в консоли (O-11), нет поиска/пагинации >200, чанк antd 887 кБ. Review dispatched (opus).
Task 14: review (opus) — spec OK; Needs fixes (1 Important).
T14.1 IMPORTANT (I-a): UsersTable.tsx:603 disabled: !r.is_active — пользователь с неактивной ролью не может её снять (rc-select: у disabled-опции нет крестика; сервер даст 422 UNKNOWN_ROLE на повторную отправку) — fix round 1 (disabled: !r.is_active && !user.roles.includes(r.code))
T14.2 minor (M-a): PermissionMatrix — ответы PUT отбрасываются, при сбое reload после сохранения показывается старое состояние — включено в round 1 (merge RoleOut через setData до reload)
T14.3 minor (M-b): неверный doc-комментарий hooks.ts (data при неудачном reload сохраняется) — включено в round 1
T14.4 minor (M-c): CreateUserModal без ref-lock от двойного клика — включено в round 1
T14.5 minor (M-d): in/draft[code] по plain object цепляет Object.prototype — включено (Object.hasOwn)
T14.6 minor (M-e): memo(Section) бесполезен без useMemo(visibleGroups) — включено
T14.7 minor (M-f): множественный выбор ролей = N PUT и N аудит-событий (не оптимистично) — deferred, README (Task 15)
O-11: консольная ошибка Redux «Store does not have a valid reducer» — pre-existing каркас (store.ts reducer: {}), не вводилась; deferred (follow-up).
Task 14: fix round 1/5 dispatched (resume aee7f102e715b6674); FIX_BASE=d3c2f8a
Task 14: fix round 1/5 (1 Important + 5 minors addressed, 0 open — T14.1..T14.6 fixed 4d719ec; commits a7502f6..4d719ec); re-review (sonnet): all addressed, no new Critical/Important. T14.7 deferred (README в Task 15).
Task 14: complete (commits a7502f6..4d719ec, review clean after 1 fix round). Решения исполнителя: R-21 (design #32).
Task 15: dispatched (sonnet, implementer ad359cdca9bdba5a0), BASE=fda907b; commit A = cleanup T12.1/T12.3/T12.4 (tests), commit B = docs+OpenAPI test+check-full
Task 15: implementer DONE (d241c07 cleanup T12.1/3/4; 7b75981 docs+OpenAPI test). check-full зелёный, 0 skipped: unit 764 passed, integration 264 passed, fixtures 266 passed/0 skipped (integration or fixtures); docker-build, web-check OK. Расхождения брифа: (1) неактивная роль с USERS_ADMIN через API = 422 UNKNOWN_ROLE (не 409 LAST_ADMIN); (2) повторный seed ВОЗВРАЩАЕТ права, снятые админом (ON CONFLICT DO NOTHING на вставку) — README описывает реальность. Review dispatched (sonnet).
Task 15: review (sonnet) — Approved with notes (документация сверена с кодом, check-full полный и зелёный: unit 764, integration 264, fixtures 266/0 skipped); Critical: none.
T15.1 IMPORTANT: design.md строка «Повторный seed не отзывает права» противоречила коду/README (на самом деле возвращает) — fixed (commit chore design.md/tasks.md, controller)
T15.2 IMPORTANT: tasks.md 5.1 [x] включал пункт archive и несуществующий get_uow_session — fixed (5.1 переформулирован, archive вынесен в 5.2 [ ] «после merge, человек»)
T15.3 minor: README импорт шаг 4 «все строки получают *_EXISTS» неточно (ранее пропущенные строки остаются ошибками) — fix в финальной волне
T15.4 minor: test_components_have_no_duplicated_models — count==1 по ключам словаря бессмыслен (реальную защиту даёт проверка "__"/"-") — fix в финальной волне
T15.5 minor: exact-set тест маршрутов хрупок по замыслу — rejected (намеренно для спек-ориентированного среза)
Расхождения брифа с кодом (приняты, факты в README): повторный seed возвращает права; неактивная роль с USERS_ADMIN через API = 422 UNKNOWN_ROLE.
Task 15: complete (commits fda907b..7b75981 + controller doc fix)
Pre-final: IntegrityError->coded 409 + T15.3/T15.4 dispatched (sonnet a1c561f3c0871b3bb), BASE=def3d72 (закрывает T6.5, T7.1, T8.2, T12.7 если пройдёт ревью)
Pre-final fix (commit 070a398): общий маппинг IntegrityError(23505) → coded 409 (7 ограничений, реестр constraint_conflicts.py), 11 новых тестов гонок; make check 773 unit, integration 275 passed. R-22 (design #33). T6.5 fixed 070a398, T7.1 fixed 070a398, T8.2 fixed 070a398 (для покрытых ограничений; ALIAS_TAKEN/email-в-другом-регистре/external_id остаются без кода — задокументировано в README), T12.7 (EMAIL_EXISTS-часть) fixed 070a398, T15.3 fixed 070a398, T15.4 fixed 070a398.
Final review: dispatched (opus a1d658a85175acb98), range 44806c8..811a1d2
Финальное ревью (opus): Ready to merge: With fixes. Critical: none.
C-/I-/M- маркеры:
I-1 IMPORTANT: требование спеки users-admin «администратор MUST NOT снять с себя последнюю роль» vs код/план (запрет только для последнего активного обладателя в системе) — Ruling R-23 (design #34): трактовка системная, спека/D-8/proposal приведены к коду — fixed (commit chore R-23/R-24)
M-1: multipart импорта читается до аутентификации (Starlette сбрасывает на диск до 401/403) — fix wave: требование к ingress в README (client_max_body_size) — документировано
M-2: README обещает legal_entity_id в INN_EXISTS, код не отдаёт — fix wave (README)
M-3: смена имени+филиала ГК — одно событие ASSIGNMENT_CHANGE — Ruling R-24 (design #35): оставлено — deferred
M-4: /admin зависит от DIRECTORY_VIEW (список филиалов) — fix wave: документировано в README; GET /admin/regional-offices — deferred
M-5: /me устаревает после правки админом себя — deferred (follow-up: reload профиля после правки своей строки/матрицы)
M-6: design #22 неверно описывал последствие T8.1 (409 строкой, не 500) — fixed (commit chore R-23/R-24)
M-7: в design #28 не названа цена «S3 до commit» — fixed (commit chore R-23/R-24)
M-8: CompanyGroupCreate/LegalEntityCreate без extra="forbid" — fix wave
T11.3 (bidi/Cf в имени файла) — fix wave (исключить Cf)
T9.6 (реальная выгрузка, строка 4034: пустая «Организация» → ЮЛ с name="") — deferred; OQ человеку до боевого импорта
Deferred minors triage финального ревью: все прочие T-маркеры — follow-up (см. отчёт ревью); must-fix-before-merge: только I-1 (закрыт рулингом R-23).
Final fix wave: dispatched (sonnet a05e1583424ebbad0), BASE=bea295d (M-1, M-2, M-4 README, M-8, T11.3, final make check-full)
Final fix wave (commit 7f891dd): M-1 fixed 7f891dd (README: требование к ingress), M-2 fixed 7f891dd, M-4 fixed 7f891dd (README, документировано), M-8 fixed 7f891dd, T11.3 fixed 7f891dd. Scoped re-review (sonnet): all addressed, no new Critical/Important. Final make check-full (HEAD 7f891dd): EXIT 0 — unit 776 passed/0 skipped, integration 277 passed/0 skipped, integration or fixtures 279 passed/0 skipped, dbml-check/check-infra пусто, docker-build OK, web-check OK.
Final review: clean (no open Critical/Important).
Вывод финального make check-full (сводка, HEAD 7f891dd)
cd api && /Users/andrey.epifancev/projects/awp-ca/.wt/slice-1/arm-core/api/.venv/bin/ruff check .
All checks passed!
cd api && /Users/andrey.epifancev/projects/awp-ca/.wt/slice-1/arm-core/api/.venv/bin/ruff format --check .
138 files already formatted
cd api && /Users/andrey.epifancev/projects/awp-ca/.wt/slice-1/arm-core/api/.venv/bin/mypy src scripts
Success: no issues found in 86 source files(B
cd api && /Users/andrey.epifancev/projects/awp-ca/.wt/slice-1/arm-core/api/.venv/bin/pytest -m "not integration"
platform darwin -- Python 3.11.15, pytest-9.1.1, pluggy-1.6.0 -- /Users/andrey.epifancev/projects/awp-ca/.wt/slice-1/arm-core/api/.venv/bin/python
cachedir: .pytest_cache
configfile: pytest.ini
===================== 776 passed, 277 deselected in 16.21s =====================
cd api && /Users/andrey.epifancev/projects/awp-ca/.wt/slice-1/arm-core/api/.venv/bin/python scripts/check_dbml_vs_orm.py ../../docs/model/data-model-v5.14.dbml
cd api && /Users/andrey.epifancev/projects/awp-ca/.wt/slice-1/arm-core/api/.venv/bin/python scripts/check_infra.py
cd api && /Users/andrey.epifancev/projects/awp-ca/.wt/slice-1/arm-core/api/.venv/bin/pytest -m integration
platform darwin -- Python 3.11.15, pytest-9.1.1, pluggy-1.6.0 -- /Users/andrey.epifancev/projects/awp-ca/.wt/slice-1/arm-core/api/.venv/bin/python
cachedir: .pytest_cache
configfile: pytest.ini
================ 277 passed, 776 deselected in 67.60s (0:01:07) ================
#10 exporting to image
#10 naming to docker.io/library/arm-core:latest done
cd web && npm install
cd web && npx biome check src
Checked 24 files in 21ms. No fixes applied.
cd web && npx tsc --noEmit
cd web && npm run build
ready built in 0.58 s
EXIT 0