docs(slice-4): ledger SDD по плану Среза 4 (задачи, находки, рулинги, итоговые прогоны)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
This commit is contained in:
Andrey Epifancev 2026-10-06 08:35:32 +04:00
parent 42c253146c
commit 5fd691a5f7

View File

@ -0,0 +1,174 @@
# SDD ledger — plan: docs/superpowers/plans/2026-10-06-slice-4-consolidation.md
Worktree arm-core: /Users/andrey.epifancev/projects/awp-ca/.wt/slice-4/arm-core (ветка feat/18-slice-4-consolidation, BASE ветки 714832e). Worktree docs: /Users/andrey.epifancev/projects/awp-ca/.wt/slice-4/docs (ветка feat/19-slice-4-consolidation).
Workspace: /Users/andrey.epifancev/projects/awp-ca/.wt/slice-4/arm-core/.superpowers/sdd/2026-10-06-slice-4-consolidation
Spec: openspec/changes/slice-4-consolidation/ (достижима, прочитана). SDD scripts: /Users/andrey.epifancev/.claude/plugins/cache/claude-plugins-official/superpowers/6.4.1/skills/subagent-driven-development/scripts
Pre-flight рулинги: preflight-rulings.md в workspace (R-1…R-6).
## Pre-flight scan (план ↔ план ↔ спека)
| Пара / задача | Производит → потребляет | Находка |
|---|---|---|
| T1 – T2 | `factories.py`: `make_osv_entry(kind)` / новые фабрики; `enums.py` | согласовано |
| T1 – T9/T10 | `Resolution` (T1, repositories) → `Resolver` Protocol в `domain` (T9) | домен не может импортировать репозиторий — **R-4** |
| T2 – T3/T5/T10/T11 | функции `repositories/consolidations.py` → сервисы | согласовано (сигнатуры в Interfaces T2) |
| T2 – T8 | `entity_files_state` создаётся в T2 и «модифицируется» в T8; build принимает и `files_state`, и `unbound` | дубль владения — **R-5** |
| T2 – T4/T6/T7 | `lock_versions_for_entity` | гонка потерянной привязки (confirm ∥ members, автозамена ∥ подхват, create ∥ confirm) — **R-2** |
| T3 – T5 – T6 | `consolidations.py`: заглушки `bind_confirmed`/`unbind_entity` (T3/T5) → реальные вызовы (T6) | согласовано (T6 явно «заглушки → реальные вызовы») |
| T3 – T8 | `_card`: `checklist = []` → `domain.checklist.build` | согласовано |
| T4 – T5/T11 | `ensure_version_editable` | согласовано |
| T4 – T6/T7 | `consolidation_status.transition` (автопереходы зовут её) | согласовано; версия должна быть под `FOR UPDATE` вызывающего (T6/T7 берут её через `lock_versions_for_entity`) |
| T5 – T6 | `unbind_entity` (T5: прямое удаление) → T6 | согласовано |
| T6 – T7 | оба правят `consolidation_documents.py`, `result_mapping.py`, `test_result_mapping.py`; `fits` → `on_documents_mapped` | согласовано, T7 запускается после T6 (последовательно) |
| T6 – спека | ключ замены «тройка» убивает сценарий «Нарастающий рядом с квартальными» | **R-1** |
| T6 – T2 (блокировки) | `lock_document`/`lock_file_documents` = FOR UPDATE; FK `consolidation_document → document` берёт KEY SHARE → взаимоблокировка confirm ∥ members | **R-3** |
| T8 – T9 | `checklist.py`: локальная проверка → `select_sources` | согласовано (явно в плане) |
| T9 – T10 | `build`/`Sources`/`Consolidated` → `_build` | согласовано |
| T10 – T11 – T12 | `services/consolidated_osv.py` (`_build` → `candidates` → `export`), `repositories/consolidations.py` | согласовано |
| T12 | `requirements-dev.txt` «Modify (ничего)» | шум — **R-6** |
| T13 – T1 | `test_mapping_golden` (kind) / хелпер `fixtures_helpers.map_golden_snapshot` | согласовано |
| T14 | docs в двух репозиториях | согласовано |
| Самосогласованность T1…T14 | тесты vs код vs файлы | T1: backfill SQL `'\s'` в python-строке миграции — экранировать; T3: `year=1999 → 422` ок; T4: перехват ∥ правка проверяется через `lock_version` на двух сессиях — ок; T9: `Gap(Q1,Q2,Q3)` для одного нарастающего Q2 при версии Q3 согласуется со спекой; прочее без противоречий |
## Rulings
- Ruling: R-1 (design #11) ключ замены привязки включает признак нарастающего — спека противоречит сама себе (тройка vs сценарий «Нарастающий рядом с квартальными») — вытеснение нарастающего/квартального друг другом
- Ruling: R-2 (design #12) `lock_versions_for_entity` блокирует надмножество версий + создание под advisory ГК — иначе тихо теряется привязка — редкая потеря привязки
- Ruling: R-3 (design #13) `lock_document`/`lock_file_documents` → `FOR NO KEY UPDATE` — взаимоблокировка confirm ∥ members по KEY SHARE FK — нулевая
- Ruling: R-4 (design #14) `Resolution` определён в `domain/osv_mapping.py` — домен не импортирует репозитории — нулевая
- Ruling: R-5 (design #15) `EntityFilesState` (T2, в domain/checklist.py) отдельно от `unconfirmed_documents` (T8) — план дублирует владение — нулевая
- Ruling: R-6 шум плана (`requirements-dev.txt`) не трогаем — нулевая
- Ruling: R-7 (design #16, implementer T1) колонка `counterparty_match_kind` последняя в ORM/DBML (ADD COLUMN + тест ordinal_position), а не после `counterparty_id` — механическое следствие миграции — нулевая (порядок колонок в DBML)
## Tasks
Task 1: implemented (commits arm-core 52ae1b9..eaee3ec [be509f2 design rows, eaee3ec feat]; docs ab564c1); make check 902 passed, dbml-check empty, migration-cycle 12 passed, integration 448 passed; review pending
O-1: workspace /Users/andrey.epifancev/projects/awp-ca/CLAUDE.md (не git) ссылается на v5.15 — править при закрытии среза (T1 concern 2)
Task 1: minor T1.1 (btrim vs strip в backfill NAME, косметика; SQL предписан брифом): deferred; T1.2 (factories.py не тронут, **overrides покрывает): rejected — фабрика уже принимает поле
Task 1: complete (commits 52ae1b9..eaee3ec, docs ab564c1, review clean)
Task 2: dispatched (sonnet, BASE eaee3ec)
Task 2: implemented (commit d3c37db; make check 907 passed, integration 466 passed); concerns T2.c1-c5 noted (c5 -> Task 6); review pending
Task 2: minor T2.1 (слабый smoke-тест test_repository_returns_selectable_models): deferred; T2.2 (ручное создание версии вместо make_version): deferred; T2.3 (`* 1`, async _osv_rows): deferred; T2.4 (тавтологичный assert в test_factories): deferred; T2.5 (make_confirmed_osv: ключи account_code/row_number в rows → TypeError): deferred; T2.6 (load_osv_rows: DEPARTMENT/OTHER/CURRENCY/QUANTITY не покрыты тестом): deferred; T2.7 (OsvFilters несёт limit/offset, план T10 — отдельный OsvPageParams → свести в T10): carried to Task 10; T2.8 (Literal ChecklistCellStatus/CounterpartyType в schemas vs доменные enum T8/T9 — не дублировать): carried to Task 8/9
Task 2: complete (commits eaee3ec..d3c37db, review clean)
Task 3: implemented (commit 9411d9d; make check 916 passed, integration 486 passed); review pending
- Ruling: R-8 (design #17 — внести в design.md, pending) коды 404 `CONSOLIDATION_NOT_FOUND`/`VERSION_NOT_FOUND` реализуются как сообщение при коде `NOT_FOUND` (конвенция Срезов 1–2c: `NotFoundError` с фиксированным кодом, как `COMPANY_GROUP_NOT_FOUND`) — единообразие с принятыми срезами; спека говорит «код» — если владелец хочет отдельные коды, меняется конструктор ошибки
- Ruling: R-9 (design #18 — pending) решения T3: `in_work_only` убирает период без версий; невидимая/несуществующая ГК в списке → 404; невидимая/неизвестная ГСК → 404 `RELATED_GROUP_NOT_FOUND`, видимая ГСК чужой ГК или `kind=gsz` → 422 `RELATED_GROUP_MISMATCH` — естественное продолжение policy 404 — цена: расхождение с ожиданием фронта по пустому списку
Task 3: minor T3.1 (tests/factories.py FINISHED_STATUSES дублирует domain/policy.py): carried to Task 4 (policy.py); T3.2 (константа CONSOLIDATION_NOT_FOUND не используется): carried to Task 4; T3.3 (решения T3 не в design.md): fixed → pending design rows R-8/R-9 (внести в Task 5); T3.4 (_card raises VERSION_NOT_FOUND при нарушении целостности): rejected — недостижимо по FK
Task 3: complete (commits d3c37db..9411d9d, review clean)
Task 4: implemented (commit b2fa590; make check 934 passed, integration 529 passed; minors T3.1/T3.2 closed in-commit); review pending. Pending design rows (T4 decisions a–f, see task-4-report.md)
Task 4 review: I-1/T4.1 (дубль _lock_out) — Important, fix round 1; T4.2 (общий пролог версии → хелпер): carried to Task 5; T4.3 (осиротевший комментарий factories), T4.4 (слабый assert ip/ua), T4.5 (docstring Review Focus 4 тест не проверяет FOR UPDATE): fix round 1
Task 4: fix round 1/5 dispatched (resumed implementer); design rows #17–#24 вносит implementer в этом же коммите
Task 4: fix round 1/5 (5 addressed, 0 open — T4.1,T4.3,T4.4,T4.5 + design rows #17–#24; commits b2fa590..e317fe5); out-of-scope: строка #24 частично дублирует #3 (косметика, deferred)
Task 4: complete (commits 9411d9d..e317fe5, review clean). design.md заполнен до #24; дальше исполнители добавляют свои решения со следующими номерами (#25+).
Task 5: implemented (commit 90b7bd5; integration 551 passed); design rows #25–#29; review pending
Task 5: minor T5.1 (rowcount type: ignore дублирует _rowcount из file_status.py): carried to Task 6 (функция переписывается); T5.2 (N DELETE в цикле по удалённым ЮЛ): deferred; T5.3 (неиспользуемая переменная other_entities в тесте): deferred; ⚠ spec-сценарий «ячейка CONFIRMED + consolidation_document после PUT members» и гонка members ∥ confirm — закрываются в Task 6/8 (carry to Task 6: интеграционный тест подхвата через PUT …/members)
Task 5: complete (commits e317fe5..90b7bd5, review clean)
Task 6: implemented (commit 36bc966; make check 944 passed, integration 571 passed; mutants confirmed); design rows #30–#36; review pending (focus: autoreplace ∥ PUT members deadlock test missing)
Task 6 review: T6.0 Important (нет двухсессионного теста автозамена ∥ подхват) — fix round 1; T6.1 (late import transition, design #36): deferred (follow-up: вынести transition/OBJECT_TYPE/release_active_lock в листовой модуль); T6.2 (слабые тесты latest/tie-break), T6.3 (no-op assert статуса), T6.4 (дубль lookup группы), T6.5 (формулировка design #30): fix round 1
Task 6: fix round 1/5 dispatched (resumed implementer ad2f118342bb9af15, opus)
Task 6: fix round 1/5 (5 addressed, 0 open — T6.0,T6.2,T6.3,T6.4,T6.5; commits 36bc966..0e1fbd8); T6.1 deferred. O-2: R-3 дедлок через API недостижим (подхват привязывает только уже подтверждённые); оставлено как страховка (design #32)
Task 6: complete (commits 90b7bd5..0e1fbd8, review clean); полный `pytest -m "integration and not e2e"` после раунда 1 — контроллер гоняет (/tmp/slice4-t6-full.log)
Task 6 post-fix full regression (controller run): 574 passed (integration and not e2e, 213s). Task 7 dispatched (sonnet, BASE 0e1fbd8)
Task 7: implemented (commit 1908288; make check 944 passed; full integration 595 passed); design rows #37–#42; review pending
Task 7 review: T7.0 Important (тест хука не защищает надмножество R-2) — fix round 1; T7.1 (сбой перехода после загрузки → 500 и откат файлов) — Ruling R-10; T7.2 (второе соединение, пул), T7.5 (окно: ЮЛ убрали из периметра) — fix round 1 (формулировка design #39); T7.3 (порядок проверок лога), T7.4 (дубль TransactionFactory) — fix round 1; N+1 session.get(Consolidation) в хуке: deferred
- Ruling: R-10 (design #43) сбой перехода DOCUMENTS_PROCESSING после загрузки перехватывается и логируется, файлы остаются принятыми (201) — приём файлов важнее побочного статуса — версия может остаться INITIATED
Task 7: fix round 1/5 dispatched (resumed implementer a62187d68a65a967c)
Task 7: fix round 1/5 (6 addressed, 0 open — T7.0,T7.1/R-10,T7.2,T7.3,T7.4,T7.5; commits 1908288..4cc08cc); N+1 session.get(Consolidation) deferred
Task 7: complete (commits 0e1fbd8..4cc08cc, review clean)
Post-fix full regression Task 7 (controller): 597 passed. Task 8 dispatched (sonnet, BASE 4cc08cc)
Task 8: implemented (commit f46cf2f; make check 971, full integration 603); design rows #44–#49; review pending
Task 8: minor T8.1 (замена _osv_coverage на select_sources — не drop-in: Gap не несёт present document_ids, и missing_quarters=() при нулевом покрытии (design #46); нужен адаптер): carried to Task 9; T8.2 (нет теста, что fits-фильтр в сервисе работает — неподходящий квартал/не-член → MISSING): deferred (final review: triage); T8.3 (нет API-теста ERROR/58.03): deferred; T8.4 (CellStatus(best.status.value) по строке → явная таблица): carried to Task 9 (checklist.py правится); T8.5 (_QUARTER_ORDER в трёх местах → один QUARTER_ORDER в consolidation_periods): carried to Task 9; T8.6 (document_ids без кандидатов, design #47): rejected — в спеке «привязанные документы»
Task 8: complete (commits 4cc08cc..f46cf2f, review clean)
Task 9: implemented (commit d5a317a; make check 1030 unit; integration 603); design rows #50–#56; review pending (opus)
Task 9: minor T9.a (account_sort_key: isdigit()+int() падает на "6²"/"①" → 500 свода на сырых account_code): carried to Task 10 (однострочник isdecimal + тест); T9.b (design #50: квартальный Q1 при версии Q1 склеивается — дубли суммируются, None→0, is_cumulative=False, тогда как нарастающий Q1 отдаётся как есть; спека: «равнозначен нарастающему»): deferred — на подтверждение владельцу (в "Rulings I made"); T9.c (design #54: при нарушении инварианта R-1 select_sources возвращает все документы, суммы квартальных тихо удваиваются; страховка warnings/лог): deferred; T9.d (мелкие пробелы тестов: расхождение id между дублями в последнем квартале, резолвер вернул id вне справочника, stored id при match_kind=None): deferred; T9.e (src/__init__ импортирует app → import src.domain тянет приложение — особенность шаблона): rejected, не вина задачи
Task 9: complete (commits f46cf2f..d5a317a, review clean)
Task 10: implemented (commit a2c2135; make check 1040; full integration 614; perf 5400 rows 0.27s/8 SELECTs); design rows #57–#61; review pending
Task 10: minor T10.1 (IN(ids) vs asyncpg 32767 params; = ANY(:ids), выбирать лишь нужные колонки; design #60): deferred; T10.2 (потолок perf-теста 5 с при цели < 2 с — комментарий; debug print noqa T201): deferred; T10.3 (нет проверки `opening_debit is None` у нарастающей строки на уровне API; слабый тест account_sort_key): deferred; T10.4 (quantize округляет молча при >2 знаках — невозможно для numeric(18,2)): rejected
Task 10: complete (commits d5a317a..a2c2135, review clean)
Task 11: implemented (commit e246531; make check 1043; full integration 640); design rows #62–#70; review pending (opus)
- Ruling: R-11 (design #62/#63, принято исполнителем T11) «строка версии» в ручном сопоставлении/автоприменении сужена до строк свода (group_type=COUNTERPARTY, data_type BU/SUM) привязанных ОСВ-документов; NU/ACCOUNT → 404 ENTRY_NOT_FOUND и в автоприменение не входят — §3 п. 2 и R-3 Среза 2c (NU не участвует) — цена: NU-строки того же контрагента остаются без counterparty_id; буквальное чтение спеки шире. На подтверждение владельцу.
Task 11: minor T11.1 → R-11; T11.2 (row:null в MapCounterpartyOut — отразить в docs/api-contract-slice-4.md, выровнять формулировку #62): carried to Task 14; T11.3 (aliases.remove без блокировки → фантомное событие при гонке с move_alias; класс гонки существовал): deferred (follow-up); T11.4 (тест «без регистра» не фиксирует регистр): deferred; T11.5 (нет тестов: несколько владельцев алиаса при move_alias/ALIAS_TAKEN; алиас и у target, и у другого): deferred; T11.6 (дубль имени индекса ALIAS_UNIQUE_INDEX и логики AliasService.add/remove): deferred; T11.7 (ответ строится полным _build под блокировками): deferred; T11.8 (README каталог ошибок): carried to Task 14; T11.9 (новый алиас не снимает неоднозначность резолва на лету с тёзкой по имени — вопрос ТЗ DOC-221278): carried to Task 14 (07-open-questions)
Task 11: complete (commits a2c2135..e246531, review clean)
Task 12: implemented (commit 8e82c2c; make check 1045; full integration 661); design rows #71–#76; review pending
Task 12 review: T12.1 Important (квадратичное время экспорта: sheet.max_row в цикле) — fix round 1; T12.2 Important (формульная инъекция из внешних строк + 500 на управляющих символах) — Ruling R-12; T12.3 (порог точности #73: ≈10^13, не 10^15), T12.4 (мёртвый параметр entity_names) — Ruling R-13, T12.5 (нет теста None-сумм), T12.6 (model_dump(mode="json") в #76): fix round 1; M-4 (run_in_threadpool): fix round 1
- Ruling: R-12 (design #77) текстовые ячейки экспорта: вырезать ILLEGAL_CHARACTERS и принудительно data_type "s" (никаких формул из внешних данных) — файлы внешние, аналитики открывают в Excel — текст с ведущим '=' не вычисляется (желаемо)
- Ruling: R-13 (design #78) параметр entity_names убран из build_workbook (в плане предписан, не используется; сигнатура плана исправлена) — YAGNI — если нужен словарь имён, вернуть
Task 12: fix round 1/5 dispatched (resumed implementer aa5d6fd0aa1c6a31f)
Task 12: fix round 1/5 (7 addressed, 0 open — T12.1–T12.6, M-4; commits 8e82c2c..5b3b101); T12 export timing 6000 rows: 8.44 s → 0.46 s
Task 12: complete (commits e246531..5b3b101, review clean); последний полный прогон integration: 661 passed (до fix round), после — 33 passed в целевых
Task 13: implemented (commit af91643, тест под xfail(strict=True) — найден реальный дефект Срезов 4): golden ОСВ Q1 имеют is_cumulative=TRUE (Q2/Q3 — false); `fits` привязывает нарастающий только за квартал версии, `select_sources` не считает нарастающий Q1 квартальным Q1 → для версии Q3 привязано 17/22, РБ 60/62 и Счастливочная 60/62/76 уходят в gaps [Q1], rows_total=0. Временный патч (task-13-experiment-fix.diff) делает сценарий зелёным (1385 строк свода, 1381 unmapped; период сверен независимо по 5 контрагентам РБ).
- Ruling: R-14 (design #81) «Q1-эквивалентность»: документ за Q1 (period_quarter=Q1) эквивалентен и нарастающему, и квартальному — нарастающий за Q1 покрывает Q1 в версиях Q2+ как квартальный Q1 (парсер помечает ОСВ за Q1 is_cumulative=true, т.к. период с начала года = Q1); `fits`: документ Q1 подходит любой версии года; `select_sources`: документ Q1 любого флага — квартальный источник Q1 (для версии Q1 поведение прежнее: как есть/склейка по флагу); ключ R-1 для Q1 не различает флаги (позднейший confirmed_at побеждает) — иначе свод на реальных данных пуст — цена: если владелец считает Q1-нарастающий иным — но по определению периода это одно и то же
- Ruling: R-15 (design #80, принято исполнителем T13) сценарий «1 + propagated» уточнён: propagated считает записи osv_entry, свод склеивает кварталы → unmapped_total падает на число СКЛЕЕННЫХ строк, не на 1+propagated; тест сверяет обе величины отдельно; формулировку spec уточнить при архивации (человеку)
Task 13: STATUS открыт — fix round (дефект R-14) через резюмирование исполнителя Task 9
Task 13 fix: commit d3c4964 (R-14 реализован: effective_cumulative; e2e тест зелёный: 22 документа привязано, rows_total 1385, unmapped 1381; full integration 668); concern → Ruling R-14a (design #81 дополняется): Q1-эквивалентность только для ОСВ (ББ/ОФР за Q1 не подходят версиям Q2–Q4) — ББ/ОФР Q1 = баланс на конец Q1 — цена: если и ББ/ОФР должны подхватываться, снять условие; fix R-14a dispatched
Task 13 review (range 5b3b101..0bfe9ee, opus): Approved, Critical/Important нет; minors T13.M1 (spec THEN «1+propagated» ложен на реальных данных), M2 (формулировка #80), M3 (зафиксировать точные значения 1385/1381/4/17 и набор gaps), M4 (кандидаты не вызываются — docstring), M5 (MANUAL/alias_created/EXTERNAL до), M6 (ссылка на #79 вместо #80), M7 (порядок assert NEEDS_CHECK), M8 (docstring effective_cumulative: R-14a живёт в вызывающем), M10 (текст отметок плана): fix round 1 (resumed implementer a9283a17cfe22228e); M9 (дубль цикла map в test_mapping_golden — оправдан замером): rejected
Task 13: R-14a commit 0bfe9ee; полный integration 670 passed (после R-14a); fix round 1/5 dispatched (doc/test-only)
Task 13 fix round 1: commit a5cde2a (M1–M3, M5–M8, M10 сделаны; M4 урезан: recommended=true на реальных данных не получается — в файлах РБ имя «Юг-Финанс / ПЕКАРНЯ СЧАСТЛИВОЧНАЯ», рекомендация = точное совпадение нормализованного имени/алиаса; тест проверяет наличие кандидата с типом GSK; сценарий «Рекомендуемая Счастливочная» покрыт на синтетике)
O-3: на реальных фикстурах `recommended` для «Юг-Финанс / ПЕКАРНЯ СЧАСТЛИВОЧНАЯ» не срабатывает (точное совпадение normalize_alias) — вопрос к продукту/ТЗ (нечёткая рекомендация), в Task 14 → 07-open-questions
O-4: match_kind склеенной строки берётся из последнего квартала (D-3, #8): сопоставление записи раннего квартала даст PROPAGATED в ответе, хотя в БД одна MANUAL; фронту слать последнюю entry_id строки или осознанно показывать способ строки — в Task 14 → docs/api-contract-slice-4.md
Task 13: fix round 1/5 (all addressed; commit a5cde2a); full chain af91643,d3c4964,0bfe9ee,a5cde2a
Task 13: complete (commits 5b3b101..a5cde2a, review clean); последний полный integration (после R-14a): 670 passed
Task 14: implemented (arm-core 2c6908a, docs 9246a61); check-full EXIT 0 (unit 1106, integration 670, fixtures 15 passed, e2e 1 skipped, docker OK, web OK); design #82–#85; OQ-124…129; review pending
Task 14 review: Approved (Critical/Important нет); minors T14.1 (OQ-121/03-architecture: замер 0,27 с относится к синтетическому тесту, не к фикстурам РБ), T14.2 (счёт design в 09-delivery-plan), T14.5 (пробелы в api-contract: OsvSummaryOut, условия PROPAGATED, 422 takeover_reason >1000), T14.6 (формулировки, design #85) — fix round 1 (resumed a35a235509fda036c); T14.3 (неточность отчёта: fixtures 14 passed + 1 skipped) — исправляется в отчёте; T14.4 (имя модели в трейлере): rejected
Task 14: fix round 1/5 dispatched
Task 14: fix round 1/5 (all addressed; arm-core 98922cc, docs 308b493); T14.3 inline: отчёт содержит поправку в конце — cosmetic
Task 14: complete (arm-core 2c6908a..98922cc, docs 9246a61..308b493, review clean). check-full: EXIT 0 (log check-full.log)
## Final whole-branch review (opus, 714832e..98922cc): Ready to merge — With fixes (Critical нет)
- I-1: правило «кандидат подходит версии (fits)» в чек-листе не защищено тестом (мутант без fits в consolidations.py:206 выжил на 127 тестах) = T8.2 → fix wave (один исполнитель)
- M-2: текст спеки расходится с кодом (коды 404 CONSOLIDATION_NOT_FOUND/VERSION_NOT_FOUND/ENTRY_NOT_FOUND/LEGAL_ENTITY_NOT_FOUND = код NOT_FOUND с текстом; «тройка» в documents-api «Подхват» (R-1); «перечитывает под FOR UPDATE» (R-3: NO KEY UPDATE)) → fix wave (только текст спеки)
- M-3: переход после загрузки коммитится раньше транзакции запроса (версия может остаться DOCUMENTS_PROCESSING без файлов при сбое коммита) → fix wave: дописать риск в design #39/#43; код deferred
- M-4: нет теста advisory-блокировки алиаса (design #66; aliases.add ∥ сопоставление) → deferred (follow-up)
- M-5: неверный docstring repositories/consolidations.py:131-132 («ЮЛ без ГК не может быть членом»; после withdraw/move может — такое ЮЛ блокирует PUT members с ENTITY_NOT_IN_GROUP) → fix wave: docstring; OQ — deferred
- M-6: нет верхних границ входов (MembersIn.legal_entity_ids, StatusIn.comment, OsvFilters.q → 500 на огромном списке; lock_visible_version берёт FOR UPDATE до проверки видимости) → deferred
- M-7: общие строки osv_entry меняются и для не-«в работе» версий (VERIFIED/маршрут) → deferred до Среза 7 (OQ-123)
- M-8: Makefile DBML ?= data-model-v5.16.dbml: в основном checkout make dbml-check упадёт, пока не смержен docs PR #20 → порядок мержа: docs PR раньше/вместе — в описание PR (человеку)
- Ledger triage финального ревью: T1.1,T2.1–T2.6,#24 дубль,T5.2,T5.3,T6.1,T7 N+1,T8.3,T9.b,T9.c,T9.d,T10.1–T10.3,T11.3–T11.7 — may stay deferred; T8.2 — must-fix (= I-1); O-1 — при закрытии среза
Final fix wave (one dispatch, resumed a9d16bc67ead11c8b): commit 42c2531 — I-1 fixed (7 тестов чек-листа; мутант без fits роняет 3), M-2 fixed (спеки: 404=NOT_FOUND+сообщение, ключ привязки R-1/R-14, блокировки R-3), M-3 fixed (design #39/#43 — риск; код deferred), M-5 fixed (docstring); scoped re-review: all addressed, no new breakage. Out-of-scope observations re-review: formulation «VERSION_NOT_FOUND» в спеках = имя константы, а не текст сообщения (косметика, deferred); docstring lock_versions_for_entity слегка преувеличивает про PUT members (deferred); нет разделителя в design #39/#43 перед добавленным предложением (deferred)
Final review: CLEAN (после fix wave)
## Итоговые прогоны
- Финальный (после fix wave 42c2531): make check — 1106 passed, 678 deselected; pytest -m "integration and not e2e" — 677 passed, 1107 deselected (4:20).
- make check-full на 98922cc: EXIT 0 — unit 1106 passed; integration 670 passed; fixtures 14 passed + 1 skipped (e2e без профиля services); e2e 1 skipped; dbml-check v5.16 OK; check-infra OK; docker build OK; web-check OK.
## Приложение: pre-flight рулинги (preflight-rulings.md)
# Pre-flight рулинги контроллера (Срез 4) — обязательны для задач, на которые указывает диспатч
Спека главнее плана; здесь — расхождения и дыры, найденные до старта. Для каждого рулинга в `openspec/changes/slice-4-consolidation/design.md`
(раздел «Decisions made without discussion») добавляется строка с номером из скобок (их добавляет исполнитель Task 1 одним коммитом).
## R-1 (design #11) — ключ «тройки» при привязке включает признак нарастающего
Спека (documents-api «Подтверждение», «Подхват») заменяет привязанный документ той же тройки `(document_type, osv_account_code, period_quarter)`, а сценарий
consolidated-osv «Нарастающий рядом с квартальными» требует, чтобы квартальный Q3 и нарастающий Q3 одной пары (ЮЛ, счёт) были привязаны одновременно. Тройка без
признака уничтожила бы второй. РЕШЕНИЕ: ключ замены/выбора «самого позднего `confirmed_at`» = `(document_type, osv_account_code, period_quarter, cumulative)`,
где `cumulative = (is_cumulative is not False)` (NULL → нарастающий, как D-5/решение №10). Цена ошибки: если владелец хотел «один документ на квартал» — нарастающий
и квартальный вытесняли бы друг друга (меняется только ключ в `bind_confirmed`/`on_document_confirmed`).
## R-2 (design #12) — гонки потерянной привязки: блокировка надмножества версий
`lock_versions_for_entity(entity_id, statuses)` блокирует `FOR UPDATE ORDER BY id` НАДМНОЖЕСТВО: версии «в работе», где ЮЛ — член ИЛИ
`consolidation.company_group_id = legal_entity.company_group_id` ЮЛ; затем вторым запросом (новый снимок READ COMMITTED) возвращает только версии, где ЮЛ — член.
Иначе confirm ∥ `PUT …/members` теряет привязку (confirm не видит незакоммиченного нового члена и не ждёт версию), а автозамена ∥ подхват оставляет привязку заменённого
документа. Те же правила: `on_document_confirmed`, `on_documents_superseded` (по ЮЛ документов), `on_documents_mapped` (Task 7). Гонка «создание версии ∥ подтверждение»:
новая версия невидима до commit — Task 6 закрывает её (предлагаемый способ: `create` берёт `pg_advisory_xact_lock` по ГК, `lock_versions_for_entity` берёт тот же lock
первым; порядок «файл → документ → advisory ГК → версии»; `PUT …/members` advisory не берёт). Любой другой способ допустим, если есть тест на двух сессиях.
Цена ошибки: тихо потерянная привязка документа (редко, чинится повторным добавлением ЮЛ в периметр).
## R-3 (design #13) — `FOR NO KEY UPDATE` на документах/файлах в confirm и автозамене
Вставка `consolidation_document` (FK на `document`) берёт на строку документа `FOR KEY SHARE`, а `lock_document`/`lock_file_documents` — `FOR UPDATE`: подтверждение
(держит документ, ждёт версию) ∥ правка периметра/подхват (держит версию, вставляет привязку на тот же документ) взаимоблокируются. РЕШЕНИЕ: `lock_document`,
`lock_file_documents` (и `lock_file`, если он участвует в тех же циклах) переводятся на `with_for_update(key_share=True)` (FOR NO KEY UPDATE; блокирует других писателей так же,
не конфликтует с KEY SHARE). Тест — на двух сессиях, таймаут 5 с. Цена ошибки: нулевая для остальных путей (слабее по FK-ссылкам, строже по обновлениям не меняется).
## R-4 (design #14) — `Resolution` живёт в домене
Task 1 по плану кладёт `Resolution` в `repositories/counterparties.py`, а Task 9 (`domain/consolidated_osv.py`, mypy strict, без ORM/репозиториев) нуждается в нём в `Resolver`.
Домен не импортирует репозитории. РЕШЕНИЕ: `Resolution` (frozen dataclass `entity_id`, `kind`) определяется в `src/domain/osv_mapping.py`; `repositories/counterparties.py`
импортирует и реэкспортирует. Цена: нулевая (перенос определения).
## R-5 (design #15) — разделение `EntityFilesState` и документов-кандидатов между Task 2 и Task 8
План описывает `entity_files_state` одновременно в Task 2 (создаёт) и Task 8 (модифицирует), а `domain/checklist.build` получает и `files_state`, и `unbound`. РЕШЕНИЕ:
Task 2 создаёт `src/domain/checklist.py` с одним frozen dataclass `EntityFilesState(has_in_progress: bool, last_file_status: FileStatus | None)` и
`repositories.entity_files_state(session, entity_ids) -> dict[uuid.UUID, EntityFilesState]` (с тестом); неподтверждённые подходящие документы отдельной функцией
`unconfirmed_documents(session, entity_ids, year) -> list[Document]` (RECOGNIZED/NEEDS_CHECK, файлы не SUPERSEDED, ОСВ/ББ/ОФР) — её добавляет Task 8 вместе с `rules_for`;
Task 8 дополняет `checklist.py` функцией `build` и остальными типами. Цена: нулевая.
## R-6 — шум плана
Task 12: `api/requirements-dev.txt` не трогать. Прочие заглушки Task 3/5 (`bind_confirmed`, `checklist=[]`, прямое удаление привязок) заменяются в Task 6/8, как в плане.