Andrey Epifancev a15fe65d52 openspec: спека Среза 3a — горячие папки как у armka_file_worker, нормализатор OCR → statementDocument v1.2 (#6)
proposal, design (D-1…D-9, решения без обсуждения №1–12), дельты спек image-recognition-gateway
(папки, очистка под флагом, нормализация, справочники с редакциями, статусы) и scan-emulator (пара по sha256,
набор по умолчанию). tasks.md заполняется планом.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
2026-10-06 18:39:52 +04:00

7.0 KiB
Raw Blame History

Proposal: Срез 3a — горячие папки как у armka_file_worker и нормализатор OCR → statementDocument v1.2

Срез: 3a, первая половина Среза 3 «ББ/ОФР: scan-gateway, маппинг форм, логические проверки» (../docs/context/09-delivery-plan.md). Репозиторий: scan-gateway. Issue: RSHB/arm-scan-gateway#6. Соседние: fixtures — пары PDF+JSON и ocr/expected_normalize/ (issue заводится при плане); docs — bounded-PR без change (03-architecture.md, OQ-137, §11.1 п. 6 и п. 9, новые OQ). Срез 3b (маппинг statementDocument в ядре, проверки §7, подтверждение, чек-лист) — отдельный change в arm-core.

Why

Срез 0 оставил scan-gateway с NullNormalizer: любой успешный ответ ИС Сканирование превращается в NOT_IMPLEMENTED. Срез 2b даёт ядру statementDocument v1.2 из Excel; чтобы ядро в 3b маппило формы одним кодом для обоих источников, PDF-путь должен отдавать тот же документ. Второй повод — 06.10.2026 в контуре проверен PDFWorker старого АРМ (RSHB/armka_file_worker): его раскладка горячих папок ИС принята владельцем как эталон (OQ-137), а модуль очистки OCR (ofr_result_conversion) разрешено переносить (§11.1 05-domain-rules.md, единственное исключение для reference-old-arm).

What Changes

  • Горячие папки (src/gateway/): входящий файл — <Input>/<job_id>.<ext> (расширение исходного имени в нижнем регистре) вместо <job_id>_<original_name>; временный .part по-прежнему в <SCAN_TMP_DIR> (решение владельца: в Input временных файлов не класть, ИС может их подхватить), rename без замены — существующее назначение считается доставленным. Новый метод cleanup(job_id, ext) под флагом SCAN_CLEANUP (по умолчанию false): удаляет Output/<job_id>/*.json, каталог, входящий файл (и его .done-вариант эмулятора) и осиротевший .part; любое удаление только при включённом флаге (на SMB может не быть прав). Ошибка удаления не меняет исход задания.
  • Эмулятор (src/scan_emulator/): входящий <uuid>.<ext>; пара фикстур ищется по sha256 содержимого среди не-JSON файлов SCAN_FIXTURES_DIR (aep_6.pdf → aep_6.json + aep_6_*.json); промах → набор SCAN_EMULATOR_DEFAULT, если задан, иначе ответ-ошибка INVALID_DOCUMENT. .part/.tmp/.done игнорируются.
  • Нормализатор (src/normalizer/ocr/): OcrNormalizer реализует протокол Normalizer Среза 0 — конвейер чистых функций text → dates → amounts → layout → rows → header → document; проверенные на стенде функции старого ядра (двойники, |, разбор дат, суммы, раскладка с шапкой и без) переносятся дословно по §11.1; выход — pydantic-модели контракта v1.2 (StatementDocument), по одному документу на таблицу table_structure[i]. checks не считаются (их считает ядро в 3b для обоих источников).
  • Статусы: ошибочные файлы — как в Срезе 0; из успешных собираются документы; ни одной таблицы → error RECOGNITION_FAILED; всё распознано → done, иначе needs_check; причины — в metrics.warnings.
  • Правила как данные (api/rules/): statement_lines.yaml с редакциями форм (66n до отчётности за 2024 включительно — из codes_mapping старого ядра; fsbu4 с 2025 — пустая, данные положит владелец), ocr_lookalikes.yaml, statement_labels.yaml. Загружаются и валидируются при старте.
  • Настройки: SCAN_CLEANUP, SCAN_RULES_DIR, SCAN_EMULATOR_DEFAULT; SCAN_TMP_DIR остаётся. Dev-compose: SCAN_CLEANUP=true.
  • Тесты: юнит по функциям, golden на ../fixtures/ocr/aep_3.json, aep_6.json (эталоны ../fixtures/ocr/expected_normalize/), обновлённые тесты горячих папок, эмулятора, воркера; roundtrip с эмулятором.

Non-goals

  • Маппинг statementDocument в financial_statement_*, проверки §7, подтверждение документа, чек-лист, UI — Срез 3b.
  • Изменение контракта (v1.2 заморожен): редакция формы в конверт не добавляется (OQ, кандидат в MINOR 1.3).
  • Расшифровки без кода (OQ-36), склейка одной формы из нескольких таблиц (OQ-77), DISCLOSURE из OCR.
  • Заполнение словаря редакции fsbu4 и подбор пар PDF+JSON — владелец, отдельный PR в fixtures.
  • Перенос обвязки старого ядра (json_to_db_rows, запись в БД, pdfworker_results.py).

Параграфы 05-domain-rules.md

§11 (структура ответа ИС, маппинг таблица → документ), §11.1 п. 1–9 (очистка OCR-текста), §12 п. 4 (разбор дат — общий смысл с Excel-путём). Правила, которых в 05 нет и которые вводятся этим change'ом: редакции форм в справочнике кодов (ASSUMPTION(OQ-12): состав 2025 года не задан), is_recognized колонок шапки. Таблицы DBML не затрагиваются (сервис без БД).

Открытые вопросы, затрагиваемые change'ом

OQ-137 (уточнить: Tmp остаётся, очистка под флагом), OQ-36 (строки без кода не отдаются), OQ-77 (одна форма двумя таблицами — два документа с предупреждением), OQ-12 (редакции форм и §7), новые: права на удаление на SMB-шаре; редакция формы в контракте; §7/§12 по редакциям для 2b и 3b.