arm-ka-docs/context/09-delivery-plan.md
Andrey Epifancev 50e588b6bd contracts: subject'ы NATS как в старом АРМ — files.*_uploaded, files.tabular_parsed (#74)
Решение владельца 08.10.2026: запросы — files.tabular_uploaded / files.img_uploaded
(было recognition.{tabular|image}.request), ack и результаты обоих сервисов —
files.tabular_parsed (было recognition.result); files.img_parsed не используется,
старый АРМ гасится. Тела сообщений, Nats-Msg-Id, queue group'ы и schema_version
не меняются.

Change nats-files-subjects: дельта recognition-contract (3 требования), Purpose
спеки; описания subject'ов в contracts/*.schema.json; 03-architecture (схема,
таблица потоков, NATS платформы), 09-delivery-plan, dev/README, dev/qa-guide,
repo-templates. DBML v5.17 не поднимается (R-3, deferred).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
2026-10-08 23:34:01 +04:00

300 lines
37 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.

# 09. План работ вертикальными срезами
Принцип: каждый срез — рабочий end-to-end кусок (миграции + API + минимальный UI + тесты на реальных фикстурах),
который можно показать аналитику. Порядок выбран так, чтобы самое рискованное (парсер ОСВ, матчинг ВГО) проверялось
на реальных данных как можно раньше, а внешние зависимости (ИС Сканирование, РАИСА, NATS платформы) не блокировали.
Каждый срез = OpenSpec change **в каждом затронутом репозитории** (`openspec/changes/<slug>/` с `proposal.md`, `specs/`,
`tasks.md`), сквозная часть — change в `docs/`; задачи — issues в Gitea; PR — по одному на репозиторий. Jira-эпики
пользователя маппятся на срезы (пользователь ведёт соответствие сам).
## Срез 0 — Скелет (2–3 дня, три репозитория + docs) — **завершён 04.10.2026**
- Три репозитория из нулевого коммита шаблона команды; соглашения шаблона — в `03-architecture.md` и в `CLAUDE.md`
каждого репозитория. **Первая задача агента — прочитать шаблон и дополнить документы.** (Сделано: раздел «Соглашения
шаблона команды» в `03-architecture.md`; в `tabular-parser`/`scan-gateway` фронт и БД вырезаются задачами среза.)
- OpenSpec change'и среза: `docs/openspec/changes/slice-0-skeleton` (сквозной: контракт, compose, проверки качества, документы) и
по одному `openspec/changes/slice-0-skeleton` в `arm-core`, `tabular-parser`, `scan-gateway`; issue #1 в каждом репозитории.
- `docker-compose` workspace-уровня (`docs/dev/docker-compose.yml`): PostgreSQL 16, NATS 2.10 **без JetStream** (как на
платформе; ack и повторная публикация — на уровне приложения), MinIO (S3, один бакет `arm-ka`), эмулятор ИС
Сканирование (часть `scan-gateway`, отдаёт `fixtures/ocr/aep_N.json`), три сервиса (профиль `services`, Python 3.11).
`Makefile` с общими целями `up/test/test-integration/lint/check/check-full` (+ `migrate/seed` в ядре) в каждом репо.
Порты, запуск профиля `services` и ручная сквозная проверка обмена `recognition_job` через NATS (сценарии `tabular` →
`NOT_IMPLEMENTED`, `image` → эмулятор ИС → `INVALID_DOCUMENT`, потеря ack → `dlq`) — `docs/dev/README.md`,
разделы «Профиль `services`» и «Проверка окружения».
- Контракт v1.0 — три схемы в `docs/contracts/`: `recognition-request`, `recognition-result-message` (конверты NATS) и
`recognition-result` (файл результата) + `examples/`. Общий пакет `arm-ka-contracts` **не публикуется**: каждый сервис
хранит копию схем в `api/contracts/` и pydantic-модели в `src/contract/`, идентичность с `docs/contracts` проверяется в `make check`
(решение D-1 change `slice-0-skeleton` в docs).
- Alembic: миграция 0001 — **все** таблицы и enum'ы DBML v5.13 (черновик из ORM, сверка с DBML скриптом
`api/scripts/check_dbml_vs_orm.py` — расхождение = ошибка CI).
- Seed (`arm-core/api/seed/`, идемпотентно): `regional_office` (ГО; примеры РФ — только `--dev`), `role` (7),
`permission` (16 осмысленных сочетаний объект × действие по §10), `role_permission` по §10, `chart_of_account`
(План счетов 94н + типовые субсчета 1С, OQ-47), `elimination_rule` v1 из §4 (только ББ), `required_document_rule`
для BASIC/EXTENDED (OQ-46).
- Auth: валидация JWT + dev-эмитент; `app_user` seed (по одному на роль); зависимость `require(permission)`.
- `audit_event` helper; `s3_object` клиент; outbox-паблишер (ack `processing`, повторная публикация без ack, `dlq`) и
подписка `files.tabular_parsed` (дедуп по `result_msg_id`, пустой маппер документов).
- Проверки качества вместо CI (в Gitea CI нет, пайплайн платформы вне репозиториев): `make check` (ruff, mypy, pytest,
контракт) и `make check-full` (+ интеграционные тесты против compose, цикл миграций, `check_dbml_vs_orm`) — `docs/dev/checks.md`.
- DoD: `GET /api/v1/me` возвращает пользователя и его эффективные права; все миграции применяются и откатываются;
`make check-full` зелёный в трёх репозиториях.
## Срез 1 — Справочники (ГК / ЮЛ / ГСК / ГСЗ / филиалы) + пользователи и роли — **завершён 05.10.2026**
Объём — **вариант Б** по решению владельца 04.10.2026: без UI справочника, с минимальным экраном администратора; цель —
быстрее дойти до Среза 2 (`tabular-parser`). Изначальный состав среза (CRUD + импорт + админка + policy + фикстуры +
UI справочника по макету) разделён: всё, кроме UI справочника, сделано; UI вынесен в отдельный срез.
**Сделано (по факту):**
- **API справочников** (`arm-core`, `/api/v1/directories/*`): ГК (`code`, `crm_id` nullable, закрепление за РФ, архив,
история SCD4), ЮЛ/ИП (ИНН 10/12, алиасы, перевод в другую ГК с `effective_date`, закрытие/вывод из ГК, история),
ГСК/ГСЗ и состав (SCD2; ЮЛ — одна ГСК и одна ГСЗ одновременно), филиалы (чтение). Каждая мутация пишет `audit_event`.
- **Импорт из выгрузки ЕЦП CRM** (`POST /directories/import`, право `DIRECTORY_ADMIN`): правила — `05-domain-rules.md` §13,
`dry_run` по умолчанию, `on_existing ∈ {skip, update}`, отчёт с кодами, `audit_event(DIRECTORY_IMPORT)` и файл в S3;
полная выгрузка (8400 строк) обрабатывается за ~1 с.
- **Policy видимости** (§10.1): по `regional_office.is_head_office`, без проверки кода роли; ЗО не фильтрует (OQ-86);
объект вне видимости — 404.
- **Админ-API и минимальный экран `/admin`** («Пользователи и роли»): пользователи, роли, матрица прав
(`USER_CHANGE`, `USER_ROLE_CHANGE`, `ROLE_PERMISSION_CHANGE`), защита `LAST_ADMIN`; вход — dev-форма (SSO РАИСА — позже).
- **DBML v5.14** (`related_group_member.kind`, partial unique `(legal_entity_id, kind)`, `audit_action` +
`LEGAL_ENTITY_CHANGE`/`USER_CHANGE`) и **миграция `0002`** в `arm-core`; `entities-v5.14.md`.
- **Фикстура** `fixtures/directories/ecp-export-russkiy-biskvit.xlsx` (21 строка ГК «Русский Бисквит»; `06-test-data.md` §6.1).
- **Документы:** §13 и §10.1 в `05-domain-rules.md` (сверены с кодом, задача 3.1 change `slice-1-directories`), OQ-31/32
закрыты, OQ-86…OQ-97 открыты.
- **PR:** arm-core #6, docs #7, fixtures #3 (issue-ы: arm-core #5, docs #6, fixtures #2).
**Перенесено в будущий срез «UI справочника»** (номер не присвоен; планируется после Среза 2):
- Экраны справочника по макету: «ГК и организации» [50] (список ГК | карточка с вкладками ЮЛ / ГСК / ГСЗ / Архив),
создание ГК/ЮЛ/ГСК/ГСЗ [51], «ЮЛ вне ГК» [61], вкладка ГСК/ГСЗ [77], изменение ЮЛ [78]–[79], перевод ЮЛ [79-01],
вывод из состава ГК [80], модалка импорта [38]. API для них готов с Среза 1 (см. `10-user-journey.md`).
- Пока UI нет, справочники заполняются импортом ЕЦП и вызовами API (`/docs`).
## Срез 2 — Загрузка файлов + табличный парсер (ОСВ, ББ, ОФР из Excel) — самый рискованный, делать рано
Решение владельца 05.10.2026: срез делится на четыре подсреза со своими change'ами и PR; сначала **только парсер ОСВ**.
Порядок: 2a → 2b → 2c → 2d. Change'и закрытых срезов заархивированы: пути ниже вида `<repo>/openspec/changes/<id>` теперь
`<repo>/openspec/changes/archive/<дата>-<id>`. DoD среза в целом (без правил под конкретный файл, без координат в коде парсера) — в конце раздела.
### Срез 2a — Парсер ОСВ (`tabular-parser`) — закрыт 05.10.2026 (tabular-parser #6, архив #8; docs #10, архив #15; fixtures #5)
Change'и: `tabular-parser/openspec/changes/slice-2a-osv-parser` (issue arm-tabular-parser#5, PR #6), сквозной
`docs/openspec/changes/slice-2a-osv-parser` (arm-ka-docs#9), фикстуры arm-ka-fixtures#4. Копия схемы v1.1 в `arm-core` —
отдельный bounded-PR без логики после merge docs и tabular-parser.
**Сделано (по факту):**
- **Парсер ОСВ** `tabular-parser/api/src/parser/`: динамическое обнаружение структуры по содержимому (§1.0): листы ОСВ,
заголовок таблицы и колонки сумм по тексту (обязательны «обороты» и «сальдо на конец»), шапка и `group_schema`
(варианты 1 и 2), иерархия каскадом `outline → indent → account_pattern → sums` (+ `trivial` для пустой ОСВ),
классификация строк, `Decimal`, контрольные суммы §1.4, статусы и коды (`05-domain-rules.md` §1.6), лимиты
`PARSE_MAX_BYTES` (50 МиБ) и `PARSE_TIMEOUT_SECONDS` (120 с). Чтение XLSX/XLS/XLSB по сигнатуре.
- **Словари** `api/rules/{osv_headers,osv_groups,periods,data_types}.yaml` (данные, не код) с проверкой при старте и
тестом «каждая запись срабатывает хотя бы раз».
- **Контракт v1.1**: `rows[].extra` (нераспознанные колонки внутри таблицы); примеры `result-osv-minimal.json`,
`result-osv-needs-check.json`.
- **Результат на реальных данных:** все 66 файлов `fixtures/osv/**` — `done`, контрольные суммы сходятся, максимум 0,6 с
на файл; 16 синтетических файлов (`fixtures/osv/synthetic/`); 82 golden-снимка (`fixtures/golden/expected_osv_parse/`);
тест-линтер «нет координат в `src/parser/`»; CLI `python -m src.parser.cli <файл>`.
- **Нетривиальное решение (R-21):** сверка «сумма детей = родитель» по сальдо идёт по колонкам либо по нетто, если у
родителя сальдо свёрнуто (1С показывает счёт нетто, субсчета — развёрнуто); без него 32 из 66 файлов не сходились.
- **Допущения парсера, решаются в 2c:** `is_cumulative` только по тексту периода (OQ-02), `data_type = NU` как есть
(OQ-45), «Итого» выдаётся строкой `TOTAL` (OQ-01).
- **Документы:** `05-domain-rules.md` §1 приведён к факту (§1.6 — результат парсера), `06-test-data.md` §7 (синтетика,
golden, рукописный xlsb), OQ-98…OQ-105 открыты (финальное ревью добавило OQ-106…OQ-109), OQ-81 частично закрыт (в `tabular-parser` `ARM_KA_FIXTURES` поддержан; `scan-gateway` — follow-up).
- **Не сделано / риски:** xlsb проверен только рукописным синтетическим файлом (OQ-102); ИНН из текста строки не
извлекался (OQ-100; реализовано позже в tabular-parser PR #15); счета с буквой в первом сегменте не распознаются (OQ-103).
### Срез 2b — ББ/ОФР из Excel (`tabular-parser`) — закрыт 06.10.2026 (tabular-parser #16, docs #27, fixtures #10; follow-up arm-tabular-parser#20)
- `tabular-parser`: ББ/ОФР из Excel по §12 в тот же `statementDocument`; поиск листов с формами по содержимому
(в 2a такие листы пропускаются с предупреждением `skipped: statement (slice 2b)`); словари синонимов `rules/*.yaml`;
контрольные формулы §7 для форм.
- Тесты: property-тесты инвариантов на **всех** файлах `fixtures/statements-xlsx/**` (формулы §7) + синтетика (ББ+ОФР на
одном листе, csv-подобный лист); снимки результата — рядом с golden ОСВ.
- **Реализовано (ветка `feat/14-slice-2b-statement-parser`, tabular-parser PR #16; docs — PR #27, ветка
`feat/26-slice-2b-statement-parser`):** `BookParser` + конвейер форм `statements/`, словари `statement_*.yaml`, контракт
v1.2 (`DISCLOSURE`, `checks`, границы периода ОСВ; копии схемы в arm-core PR #27 и scan-gateway PR #5), `§12`/`§7` по
решениям владельца. Тесты: манифест `api/tests/statements_manifest.yaml` (18 реальных файлов), синтетика `st01`–`st07`,
golden `expected_statement_parse/` (18 + 7 снимков), интеграционный тест воркера на книге НПЗ.
- **Объём:** парсер + контракт v1.2; **маппинг форм в ядре (`statementDocument → document + financial_statement_*`) —
Срез 3**. По факту `done` только у форм Межениновской; остальные `needs_check` по данным файлов (рубли с копейками при
«тыс. руб.», страницы без шапки, шаблоны) — OQ-138, OQ-140.
- **Не сделано / риски:** единицы не нормализуются (OQ-138); форма без колонки кодов не разбирается (OQ-139); страницы
в разных файлах не склеиваются (OQ-140); строки без кода не хранятся (OQ-141); после контракта v1.2 `docs/contracts/**` заморожены (копии схемы в arm-core и scan-gateway должны совпадать с ними; в tabular-parser версия закреплена тестом `api/tests/unit/test_contract_v12.py`, побайтное сравнение копий — вручную через `diff` при раскатке); косметика T1.1/T2.x уходит в следующий MINOR.
### Срез 2c — Загрузка файлов и маппинг результата в ядре (`arm-core`) — закрыт 06.10.2026 (arm-core #15, архив #16; docs #17, архив #18)
Change'и: `arm-core/openspec/changes/slice-2c-osv-ingest` (issue arm-core#14, ветка `feat/14-slice-2c-osv-ingest`), сквозной
`docs/openspec/changes/slice-2c-osv-ingest` (ветка `feat/16-slice-2c-osv-ingest`, DBML v5.15).
**Сделано (по факту):**
- **Модель:** DBML v5.15 — `osv_data_type + NU` (налоговый учёт, OQ-45), `app_user.email` в нижнем регистре (закрывает OQ-59);
миграция `0003_dbml_v5_15`, цикл `upgrade → downgrade → upgrade` и пустой autogenerate.
- **Загрузка:** `POST /legal-entities/{id}/files` (xls/xlsx/xlsb/pdf/png/jpg, проверка сигнатуры, лимит 50 МиБ, дедуп по
`sha256` в пределах ЮЛ, частичный успех `accepted`/`rejected`), S3 `uploads/{file_id}/{job_id}/…`, `stored_file` +
`recognition_job` (outbox) + `FILE_UPLOAD`.
- **Статусы файла** ведёт код по текущему заданию (`UPLOADED → PROCESSING → PROCESSED | ERROR`, `SUPERSEDED`): ack, результат,
`error`, `timeout`, `dlq`; inbox результат не читает (OQ-67, OQ-69).
- **Цикл маппинга** (третий фоновый цикл рядом с публикатором и вахтёром): проверка ключа результата (OQ-70), чтение S3
вне транзакции, `document` на лист ОСВ, `osv_entry` без `TOTAL` с деревом, `account_root`/`account_id`, резолв
`counterparty_id` по ИНН/имени/алиасу, `counterparty_check`, статус `RECOGNIZED`/`NEEDS_CHECK`, `is_cumulative` по данным
Q1 (OQ-02), до 3 попыток; ОСВ_60 за 2024 (1034 строки) — 0,3 с.
- **Автозамена версий** (`SUPERSEDED`, `version_no`, `FILE_REPLACE`; заменяется и `CONFIRMED`), **API** файлов/документов/строк,
`POST /documents/{id}/confirm`, `DELETE /files/{id}` (запрет при `CONFIRMED` и при заменивших другие, `expires_at` у объектов S3).
- **Проверки на реальных данных:** golden — все 82 снимка парсера (85 документов, 13 510 строк, ≈ 4 с) разобраны, инварианты
спеки выполняются; сквозной прогон `make e2e` с реальным `tabular-parser` через compose (ОСВ_66 за 2024 → `PROCESSED`, 9
строк). Итог `make check-full` (контроллер, HEAD f9b8a21): EXIT 0 — unit 900, integration 437, e2e skipped (профиль
`services` не поднят), docker-build и web-check OK; отдельный прогон e2e с реальным `tabular-parser`: 1 passed (4,3 с);
`-m fixtures`: 13 passed + 1 skipped.
- **Решения без обсуждения** — `arm-core/openspec/changes/slice-2c-osv-ingest/design.md`, разделы «Decisions made without
discussion» (№1–52). Правила — `05-domain-rules.md` §1.3, §1.5, §3 п. 4, §9.2–9.5; архитектура — `03-architecture.md`
(«Цикл маппинга результата и порядок блокировок»); QA — `dev/qa-guide.md` §6.6.
- **Закрыты OQ:** 67, 69, 70, 59, 109. **Открыты новые:** OQ-110 (удаление файла после консолидаций), OQ-111 (ретеншн S3
при удалении), OQ-112 (остаточные объекты e2e), OQ-113 (полнота субсчетов seed), OQ-114 (`is_modified` в `EntryOut`),
OQ-115 (частичная замена файла), OQ-116 (нарастающая по Q1 → `NEEDS_CHECK`).
- **Остаётся (Срез 2d и далее):** UI «Первичные документы»; маппинг ББ/ОФР (`statementDocument`) — общий для Excel и OCR (Срез 3
его переиспользует); ручное сопоставление контрагента и пересчёт резолва при построении свода (Срезы 4–5); OQ-100 (ИНН из
текста имени контрагента) по-прежнему не решён — резолв идёт по ИНН колонки, имени и алиасу.
- Копия схемы контракта v1.1 в `arm-core` (`OsvRow.extra`) — если не сделана bounded-PR раньше.
### Срез 2d — UI «Первичные документы»
**Статус:** API готов (Срезы 2c, 3b, arm-core #42); UI — у отдельного исполнителя фронта по `dev/frontend-api-guide.md`.
- Панель «Первичные документы», окно загрузки, таблица ОСВ с деревом строк и подсветкой нераспознанных контрагентов.
**DoD среза 2 целиком:** все реальные файлы и вся синтетика проходят **без единого правила под конкретный файл или
координату**; в коде парсера нет литералов букв колонок/номеров строк/имён листов (проверяется тестом-линтером).
## Срез 3 — ББ/ОФР: scan-gateway, маппинг форм, логические проверки, подтверждение документа
Срез 3 делится на два подсреза с отдельными change'ами.
### Срез 3a — scan-gateway: горячие папки и нормализатор OCR (репозиторий scan-gateway)
**Статус: закрыт 07.10.2026** (scan-gateway #8, архив #10; docs #47; fixtures #13; follow-up — issue arm-scan-gateway#9). Change `slice-3a-ocr-normalizer` (arm-scan-gateway#6;
docs#46, fixtures#12), спецификация — `scan-gateway/openspec/changes/slice-3a-ocr-normalizer/` (design D-1…D-9, «Decisions made
without discussion» №1–№35). Реализация — по `05-domain-rules.md` §11 и §11.1.
- Горячие папки в раскладке FileWorker (OQ-137): `Input/<job_id>.<ext>`, `.part` в `Tmp` и запись без замены, `Output/<job_id>/*.json`
набором (по стабилизации), очистка — под флагом `SCAN_CLEANUP` (по умолчанию выключена, OQ-144); таймаут 5 мин.
- Эмулятор ИС Сканирование для dev: пара PDF + JSON по sha256 содержимого (`fixtures/ocr`), набор по умолчанию
`SCAN_EMULATOR_DEFAULT`; сырой ответ → S3 `raw/`.
- Нормализатор `OcrNormalizer`: перенос проверенного на стенде модуля `ofr_result_conversion` старого ядра (единственное
разрешённое исключение для `reference-old-arm`, решение владельца 06.10.2026) в `statementDocument` v1.2 — по документу на
таблицу; справочники `api/rules/` с редакциями форм `66n` / `fsbu4` (`fsbu4` заполняет владелец, OQ-150); `checks` не передаются.
- Эталоны нормализатора — `fixtures/ocr/expected_normalize/` (отдельный PR fixtures), `make golden-update`.
- Новые вопросы: OQ-144…OQ-152.
### Срез 3b — маппинг форм, проверки, подтверждение и API до сводной ОСВ (arm-core)
**Статус: закрыт 07.10.2026** (arm-core #39, архив #41; docs #49; follow-up arm-core#40); UI экрана «Проверка распознавания» в срез не входит. Объём по
решению владельца 07.10.2026: весь API пути пользователя до сводной ОСВ включительно. Issue arm-core#36, docs#48; спецификация —
`arm-core/openspec/changes/archive/2026-10-07-slice-3b-statements-api/` (design D-1…D-14, «Decisions made without discussion» №1–№22); документы
workspace — bounded-PR в docs (без сквозного change: DBML и контракт не менялись).
**Сделано (по факту):**
- Маппинг форм в ядре: `statementDocument` `BALANCE`/`INCOME` (Excel и OCR) → `document` + `financial_statement_meta/column/line/value`
в той же транзакции `map_file`; период по дате отчёта либо по самой поздней дате колонок; `DISCLOSURE`/`OTHER` — документ без
данных; файл только с формами — не ошибка (`05-domain-rules.md` §9.5 п. 2, `03-architecture.md`).
- Проверки §7 в ядре (`domain/statement_checks.py`, словарь `api/rules/statement_checks.yaml` — копия словаря tabular-parser),
межформенная проверка 1370/2400 — мягкая; акцепт жёстких нарушений с комментарием; статус документа формы (§9.3).
- API формы: `GET`/`PATCH /documents/{id}/statement`, `POST …/statement/checks/accept`; правка значений и ИНН/даты отчёта/единиц только
до подтверждения; подтверждение ББ/ОФР с условиями `PERIOD_REQUIRED` и `CHECKS_NOT_PASSED`; `RECOGNIZED_DATA_EDIT` с before/after.
- Мелкие дыры пути пользователя: `GET /files/{id}/content` (исходный файл потоком), комментарии к документу и `comments_count`,
`GET /users/brief`, `documents_count` у ЮЛ, CSV чек-листа (`…/checklist?format=csv`).
- Контракт API — `dev/api-contract-slice-3b.md`; QA — `dev/qa-guide.md` §6.9.
- **Не вошло:** UI; правка строк ОСВ, добавление/удаление строк формы и правка кода/названия строки; отмена подтверждения (исправление
— новым файлом); формулы §7 и справочники по редакциям (OQ-146); аудит скачивания (OQ-153).
- **Закрыты допущения:** OQ-23, 36, 77, 140, 145 («Срез 3b: допущение реализовано»). **Открыт новый:** OQ-153. Открытые вопросы 3b:
OQ-146 (словари по редакциям), OQ-149, OQ-152.
**Исходный план среза (для справки):**
- Маппинг: `financial_statement_meta/column/line/value` из `statementDocument` (общий для Excel и OCR); числа, скобки, confidence; ИНН/имя сверка.
- Логические проверки §7 (чистые функции `domain/checks.py`), акцепт КА, «Подтвердить данные» → `CONFIRMED`,
`RECOGNIZED_DATA_EDIT` с before/after.
- Чек-лист `required_document_rule × периметр − document CONFIRMED` (API + модалка + CSV).
- UI: экран «Проверка распознавания» (исходник слева — PDF.js, результат справа, блок «Логические ошибки»).
- Тесты: пары `aep_N.pdf/json`; машиночитаемые PDF АО РБ 2024 как dev-путь без OCR (опционально pdfplumber).
## Срез 4 — Консолидация: карточка, версии, периметр, периоды, сводная ОСВ
**Статус: закрыт 07.10.2026** (arm-core #19, архив #20; docs #20, архив #21; follow-up arm-core#21): бэкенд + API + экспорт xlsx; UI — отдельный исполнитель. Объём по решению
владельца 06.10.2026: бэкенд, API, экспорт xlsx. Issue arm-core#18, docs#19; спецификация —
`arm-core/openspec/changes/archive/2026-10-06-slice-4-consolidation/` (design D-1…D-16, «Decisions made without discussion» №1–№85),
документный change — `docs/openspec/changes/archive/2026-10-06-slice-4-consolidation/`.
**Сделано (по факту):**
- DBML v5.16: `osv_entry.counterparty_match_kind` + enum `counterparty_match_kind` (миграция `0004_dbml_v5_16`, backfill).
- Версии консолидации (создание, список по ГК, карточка), периметр (`PUT …/members`), блокировка «взять в работу»
(перехват с причиной, без истечения), статусы: автоматические по событиям документов (INITIATED → DOCUMENTS_PROCESSING →
DATA_CORRECTION) и ручные (`CANCELLED`, возврат в `INITIATED`); остальные — Срез 7.
- Явная привязка подтверждённых документов к версии (`consolidation_document`): подтверждение, подхват при создании версии и
добавлении ЮЛ, отвязка при автозамене файла и удалении ЮЛ; Q1-эквивалентность ОСВ (R-14, OQ-124).
- **Чек-лист** комплекта документов (перенесён из Среза 3: `required_document_rule × периметр`, статусы ячеек) — API; CSV не делали.
- Сводная ОСВ как чистая функция от версии: выбор источников и склейка кварталов (§2), пробелы, дорезолв контрагента, признак
`GSK/GK/EXTERNAL`, способ сопоставления; страница, сводка, фильтры.
- Кандидаты и ручное сопоставление контрагента с алиасом, переносом алиаса и автоприменением (`PROPAGATED`).
- Экспорт xlsx (лист «Сводная ОСВ» и лист «Пробелы»).
- Сквозной тест на фикстурах РБ: ОСВ 60/62 РБ и 60/62/76 Счастливочной за Q1–Q3 2024 → версия Q3 → свод 1385 строк, 22
документа привязано; пары с одним лишь Q3 (РБ: 01, 58.03, 66, 67, 76; Счастливочная: 01, 02) — в `gaps` с пропуском Q1–Q2;
одно ручное сопоставление «Пекарня Счастливочная ООО» → ГСК.
- Контракт API для фронта — `dev/api-contract-slice-4.md`; QA — `dev/qa-guide.md` §7.
**Закрыты OQ:** 03, 19, 40 (допущение реализовано), 110 (для Среза 4). **Открыты новые:** OQ-118…OQ-129 (запрет загрузки после
DATA_CORRECTION, истечение блокировки, `period_check`, порог кэша свода, `PROCESSING` в чек-листе, общие строки `osv_entry`
у версий, Q1-эквивалентность, склейка Q1 версии Q1, область ручного сопоставления, рекомендация кандидата, неоднозначность
дорезолва, формулировка сценария «1 + propagated»).
**Остаётся (UI и последующие срезы):** экраны `34`–`37`, `42`, `30`, `32-*`, `31-00`; CSV чек-листа (сделан в Срезе 3b);
«Добавить версию», `CONFIRM`, `EXTENDED`, гудвилл, маршрут согласования (Срезы 6–7); формы ББ/ОФР в чек-листе по
реальным документам — Срез 3b (реализован); шахматка и ВГО — Срез 5.
**Исходный план среза (для справки):** `consolidation` / `consolidation_version` / `consolidation_member` /
`consolidation_document`; приведение квартальных ОСВ (§2); сводная ОСВ (§3) как запрос; признак контрагента; ручное
сопоставление с алиасами и автоприменением; UI со списком, карточкой, составом, сводной ОСВ с фильтрами и экспортом;
тест: строки «Юг-Финанс / ПЕКАРНЯ СЧАСТЛИВОЧНАЯ» → ГСК после одного ручного сопоставления.
## Срез 5 — ВГО: автоматическое сопоставление, шахматка, корректировки
- `elimination_rule` как данные + версия в `consolidation_version`; матчинг §5 (чистые функции на pydantic-моделях);
шахматка §6; `correction` с дочерними строками, контроль 5 %, статусы DRAFT/APPLIED/REVOKED; `ADJUSTMENT`.
- UI: шахматка (ячейки с комментариями), таблица пар (авто/ручные/несопоставленные), таблица корректировок с
импортом/экспортом xlsx.
- Тест: пара 60.01 АО РБ ↔ 62.01 Счастливочная на 30.09.2024 = 106 311 116,46 (точный матч по правилу № 6); заём
42 млн 66.03 ↔ 58.03; сверка «в т. ч. взаимные обязательства» с `Консолидация_Группа РБ.xlsx` лист `01.10.2024` —
**только по точным парам**: эталон элиминирует полную сумму ВГО каждой стороны, а Срез 5 — по алгоритму ТЗ (`min` по
паре); расхождение ожидаемо и описывается в тесте (OQ-155, решение владельца 08.10.2026).
## Срез 6 — КФО: результат, гудвилл, пороги, сравнение с клиентом
- `kfo_result(_line, _line_company)`: сумма → авто-элиминация → корректировки → гудвилл → итог; единицы (OQ-26);
пороги §8; `goodwill`; `client_kfo_line` для `CONFIRM`.
- UI: КФО (колонки как в эталонном Excel), сводка расхождений, гудвилл, подтверждение КФО клиента, экспорт xlsx.
- Тест: КФО группы РБ на 01.10.2024 vs эталонный лист (допуск на ручные правки аналитика — зафиксировать расхождения
и показать аналитику).
## Срез 7 — Маршрут согласования, ЕХ, журнал, выгрузки
- Полная машина статусов §9.1 с `consolidation_status_history`, policy по ролям/филиалам; № и дата УОБ; новая версия.
- `VERIFIED` → `ex_snapshot/_member/_item` (payload по схеме `docs/contracts/ex-payload-v1.schema.json`); read-only.
- ЛК ЕХ: дерево ГК → ГСК → организации, просмотр payload, скачивание первички, выгрузка xlsx на лету.
- Журнал действий: экран с фильтрами по разделам/объектам/полям.
- Экспорт: настраиваемые колонки, `audit_event(EXPORT)`, S3 `exports/` 7 дней.
## Срез 8 — Нефункциональные: ретеншн, архивирование, производительность, безопасность
- Lifecycle S3 и удаление по ТЗ 8.4 (5 лет), архив в data lake (ссылка), объёмы `osv_entry` (индексы, partitioning по
году при необходимости), нагрузочный тест парсера (1000-строчные ОСВ × 20 компаний), OWASP-проверка, аудит доступа.
## Поперечные правила
- Любой срез, затрагивающий модель, начинается с diff к DBML (новая версия v5.14+ с записью в changelog) и миграции.
- Вычисляемые объекты — только `domain/` + `repositories/` запросы; никаких таблиц «для ускорения» без DECISION.
- Каждый алгоритм из `05-domain-rules.md` — отдельный модуль с docstring-ссылкой на § и тестами на фикстурах.
- Открытые вопросы — только через `07-open-questions.md` с `ASSUMPTION(OQ-nn)` в коде.