arm-tabular-parser/CLAUDE.md
Andrey Epifancev 388276c261 ББ/ОФР из Excel: значение ячейки — как показывает числовой формат (#42)
statement_money вместо to_money в разборе строк форм, предупреждения о неразобранном формате и об xlsb,
сравнение условий формата с точностью Excel (15 значащих цифр), тесты и тексты.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 17:26:36 +04:00

14 KiB
Raw Blame History

CLAUDE.md — tabular-parser

Общий контекст системы — ../CLAUDE.md и ../docs/. Этот файл — только про этот сервис.

Что это

Stateless-сервис разбора XLS/XLSX/XLSB: ОСВ по счетам (§1 правил) и ББ/ОФР из Excel (§12). Вход — NATS files.tabular_uploaded (по умолчанию; задаётся OPENBAO__NATS__NATS_*_SUBJECT) + файл в S3 uploads/; выход — JSON по ../docs/contracts/recognition-result.schema.json в S3 results/ + NATS files.tabular_parsed. Нет доступа к БД. Не знает про организации, периметры, план счетов — возвращает структуру файла; нормализация и резолв — в arm-core.

Главное правило

Структура определяется динамически по содержимому (§1.0 правил): никаких координат ячеек, имён листов, букв колонок в коде. Словари синонимов — rules/*.yaml. Фикстуры ../fixtures/osv/** — регрессия, не спецификация формата: новый формат = новая фикстура + синоним, не новый if.

Тесты

  • property-тесты инвариантов (§1.4) на всех файлах ../fixtures/osv/** и ../fixtures/statements-xlsx/**; для пакета ДКБ ../fixtures/osv-dkb/** — tests/fixtures/test_osv_dkb.py: ожидаемые needs_check — манифест tests/osv_dkb_expectations.yaml (данные), период — по имени каталога (2025-Q3, 2024-10_2025-09);
  • снимки ../fixtures/golden/expected_osv_parse/*.json — после ручной сверки «Итого»;
  • синтетика только для вариантов, которых нет в фикстурах (шапка вариант 2, Вал./Кол., xlsb, без outline level).

Соглашения шаблона

Тот же шаблон команды, что у arm-core («Шаблон FastAPI», RSHB/DKB_ARMKA@ba172f8); общие соглашения — ../docs/context/03-architecture.md, «Соглашения шаблона команды». Репозиторий приведён к сервису без фронта и без БД (change slice-0-skeleton). Команды backend — из api/, make — из корня репозитория.

Оставлено от шаблона: api/run_server.py (точка входа платформы, SERVER__HOST/PORT), api/src/app.py (lifespan: подключение к MinIO и NATS, ошибка подключения логируется и не мешает старту; GET /healthcheck, gzip/proxy middleware), core/config.py (pydantic-settings, SERVICE_NAME — только service.name конвертов (queue group — константа в core/nats.py), OPENBAO__MINIO__* включая MINIO_SECURE, OPENBAO__NATS__*, DEBUG, ENV_FILE, NATS_ENABLED, WORKER_CONCURRENCY, PUBLISH_RETRIES, DONE_CACHE_SIZE), core/clients.py (S3), core/logger.py, core/exception_handlers.py (без SQLAlchemy), common/exceptions.py, pytest.ini, requirements*.txt, raisa_config.yml, setup/setup.sh + pip.conf (без npm-шага). Шаблонный core/nats_callback.py (consumer files.tabular_parsed — зона ядра) удалён задачей 4.3: его место занял воркер core/worker.py.

Вырезано (Срез 0): web/ целиком и setup/.npmrc; из app.py — mount SPA (SPAStaticFiles, os.makedirs("web")), CORS, регистрация SMB-сессии, импорт raisa_fastapi_protected_api, зависимость от SQLAlchemy в обработчиках ошибок; db/, models/, repositories/, alembic/, alembic.ini; из настроек — OPENBAO.DATABASE, OPENBAO.SMB; из requirements.txt — asyncpg, SQLAlchemy, alembic, smbprotocol, argon2-cffi, greenlet, Mako, raisa_fastapi_protected_api; шаблонный README.md заменён описанием сервиса.

Добавлено (в скобках — задача openspec/changes/slice-0-skeleton/tasks.md):

  • api/src/parser/ — интерфейс Parser и заглушка NullParser; разбор ОСВ §1 — Срез 2a (см. ниже), форм §12 — Срез 2b;
  • api/src/contract/ — pydantic-модели контракта v1.1 (запрос, результат, конверт); api/contracts/ — копии схем (идентичность с ../docs/contracts проверяется в make check) — 3.1;
  • api/rules/ — README о назначении; сами словари *.yaml (в документах rules/*.yaml относительно api/) появились в Срезе 2a — 4.1;
  • api/src/core/nats.py и api/src/core/worker.py — подписка queue group tabular-parser на files.tabular_uploaded → ack → S3 uploads/ → парсер → S3 results/ → files.tabular_parsed — 4.2–4.3;
  • api/pyproject.toml (ruff, mypy) и Makefile — 2.1; Dockerfile, api/.env.example, api/.env.test — 2.2.

Добавлено в Срезе 2a (парсер ОСВ; openspec/changes/slice-2a-osv-parser/)

Модули api/src/parser/ (конвейер сверху вниз; подробности и статусы — README.md, раздел «Парсер ОСВ»):

  • grid.py + readers/ — модель листа и чтение xlsx/xls/xlsb; единственное место, где допустимы индексы и буквы колонок (column_letter — только диагностика). read_book(data, file_type) -> (листы, предупреждения), формат — по сигнатуре.
  • rules.py — загрузчик словарей api/rules/*.yaml; detect.py — вид листа и заголовок таблицы; header.py — шапка и group_schema; hierarchy.py — каскад outline → indent → account_pattern → sums (+ trivial); inn_rows.py — строки ИНН рядом с контрагентом (OQ-163); numbers.py — Decimal и ИНН (контрольная сумма); classify.py — строки и группировки; checks.py — контрольные суммы §1.4; osv.py — оркестратор OsvParser; deadline.py — кооперативный таймаут; errors.py — коды ошибок; cli.py — разбор файла.
  • get_parser(settings) создаёт OsvParser один раз при старте (кэша нет); NullParser — только для тестов воркера.

Правила расширения:

  • Новый формат ОСВ = фикстура в ../fixtures/osv/ (или синтетика s1x_… в tests/synthetic/generate.py) + синоним в YAML. Не новый if под файл, организацию, имя листа или номер строки; «нужна координата» означает, что алгоритм не по содержимому — остановиться и обсудить.
  • Нет координат: AST-линтер tests/unit/parser/test_no_coordinates.py проверяет src/parser/** (кроме grid.py и readers/): литералы букв колонок и адресов ячеек, имена листов по умолчанию, сравнения номера строки с числом, обращения к ячейке константами. Значения enum контракта вычисляются из src.contract и под линтер не попадают.
  • Каждая запись словаря должна срабатывать хотя бы на одном реальном или синтетическом файле (tests/fixtures/test_rules_coverage.py); мёртвый синоним удаляется, пробел в фикстурах закрывается синтетикой.
  • Golden-снимки (../fixtures/golden/expected_osv_parse/, 123 файла: 72 реальных + 23 пакета ДКБ (osv-dkb/** до 1 МиБ, снимки в osv-dkb/) + 28 синтетических) — регрессия, а не оракул; правятся только make golden-update с просмотром diff и отдельным коммитом в fixtures. Корректность проверяют property-тест и ручная сверка «Итого».
  • Деньги — Decimal, float в домене нет; needs_check — только по контрольным суммам, структуру под сходимость не подгонять.

Нетривиальное правило §1.4 (R-21): при сверке «сумма детей = родитель» обороты сравниваются по колонкам, а сальдо — по колонкам либо по нетто дебет − кредит, если у родителя сальдо свёрнуто (1С показывает счёт нетто, а субсчета и контрагентов — развёрнуто). Это решение зафиксировано в design №31; не ослаблять и не расширять без обсуждения.

Счета без сальдо по аналитике (OQ-132, #11 п. 2): список счетов (91, 99) — данные api/rules/osv_checks.yaml, не код; тождество строки на них не проверяется, «сумма детей = родитель» (обороты всегда, сальдо — только корень → субсчета) и «Итого» сохраняются. Счёт 90 в список не входит. Не расширять список «чтобы файл прошёл» — только по решению владельца.

Настройки: PARSE_MAX_BYTES (по умолчанию 50 МиБ; больше — UNSUPPORTED_FORMAT) и PARSE_TIMEOUT_SECONDS (120 с; кооперативный, → TIMEOUT) — core/config.py. Фазу чтения таймаут не прерывает: до библиотеки чтения действуют лимиты readers/_limits.py (распакованный zip ≤ 10 × PARSE_MAX_BYTES, площадь объединённых ячеек ≤ 200 000; R-23).

Контракт v1.1: rows[].extra (нераспознанные колонки таблицы по заголовку); CONTRACT_VERSION = "1.1"; копия схемы в api/contracts/ идентична ../docs/contracts/ (проверяется в make check).

Тесты: make check ≈ 95 с, потому что make test (-m "not integration") включает тесты маркера fixtures (golden, property, покрытие словарей); без каталога ../fixtures они пропускаются. Полный набор — make check-full (+ интеграция с compose и сборка образа).

Проверки: make check (pytest, ruff check . && ruff format --check ., mypy src), make check-full перед PR.

Добавлено в Срезе 2b (парсер ББ/ОФР; openspec/changes/slice-2b-statement-parser/)

Модули: api/src/parser/book.py (BookParser: лимиты, чтение, дедлайн, вид листа, статус и карта ошибок; документы — ОСВ в порядке листов, затем формы) и api/src/parser/statements/ (detect → columns → header → lines → checks → merge/statement; подробности — README.md, раздел «Парсер форм (ББ/ОФР)»). get_parser(settings) теперь создаёт BookParser (листы ОСВ по-прежнему разбирает osv.py).

Правила расширения форм:

  • Новая форма или написание = строка в api/rules/statement_*.yaml (+ синтетика st0x в tests/synthetic/generate_statements.py или реальный файл в ../fixtures/statements-xlsx/). Не if под файл, организацию или лист. Соотношения §7 — данные statement_checks.yaml.
  • Координат в src/parser/** нет (кроме grid.py, readers/): тот же AST-линтер test_no_coordinates.py; слов формы в коде нет — только в YAML. Деньги — Decimal; единицы не нормализуются, первичка не мутируется.
  • Модули ОСВ (header.py, classify.py, checks.py, hierarchy.py) под формы не правятся.

Тесты на реальных данных: манифест ожиданий — api/tests/statements_manifest.yaml (18 файлов). Golden — два каталога в ../fixtures/golden/: expected_osv_parse и expected_statement_parse; правятся только make golden-update с просмотром diff и отдельным коммитом в fixtures.

Контракт v1.2: CONTRACT_VERSION = "1.2"; documents[] получает BALANCE/INCOME/DISCLOSURE (statementDocument: header, lines, checks); копия api/contracts/ идентична ../docs/contracts/. Значения ББ/ОФР из Excel — как их показывает числовой формат ячейки (#42, statements/number_format.py; xlsb — как в файле + предупреждение); ОСВ формат не применяет. Книга НПЗ в манифесте — done (итоги расходятся в пределах округления, OQ-138).