arm-ka-docs/context/09-delivery-plan.md
Andrey Epifancev 45ee08c0f8 docs: §1.3/§1.5/§3/§9 по факту маппинга, архитектура (цикл маппинга), OQ, план срезов, qa-guide
05-domain-rules: §1.3, §1.5, §3 п. 4, §9.2–9.3, новый §9.5 (загрузка, маппинг, автозамена, подтверждение, удаление, аудит).
03-architecture: цикл маппинга, порядок блокировок, схема ключей S3. 07-open-questions: закрыты OQ-67/69/70/59/109,
новые OQ-110…116. 09-delivery-plan: Срез 2c по факту. dev/qa-guide: раздел 6.6 «Загрузка ОСВ и просмотр строк через API».
tasks.md: 2.1 выполнен.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
2026-10-06 00:11:52 +04:00

215 lines
25 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)
- Три репозитория из нулевого коммита шаблона команды; соглашения шаблона — в `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`) и
подписка `recognition.result` (дедуп по `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. DoD среза в целом (без правил под конкретный файл, без координат в коде парсера) — в конце раздела.
### Срез 2a — Парсер ОСВ (`tabular-parser`) — реализован, ожидает merge
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); счета с буквой в первом сегменте не распознаются (OQ-103).
### Срез 2b — ББ/ОФР из Excel (`tabular-parser`)
- `tabular-parser`: ББ/ОФР из Excel по §12 в тот же `statementDocument`; поиск листов с формами по содержимому
(в 2a такие листы пропускаются с предупреждением `skipped: statement (slice 2b)`); словари синонимов `rules/*.yaml`;
контрольные формулы §7 для форм.
- Тесты: property-тесты инвариантов на **всех** файлах `fixtures/statements-xlsx/**` (формулы §7) + синтетика (ББ+ОФР на
одном листе, csv-подобный лист); снимки результата — рядом с golden ОСВ.
### Срез 2c — Загрузка файлов и маппинг результата в ядре (`arm-core`) — реализован, ожидает ревью и merge
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`: 900 unit, 437 integration, e2e 1, `-m fixtures` 13 (+1 e2e skip без профиля).
- **Решения без обсуждения** — `arm-core/openspec/changes/slice-2c-osv-ingest/design.md`, разделы «Decisions made without
discussion» (№1–50). Правила — `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 «Первичные документы»
- Панель «Первичные документы», окно загрузки, таблица ОСВ с деревом строк и подсветкой нераспознанных контрагентов.
**DoD среза 2 целиком:** все реальные файлы и вся синтетика проходят **без единого правила под конкретный файл или
координату**; в коде парсера нет литералов букв колонок/номеров строк/имён листов (проверяется тестом-линтером).
## Срез 3 — ББ/ОФР: scan-gateway, маппинг форм, логические проверки, подтверждение документа
- `scan-gateway`: горячие папки (по FNR02) с заглушкой-эмулятором ИС Сканирование для dev (возвращает `aep_*.json`
по имени файла); сырой ответ → S3 `raw/`; нормализация в контракт результата; таймаут 5 мин.
- Маппинг: `financial_statement_meta/column/line/value` из JSON; числа, скобки, 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 — Консолидация: карточка, версии, периметр, периоды, сводная ОСВ
- `consolidation` / `consolidation_version` / `consolidation_member` / `consolidation_document`; статусы INITIATED →
DOCUMENTS_PROCESSING → DATA_CORRECTION; `consolidation_lock`.
- Приведение квартальных ОСВ к нарастающим (§2) как вычисляемое представление.
- Сводная ОСВ (§3) как запрос; признак контрагента; ручное сопоставление с алиасами и автоприменением.
- UI: список консолидаций, карточка, состав, сводная ОСВ с фильтрами и экспортом xlsx.
- Тест: сводная ОСВ группы РБ за 9 мес. 2024: строки «Юг-Финанс / ПЕКАРНЯ СЧАСТЛИВОЧНАЯ» → ГСК после одного ручного
сопоставления.
## Срез 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`.
## Срез 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)` в коде.