Andrey Epifancev e705d75520 feat(parser): контрольные суммы §1.4, статусы, OsvParser как парсер по умолчанию, лимиты
checks.py: тождество сальдо по каждой строке, суммы родителя = Σ детей того же data_type
(CURRENCY/QUANTITY исключены; обороты — по колонкам, сальдо — по колонкам или по нетто при
свёрнутом сальдо родителя), «Итого» = корневой счёт (R-13: нет корня — «Итого» пусто/0;
R-20: нет «Итого» — false и предупреждение).
osv.py: OsvParser — лимит размера, чтение по сигнатуре, листы по содержимому (формы ББ/ОФР и
прочие — пропуск с предупреждением), карта исходов D-6, кооперативный дедлайн, metrics
(rows без TOTAL, warnings с «sheet=<имя> hierarchy=<стратегия>» первой записью по листу).
get_parser(settings, rules_dir=…) → OsvParser (словари грузятся при создании: битый словарь —
падение старта); PARSE_MAX_BYTES / PARSE_TIMEOUT_SECONDS в Settings и .env.*; app.py передаёт
настройки. test_null_parser.py → test_parser_factory.py.
Тесты: юнит-карта статусов, «Испорченная сумма», «Пустая ОСВ», два листа ОСВ, новый синоним
без релиза кода (R-8); property-тест на 66 реальных файлах (все done, < 5 с, схема v1.1).
test_app и интеграционный round-trip ждут ошибку реального парсера вместо NOT_IMPLEMENTED.

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

8.9 KiB
Raw Blame History

Tasks: Срез 2a — парсер ОСВ (tabular-parser)

Каждая задача ≤ половины дня, начинается с тестов, заканчивается зелёным make check. Пути — от корня репозитория. Фикстуры — ../fixtures (worktree .wt/slice-2/fixtures, ветка feat/4-slice-2a-osv-parser); контракт v1.1 — ../docs/contracts (ветка feat/9-slice-2a-osv-parser).

1. Основание

  • 1.1 Зависимости и фикстуры: openpyxl==3.1.5, xlrd==2.0.2, pyxlsb==1.0.10 в api/requirements.txt; make venv; tests/fixtures_helpers.py (ARM_KA_FIXTURES, как в arm-core) и conftest.py с фикстурой osv_files (все файлы ../fixtures/osv/** кроме synthetic, параметризация по относительному пути); маркер fixtures в деле; test_layout не ломается. Проверка: pytest -m fixtures --collect-only показывает 66 параметров.
  • 1.2 Контракт v1.1: скопировать ../docs/contracts/recognition-result.schema.json v1.1 в api/contracts/, OsvRow.extra: dict[str, str | None] | None, CONTRACT_VERSION = "1.1"; примеры docs проходят; тест идентичности зелёный; воркер пишет result_schema_version = "1.1" (test_worker обновлён). Проверка: make check.
  • 1.3 grid.py и читатели: Cell, Sheet, MergedRange, column_letter; readers/xlsx.py (openpyxl, полное чтение, outline_level, alignment.indent, bold), readers/xls.py (xlrd formatting_info, rowinfo_map, XF bold/indent), readers/xlsb.py (pyxlsb, без outline), readers/__init__.py:read_book(data, file_type) -> tuple[Sheet, ...] с определением по сигнатуре и предупреждением при несоответствии; ошибки → UnreadableError/UnsupportedFormatError. Тесты: три читателя на одном синтетическом листе дают одинаковые ячейки; реальные xls/xlsx читаются с outline; fixtures-тест: все 66 файлов читаются < 5 с каждый.
  • 1.4 Словари: api/rules/{osv_headers,osv_groups,periods,data_types}.yaml (содержимое по §1.1/§1.5 и синонимам из §1.0), src/parser/rules.py:load_rules(path) -> Rules (pydantic, компиляция регулярок с нормализацией ё/е и пробелов), тест на падение при битом словаре. Покрытие словарей — задача 4.3.

2. Обнаружение структуры

  • 2.1 detect.py: find_osv_sheets, find_table_header (группы по merged-тексту в 1–4 строках, Дебет/Кредит под группой, свёрнутое сальдо, отсутствие «начала», right_edge, text_col, inn_col по заголовку и по доле 10/12 цифр, extra-колонки). Unit-тесты на сетках: заголовок 1/2/3/4 строки, merged, без «начала», свёрнутое сальдо, КПП между ИНН и суммами, мусор правее. fixtures-тест: columns для РБ E/H/J/L/O/Q, для Счастливочной xls G/J/K/N/Q/S + inn=E.
  • 2.2 header.py: шапка документа (организация, счёт, период, data_type, валюта), group_schema вариант 1 и 2, has_inn, период → period_year, period_quarter, is_cumulative по periods.yaml (ASSUMPTION(OQ-02)). Unit-тесты: обе шапки, отсутствие полей → null + warning, субсчёт 58.03, все строки таблицы §1.5. fixtures-тест: у всех 66 файлов account_code_raw совпадает с кодом в имени каталога/файла по регулярке и период распознан.
  • 2.3 hierarchy.py: стратегии outline, indent, account_pattern, sums, каскад с проверкой непротиворечивости, parent_row_number. Unit-тесты на сетках для каждой стратегии и для отказа стратегии (одинаковые уровни); fixtures-тест: hierarchy=outline у всех 66.
  • 2.4 classify.py + numbers.py: классификация (ACCOUNT по регулярке, позиция в group_schema, OTHER при выходе, TOTAL, COUNTERPARTY с ИНН, Вал./Кол./код валюты → data_type), числа (Decimal, текстовые форматы, скобки, прочерки, экспонента), extra. Unit-тесты по §1.3 и §1.2; fixtures-тест: в schastl …62 за 3 кв 2024 «БП Аренда» → DEPARTMENT, «МАЛЕЧКИНСКАЯ ПТИЦЕФАБРИКА ООО» с ИНН, физлицо без ИНН.

3. Сборка и статусы

  • 3.1 checks.py + osv.py: инварианты §1.4 (±0.01, null = 0, CURRENCY/QUANTITY исключены), checks, статусы D-6, Deadline и PARSE_MAX_BYTES/PARSE_TIMEOUT_SECONDS в Settings (+ .env.example), OsvParser.parse(data, file_type) -> ParseOutcome, get_parser() возвращает его, NullParser остаётся только для тестов воркера. Unit-тесты: карта статусов (нет листов, нет заголовка, лимит, таймаут через монкипатч часов, исключение → RECOGNITION_FAILED), испорченная сумма → needs_check с номерами строк. fixtures-тест (property): все 66 — done, инварианты, корень = счёт шапки.
  • 3.2 Интеграция с воркером: test_worker на реальном OsvParser с байтами ОСВ_66 за 2024.xls (unit, без NATS) и интеграционный roundtrip (NATS + MinIO): конверт done, result_schema_version = "1.1", файл результата проходит схему. README: раздел «Запуск на файле» (python -m src.parser.cli <файл> — печать результата JSON; добавить CLI).

4. Синтетика, golden, линтер

  • 4.1 Генератор синтетики tests/synthetic/generate.py (детерминированный openpyxl) → ../fixtures/osv/synthetic/: вариант 2 шапки; без outline (отступы); без outline и отступов; Вал./Кол.; текстовые числа и скобки; два листа ОСВ + посторонний; без «начала» + свёрнутое сальдо; КПП-колонка; название субсчёта в строке; испорченная сумма; плюс один xlsb, сконвертированный вручную из синтетического xlsx и закоммиченный с пометкой в README fixtures. Тесты сценариев спеки на этих файлах.
  • 4.2 Golden: tests/golden_helpers.py (нормализация результата без volatile-полей), тест сравнения для всех fixtures/osv/** включая синтетику, make golden-update (перезапись + diff), первичная запись снимков в ../fixtures/golden/expected_osv_parse/ после ручной сверки «Итого» трёх файлов (РБ 60 1 кв 2025, Счастливочная 62 3 кв 2024 xls, 58.03) — сверка описывается в отчёте задачи.
  • 4.3 Линтер координат tests/unit/test_no_coordinates.py (AST по src/parser/** кроме grid.py, readers/) и тест покрытия словарей (каждая запись сработала хотя бы раз на фикстурах+синтетике; список несработавших — в сообщении).

5. Завершение

  • 5.1 Документы: README (архитектура парсера, статусы и коды, словари, golden-процесс, лимиты), CLAUDE.md (модули, правила расширения), rules/README.md; make check-full (lint, unit, fixtures, integration, docker) зелёный, вывод — в PR. Проверка: сценарии всех требований osv-parsing имеют тест (таблица в отчёте задачи).
  • 5.2 После merge: openspec archive slice-2a-osv-parser.