Шаблоны ФСБУ разносят год по ячейкам «20» и « г.», по тексту дату не собрать; требование к ним — разбор без ошибки
и needs_check.
Refs #14
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: cedd7915-2efe-41e5-988c-a9117033598b
Proposal, design (D-1…D-11, решения без обсуждения №1–№11), новая capability statement-parsing,
дельта tabular-recognition-service (BookParser, schema_version 1.2), tasks. Решения владельца 06.10.2026:
только парсер и контракт; страницы-продолжения без склейки между файлами; единицы как есть; пятизначные
подстроки как строки; контракт MINOR 1.2; без запасного пути по именам.
Refs #14
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: cedd7915-2efe-41e5-988c-a9117033598b
Предскан объединённых ячеек xlsx отдаёт expat каждую некаталожную запись zip:
отбор «похоже на XML» по первым 64 байтам обходился листом в части без .xml
с ведущими пробелами и без XML-декларации (1,9 КБ → > 3 с, у ревьюера 27 с /
1,4 ГБ). Части *.xml — строго, остальные — мягко (ошибка разбора лишь
заканчивает скан части). После чтения любого формата число диапазонов листа
площадью > MAX_INDEXED_AREA ограничено LARGE_MERGED_COUNT_LIMIT = 1000:
Sheet.merged_span перебирает их линейно. README, design.md №39, дельта спеки.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
R-25. Регулярочный скан `<mergeCell ref>` в xl/worksheets/*.xml обходился файлом < 5 КБ четырьмя способами
(лист вне xl/worksheets, ссылка на символ в ref, атрибут a:ref перед ref, «>» в кавычках). Теперь до openpyxl —
потоковый разбор xml.parsers.expat (namespace-aware) всех записей zip *.xml и записей, похожих на XML (openpyxl идёт
к листу по цели связи и расширение не проверяет): элемент с локальным именем mergeCell в любом пространстве имён
(openpyxl берёт дочерние mergeCell по локальному имени), только ref без префикса, площадь по строке ref через
CellRange openpyxl, ранний отказ при превышении MERGED_AREA_LIMIT; битый XML части *.xml и неразбираемый ref —
UNREADABLE. Стоимость: 12 мс на самом большом реальном xlsx (openpyxl 0,58 с), 0,8 с на 2 млн ячеек (openpyxl 10 с).
N1: check_merged_area после чтения считает только диапазоны площадью ≤ grid.MAX_INDEXED_AREA (константа стала
публичной) — законный merged на весь столбец в xls больше не отвергается.
Спека и README: правила скана, не покрытые риски (объём ячеек, sharedStrings/стили, entity expansion); N2 —
слепая зона R-21: достаточно свёрнутого прямого родителя.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
- T13.1: contracts/examples/result-osv-needs-check.json — предупреждения как пишет парсер
(«sheet=Лист_1 hierarchy=outline», текст нарушения из checks.py), organization_inn = null, версия 1.0.0;
копия в docs байт в байт.
- T13.2: лист ОСВ — признак в первых 30 строках (SHEET_MARKER_ROWS); T13.3: сломанный code span.
- T13.4: остаточные риски R-21 списком (RR-1…RR-4, слепая зона «+X к обеим сторонам», (0,0) при (X, X), «12.ООО»).
- I-1/I-2/M-1/M-3 в тексте спеки: лимиты чтения (R-23) и сценарии, лог стека, флаги ACCOUNT_RE, R-24.
- T13.6: OQ-81 — «частично закрыт» в design.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
- golden_helpers, scripts/golden_update.py (--check), test_golden на 66 ОСВ и синтетике; make golden-update/golden-check/test-fixtures
- тест покрытия словарей (R-22): счётчик срабатываний Pattern; из osv_headers.yaml удалены неиспользуемые service_columns kpp/code/quantity/currency
- синтетика s14–s16 в генераторе и таблице ожиданий; skip вместо падения при отсутствии osv/synthetic (T11.1); комментарий R-21 в s07 (T11.2)
- линтер: предел адреса 10 вместо 4 (T3.1), обращение cell(3, 4) (T3.8); реальные файлы не фиксируют BU/RUB (T3.9)
- README: golden-процесс; отмечены Task 12 и пункты 4.2, 4.3
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
api/tests/synthetic: детерминированный генератор s01…s13 (openpyxl, метки времени книги и zip зафиксированы),
минимальный писатель BIFF12 для s13.xlsb (ruling R-9, читается pyxlsb), тесты сценариев спеки osv-parsing на файлах
из fixtures/osv/synthetic/ (таблица «файл → ожидания», s13 равен s02 кроме предупреждения xlsb), тест побайтовой
воспроизводимости генерации. synthetic_files() учитывает только файлы Excel (README.md каталога не файл набора).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
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
classify.py: collect_body_rows — строки таблицы до «Итого» (пустые, из пробелов,
повторы заголовка и строки правее таблицы пропускаются; подвал после «Итого» не данные),
«Итого» по тексту и корневому оформлению до уровней, без требования сумм (R-13, T3.3),
шесть сумм через to_money с номером строки в предупреждениях, свёрнутое сальдо по знаку
(R-1); classify_rows — ACCOUNT по ACCOUNT_RE, позиция в схеме, OTHER за схемой, TOTAL,
COUNTERPARTY с ИНН (inn_digits), Вал./Кол./код валюты -> data_type, extra без
дубликатов ключей и без колонок подвала (T4.5).
Fixtures-тесты иерархии (R-2) и классификации на всех 66 реальных ОСВ.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
hierarchy.py: BodyRow, Levels, стратегии outline (сдвиг к 0), indent (ранги
различных отступов — единицы alignment.indent и пробелов не смешиваются),
account_pattern (ранг кода + жирность, ASSUMPTION в docstring), sums
(жадно, без рекурсии); is_consistent, parents (по позициям и уровням,
Review Focus 3), resolve_levels: «Итого» в стратегии не передаётся и
получает уровень 0; меньше двух строк кроме «Итого» — дерево «trivial».
deadline.py: кооперативный Deadline (R-3). Unit-тесты; fixtures-тест — в
Задаче 8 (R-2). Отметки плана и tasks.md 2.3.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
- header.py: parse_doc_header (организация, счёт, период, data_type, валюта, вариант шапки,
предупреждения на русском) и build_group_schema (вариант 1 — метки в text_col, вариант 2 —
«субконто: …»); has_inn по ячейке ИНН в строке метки (отдельной или в тексте метки)
- periods.yaml: end — конец текста периода («г.», «год»); PeriodRules.end
- линтер координат: Literal[1, 2] не считается индексацией ячейки
- fixtures-тест на 66 файлах: счёт по имени файла одним правилом без исключений
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
detect.py: classify_sheet (ОСВ / форма ББ-ОФР / прочее по словарю), find_table_header
(строка с ≥ 2 группами сумм, «Дебет/Кредит» под группой в пределах merged-диапазона
или до следующей группы, заголовок в 1–4 строки, в т.ч. «Нач. сальдо Дт» в одной
ячейке; без «начала»; свёрнутое сальдо в *_debit + collapsed; свёрнутые «Обороты» и
половинчатая пара Дт/Кт — InvalidFormatError с названием группы), text_col, inn_col
(заголовок «ИНН» или ≥ 80 % значений из 10/12 цифр), right_edge, extra_cols,
group_label_rows, first_data_row, is_header_repeat (две группы или копия строки блока).
Pattern.search_normalized — ячейка нормализуется один раз (T3.6). osv_groups: известные
группировки OTHER перечислены явно, чтобы строка-метка «Основные средства» в
неслитом заголовке была частью блока. Хелпер тестов gridbuilder (Sheet из «картинки»).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
Словари api/rules/{osv_headers,osv_groups,periods,data_types}.yaml и src/parser/rules.py
(normalize_text, Pattern, load_rules, pydantic-валидация с RuntimeError по файлу и ключу,
currency_aliases по R-6). AST-линтер координат tests/unit/parser/test_no_coordinates.py
перенесён из Task 12 (R-7, правила R-4/R-5). Тест формулировок реальных ОСВ.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
grid.py: Cell, MergedRange, Sheet (1-based, только непустые ячейки, ленивые
индексы строк и merged; диапазоны больше 10 000 ячеек проверяются перебором),
column_letter для диагностики. errors.py: ParserError и коды контракта.
readers: read_book определяет формат по сигнатуре (zip → XLSX/XLSB по
xl/workbook.bin, OLE2 → XLS), предупреждает о несовпадении с file_type;
ошибки библиотек → UnreadableError одной строкой. Числа — Decimal(str(value)),
bool отдельно, даты как datetime; xlsb без outline/merged с предупреждением.
Тесты: синтетическая книга openpyxl, подмена file_type, мусор/обрезанные
файлы, xlsb через подменённый pyxlsb; 66 реальных ОСВ читаются < 5 с с
outline и merged; xls «62 за 3 квартал 2024»: строка 15 — outline 3.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
Требование «Ошибки транспорта превращаются в результат» приведено к коду:
SCHEMA_MISMATCH публикуется, если читаемы job_id, file_id и attempt и job_id
ещё не известен; иначе — лог (error/warning) без ack и результата. Добавлены
два сценария (их покрывают test_unreadable_request_is_logged_and_dropped и
test_malformed_request_for_{job_in_progress,done_job}_is_dropped).
В proposal.md поле healthcheck nats — ok/down/off.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Два подряд прогона: ruff/mypy чисто, unit 247 passed, integration 8 passed,
образ tabular-parser собран.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Приложение поднимается в процессе теста через lifespan с NATS_ENABLED=True; тест в роли ядра кладёт исходник в
uploads/, публикует запрос и собирает recognition.result по своему job_id: ack processing, затем error
NOT_IMPLEMENTED с правильными Nats-Msg-Id и валидные по схеме конверты; повтор запроса — только ещё один ack
(второго результата нет в окне тишины QUIET_WINDOW). Плюс сценарии spec: нет объекта в S3 -> FILE_READ_ERROR и
сервис работает дальше; schema_version 2.0 -> один SCHEMA_MISMATCH без ack.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
T9.2: события старта worker.start → nats.start; _connect_s3 — info/warning/error по ответу проверки бакета, клиент
остаётся. T9.3: spec без «R-17». T9.4: README и CLAUDE.md описывают уже существующие файлы без отсылок к будущим задачам.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>