arm-core/docs/superpowers/plans/2026-10-06-slice-4-consolidation-ledger.md

175 lines
38 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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, как в плане.