arm-tabular-parser/CLAUDE.md
Andrey Epifancev 9b8b8ea727 fix: финальное ревью — лимиты чтения xlsx (I-1), лог traceback (I-2), ACCOUNT_RE (M-1), ИНН/КПП (M-3)
- I-1 (R-23): readers/_limits.py — сумма ZipInfo.file_size ≤ 10 × PARSE_MAX_BYTES (параметр read_book от
  OsvParser), потоковый скан <mergeCell ref> до openpyxl и площадь merged после чтения любого формата
  (MERGED_AREA_LIMIT = 200 000; реальный максимум 10 090) → UNSUPPORTED_FORMAT; дедлайн в циклах detect
  (поиск строки групп, дополнительные колонки); README «Лимиты» исправлен, «Известные ограничения» (defusedxml).
- I-2: OsvParser логирует стек непредвиденной ошибки (loguru ERROR), UNREADABLE — WARNING без стека.
- M-1: ACCOUNT_RE с re.ASCII | re.DOTALL (многострочное название после запятой, только цифры 0–9).
- M-3 (R-24): правило детектора ИНН не дополняет 9/11 цифр нулём; дополнение — только при извлечении
  (numbers.inn_digits) из колонки, выбранной по заголовку или по строгому правилу.
- T9.1: тесты R-21 (свёрнутый родитель на противоположной стороне; (0,0) при детях (X, X)).
- T1.1: хелперы фикстур пропускают скрытые файлы/каталоги и lock-файлы Excel (~$…).
- T12.3: golden-update не удаляет снимки части без исходного каталога (osv/synthetic или реальные файлы).

Golden-снимки не изменились (82/82).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
2026-10-05 16:29:53 +04:00

10 KiB
Raw Blame History

CLAUDE.md — tabular-parser

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

Что это

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

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

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

Тесты

  • property-тесты инвариантов (§1.4) на всех файлах ../fixtures/osv/** и ../fixtures/statements-xlsx/**;
  • снимки ../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 recognition.result — зона ядра) удалён задачей 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 на recognition.tabular.request → ack → S3 uploads/ → парсер → S3 results/ → recognition.result — 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); 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/, 82 файла: 66 реальных + 16 синтетических) — регрессия, а не оракул; правятся только make golden-update с просмотром diff и отдельным коммитом в fixtures. Корректность проверяют property-тест и ручная сверка «Итого».
  • Деньги — Decimal, float в домене нет; needs_check — только по контрольным суммам, структуру под сходимость не подгонять.

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

Настройки: 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.