arm-ka-docs/context/09-delivery-plan.md

31 KiB
Raw Blame History

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; реализовано позже в tabular-parser PR #15); счета с буквой в первом сегменте не распознаются (OQ-103).

Срез 2b — ББ/ОФР из Excel (tabular-parser) — реализован, ожидает ревью и merge

  • 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) — реализован, ожидает ревью и 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 (контроллер, 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 «Первичные документы»

  • Панель «Первичные документы», окно загрузки, таблица ОСВ с деревом строк и подсветкой нераспознанных контрагентов.

DoD среза 2 целиком: все реальные файлы и вся синтетика проходят без единого правила под конкретный файл или координату; в коде парсера нет литералов букв колонок/номеров строк/имён листов (проверяется тестом-линтером).

Срез 3 — ББ/ОФР: scan-gateway, маппинг форм, логические проверки, подтверждение документа

  • scan-gateway: горячие папки (по FNR02) с заглушкой-эмулятором ИС Сканирование для dev (возвращает aep_*.json по имени файла); сырой ответ → S3 raw/; нормализация в контракт результата по §11 и §11.1 (очистка OCR-текста: перенос проверенного на стенде модуля ofr_result_conversion старого ядра — единственное разрешённое исключение для reference-old-arm, решение владельца 06.10.2026); таймаут 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 — Консолидация: карточка, версии, периметр, периоды, сводная ОСВ

Статус: реализован в arm-core (бэкенд + API + экспорт xlsx), октябрь 2026; UI — отдельный исполнитель. Объём по решению владельца 06.10.2026: бэкенд, API, экспорт xlsx. Issue arm-core#18, docs#19; спецификация — arm-core/openspec/changes/slice-4-consolidation/ (design D-1…D-16, «Decisions made without discussion» №1–№85), документный change — docs/openspec/changes/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 чек-листа; «Добавить версию», CONFIRM, EXTENDED, гудвилл, маршрут согласования (Срезы 6–7); ББ/ОФР в своде и чек-лист ББ/ОФР по реальным документам — Срез 3; шахматка и ВГО — Срез 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.

Срез 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) в коде.