26 Commits

Author SHA1 Message Date
Andrey Epifancev
817487b976 nats: subject'ы как в старом АРМ — files.*_uploaded, files.tabular_parsed (#28)
Решение владельца 08.10.2026: recognition.tabular.request → files.tabular_uploaded,
recognition.image.request → files.img_uploaded, recognition.result →
files.tabular_parsed (общий subject ack и результатов; files.img_parsed не
используется). Тела сообщений, Nats-Msg-Id, queue group'ы и schema_version не
меняются. Константы api/src/contract/subjects.py, докстринги, тесты, README,
CLAUDE.md; копии схем api/contracts/ из docs/contracts/ побайтно; change
nats-files-subjects с дельтой спеки. Сквозной change — RSHB/arm-ka-docs#74.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
2026-10-08 23:46:19 +04:00
Andrey Epifancev
49400682bc feat(osv): счета без сальдо по аналитике (91, 99) вне тождества строки (OQ-132, #11 п. 2)
У 91 и 99 строки аналитики несут только обороты, сальдо закрыто на субсчёт закрытия
(91.09), поэтому каждая ОСВ 91 получала needs_check. Список счетов — данные
api/rules/osv_checks.yaml (accounts_without_analytic_balance: 91, 99; 90 не входит).

- тождество строки на таких счетах не проверяется;
- «сумма детей = родитель» сохраняется: обороты всегда, сальдо — только у родителя
  с дочерними субсчетами (корень -> субсчета); у родителя с одной аналитикой сальдо
  не сверяется;
- «Итого» сверяется как обычно;
- строка относится к счёту списка по коду ближайшего счёта ветки (91, 91.01; не 910).

Тесты: unit checks/rules, манифест osv_dkb_expectations (две ОСВ 91 теперь done).
README, CLAUDE.md, openspec/specs/osv-parsing, api/rules/README.md обновлены.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
2026-10-08 00:18:36 +04:00
Andrey Epifancev
be67828cbb feat(config): несекретные настройки из OpenBao через OPENBAO__APP__<ИМЯ> (#22)
Тот же источник настроек, что в arm-core#44: приоритет обычный env -> OPENBAO__APP__* -> .env -> умолчание;
неизвестное имя — ошибка при старте; ключ APP не ломает OpenBAOSettings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EAKRDNzXtRrwmWz9z6PZu3
2026-10-07 18:39:31 +04:00
Andrey Epifancev
1abe212524 fix(statements): финальная волна ревью — I-1 (O-9), M-10, I-2, I-3, M-5, M-8, M-11, T13.5, M-6, M-7, T15.1, T15.2
I-1: лист с 1–2 кодами после листа формы — продолжение (min_codes_continuation в словаре).
M-10: тип формы маркера продолжения запрещает присоединение к документу иного типа.
I-2: одиночный месяц не задаёт границы при дате на число/квартале (not_single_month).
I-3: название строки из ближайшей текстовой ячейки левее кодов, пустое имя без confidence.
M-5: units_thousands и okud_code в словаре, без type: ignore в detect.py.
M-8: строка нумерации и нули не считаются строкой без кода с суммами.
M-11: предупреждение о дробных значениях — одно на документ.
T13.5: _upper_header_row возвращает верхнюю строку при склейке хотя бы одной колонки.
M-6/M-7/T15.1/T15.2: остатки statement_markers, описание fractional_share_percent, README, тест воркера.

Refs #14

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EAKRDNzXtRrwmWz9z6PZu3
2026-10-06 21:58:24 +04:00
Andrey Epifancev
be47e82e72 docs/openspec: перенумерация OQ-137…142 -> OQ-138…143 (R-15)
Номер OQ-137 в main docs занят вопросом о раскладке горячих папок; design: решение №26.

Refs #14

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: cedd7915-2efe-41e5-988c-a9117033598b
2026-10-06 21:28:25 +04:00
Andrey Epifancev
3a370d4872 docs(statements): README и CLAUDE.md — парсер форм; интеграционный тест книги НПЗ через воркер
Тест ожидает needs_check (рубли при шапке «тыс. руб.», OQ-137).

Refs #14

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: cedd7915-2efe-41e5-988c-a9117033598b
2026-10-06 21:11:36 +04:00
Andrey Epifancev
43ee753d74 refactor(parser): BookParser — оркестратор книги; OsvParser — конвейер листа ОСВ; заглушка форм до конвейера
Листы-продолжения форм (STATEMENT_CONTINUATION) идут в конвейер форм, а не в разбор ОСВ (R-5).

Refs #14

Co-Authored-By: Claude Sonnet <noreply@anthropic.com>
Claude-Session: cedd7915-2efe-41e5-988c-a9117033598b
2026-10-06 15:49:27 +04:00
Andrey Epifancev
a592c68509 feat(osv): ИНН из текста названия контрагента, ИНН строкой выше, «ИНН/КПП» в ячейке
- OQ-100: ИНН в конце названия («Полиграф, 3528315710», «(ИНН …)», пробел) уходит в counterparty_inn, имя — без ИНН
  и разделителя; колонка ИНН приоритетнее; только для COUNTERPARTY.
- OQ-130: строка без сумм с одним ИНН над строкой с суммами того же уровня склеивается со следующей и не уходит в OTHER.
- «ИНН/КПП» в ячейке колонки ИНН даёт ИНН.
- Правила — osv_headers.yaml (counterparty_inn_suffix, inn_only_row, inn_kpp_cell), не код.
- Синтетика s17–s19 (генератор, тесты); golden — в репозитории fixtures (feat/9-golden).

Stacked on #13 (feat/11-dkb-formats).
Closes #9

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Claude-Session: cedd7915-2efe-41e5-988c-a9117033598b
2026-10-06 13:13:44 +04:00
Andrey Epifancev
af8ec918c5 feat(osv): шапка ОСВ без меток и «Счет: Код / Наименование» (ОСВ 63 Новоросметалл), «Итого» ↔ Σ уровня 0 по нетто
- header: период отдельной ячейкой (periods.yaml: standalone), тип данных отдельной ячейкой («Бухгалтерский»,
  data_types.yaml: standalone_header), организация — хвост названия ведомости; код счёта — из первой строки-счёта
  таблицы (с предупреждением), если «по счету» в шапке нет.
- classify/detect: текст строки может стоять правее колонки текста (колонка задаёт уровень), подписи «Код»/
  «Наименование» (osv_headers.yaml: column_captions) — не группировки, строка сумм без подписи в конце таблицы —
  «Итого» (с предупреждением).
- checks: сумма уровня 0 сверяется с «Итого» по правилу R-21 (свёрнутое сальдо «Итого» — по нетто), как у родителя.
- tests: osv_dkb_files, покрытие словарей включает osv-dkb/**; README.

Refs #11

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Claude-Session: cedd7915-2efe-41e5-988c-a9117033598b
2026-10-06 13:04:31 +04:00
Andrey Epifancev
1eb57da5cb fix(readers): предсканирование xlsx без sniff — expat по всем частям архива, потолок крупных merged в xls (I-1б)
Предскан объединённых ячеек 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
2026-10-05 18:11:19 +04:00
Andrey Epifancev
471ae70873 docs: README — остаточные риски предсканирования merge-диапазонов (R-26)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
2026-10-05 17:18:32 +04:00
Andrey Epifancev
6f579b6403 fix: merge-bomb — потоковый expat-скан всех XML-частей (I-1б), after-read считает только индексируемые диапазоны
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
2026-10-05 17:06:53 +04:00
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
Andrey Epifancev
7738221b5a docs: README/CLAUDE.md Среза 2a, tasks.md, спека-дельты
- README: парсер по модулям, статусы и коды, словари и добавление синонима, лимиты, golden-процесс, настройки PARSE_*
- CLAUDE.md: раздел «Добавлено в Срезе 2a» (модули, правила расширения, линтер, R-21); api/rules/README.md: ключи словарей
- openspec: tasks.md (5.1), design.md (D-1…D-7, O-7/O-8), спека-дельты osv-parsing (R-12…R-22) и tabular-recognition-service («Результат по контракту»)
- tests: check_snapshot в golden_helpers + тесты «нет снимка»/«регрессия»; locator в property-тесте; пример needs_check синхронизирован с docs

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
2026-10-05 15:53:11 +04:00
Andrey Epifancev
bc143ef140 test: golden-снимки, линтер координат, покрытие словарей; make golden-update
- 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
2026-10-05 15:28:41 +04:00
Andrey Epifancev
621c0b2dc4 feat: реальный парсер в воркере, CLI разбора файла, интеграционный сценарий
CLI `python -m src.parser.cli <файл> [--file-type]` печатает JSON результата (коды 0/1/2 по статусу, 3 — файл/использование);
сборка RecognitionResult выделена из воркера в чистую `build_result` без изменения логики; тесты воркера на реальном
OsvParser, интеграционный сценарий «Реальная ОСВ через воркер», README «Запуск на файле».

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
2026-10-05 14:51:01 +04:00
Andrey Epifancev
777d4aa850 contract: needs_check-пример по docs (строка 3 CURRENCY, уровень 2), README v1.1
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
2026-10-05 11:59:55 +04:00
Andrey Epifancev
8685441fcd fix: настройки — SERVICE_NAME только tabular-parser, PUBLISH_RETRIES ≥ 0 (M-1, M-3, M-4, M-5)
- SERVICE_NAME: Literal["tabular-parser"] (R-35); в config.py, .env.example,
  README и CLAUDE.md исправлено «имя queue group» — queue group задаёт
  константа REQUEST_QUEUE_GROUP в core/nats.py.
- PUBLISH_RETRIES: NonNegativeInt — 0 означает одну попытку без повторов,
  как и принимает NatsConnection.
- Удалён неиспользуемый модульный settings = get_settings() из config.py.
- pyproject.toml: убран override mypy для raisa_fastapi_protected_api
  (зависимость удалена D-1).
- tests/conftest.py: фабрика minimal_settings (Settings из временного .env с
  минимальным окружением, окружение процесса скрыто); test_config.py —
  умолчания при минимальном окружении (M-8), PUBLISH_RETRIES=0/-1,
  SERVICE_NAME≠tabular-parser.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-04 03:48:34 +04:00
Andrey Epifancev
54197a07db docs: README — адрес по умолчанию 0.0.0.0, а .env.example сужает до 127.0.0.1
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-04 03:33:45 +04:00
Andrey Epifancev
e0b2aaca9a docs: оговорка про queue group в интеграционных тестах, правка CLAUDE.md (T10.3, T3.3)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-04 03:29:28 +04:00
Andrey Epifancev
b7d4f0c17a test, docs: порядок старта и проверка бакета в тестах приложения; README/CLAUDE.md без устаревших «добавляется задачей», spec без номера рулинга
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>
2026-10-04 03:18:09 +04:00
Andrey Epifancev
a7862c1843 feat: запуск воркера в lifespan при NATS_ENABLED и поле nats = ok | down | off в /healthcheck
Подписка — через on_connected NatsConnection; остановка: воркер (дорабатывает очередь) → NATS → S3. Клиент S3
остаётся и при неудачной проверке бакета, отсутствие бакета — warning (T2.1). Тест lifespan и healthcheck с фейками
(T8.7); формулировки nats в README, .env.example и spec приведены к R-17.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-04 03:09:06 +04:00
Andrey Epifancev
6c8d7b6e9b fix: NATS — PUBLISH_RETRIES как повторы после первой попытки (R-16), flush после подписки, снятие подписок при сбое on_connected, проброс отмены в stop(), error-лог самопроизвольного закрытия
Находки ревью Task 8: T8.2–T8.6.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-04 02:57:23 +04:00
Andrey Epifancev
8e15696913 chore: pyproject.toml (ruff, mypy), requirements-dev.txt, Makefile, маркеры pytest
Задача 2.1. make test допускает код 5 pytest (тестов пока нет). README: все цели Makefile создаются в 2.1 (T3.1).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-04 02:32:49 +04:00
Andrey Epifancev
ffaa982fb1 docs: README с описанием сервиса и актуализация CLAUDE.md после выреза шаблона
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-04 02:30:05 +04:00
Andrey Epifancev
29e5cc5bf4 chore: каркас из шаблона команды (DKB_ARMKA@ba172f8, доменный код удалён) 2026-10-02 23:15:38 +04:00