- README: верхняя граница задержки settle, шаг 5 «Полного круга»; критерии ошибочного файла (корень не объект) - CLAUDE.md: по одному json на каждый распознанный документ; критерии ошибочного файла - design.md: D-5 — оговорка про нестроковый error.code отдельным предложением; №79 — источник указания, SQL уборки в порядке FK - open-questions-proposal.md п.20: замены ../docs/dev/README.md (185–187, 224–226, 252–253) и комментарий docker-compose.yml:184 - план: Task 6 — пометка про замену down -v (R-27) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
16 KiB
16 KiB
scan-gateway — доработка по реальным ответам ИС Сканирование (Срез 0, правки в PR #3)
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Branch: feat/1-slice-0-skeleton (worktree .wt/slice-0/scan-gateway), поверх уже выполненного плана 2026-10-03-slice-0-skeleton.md. PR #3 открыт, правки идут в ту же ветку.
Spec: openspec/changes/slice-0-skeleton/ (specs image-recognition-gateway, scan-emulator, design.md). Спека — единственный источник истины; задача 1 приводит её в соответствие с решениями человека 04.10.2026, остальные задачи реализуют обновлённую спеку.
Решения человека (04.10.2026) — основание правок
- OQ-78. ИС Сканирование кладёт все распознанные документы одновременно в каталог
<output>/<job_id>/— по одному json на документ. Шлюз читает все json каталога и склеивает в один ответ (как в старом кодеreference-old-arm/api/src/core/PDFWorker/PDFTempPoller.py, но с понятной структурой). - OQ-79. Временный файл атомарной записи уходит из
Inputв служебный каталог (SCAN_TMP_DIR, по умолчаниюTmp, рядом сInput/Output, на той же шаре/ФС, чтобыrenameоставался атомарным). - OQ-77 / OQ-76. Образцы реальных ответов добавлены в
../fixtures/ocr:aep_3.pdf+aep_3.json(успех: таблицы «Бухгалтерский баланс» 42 строки и «Отчет о финансовых результатах» 18 строк) +aep_3_ofr.json(второй ответ ИС для того же файла:INVALID_DOCUMENT),aep_6.pdf+aep_6.json(успех: баланс 122 строки с шапкой дат + ОФР 49 строк);aep_5.json,aep_8_form4.json— ответы-ошибки.
Факты о формате реального ответа (проверены по ../fixtures/ocr/aep_3.json, aep_3_ofr.json, aep_6.json)
- Корень:
{"file_uid": str, "error": {"code": str, "message": str}, "data_ocr": {...}}.data_ocr— объект, не список (label,organization,inn,units,date— каждый{"form_name": str, "confidence": number 0..100};table_structure— список таблиц{"label": {"form_name": "Бухгалтерский баланс" | "Отчет о финансовых результатах", "confidence"}, "rows": [{"row_index": int, "cells": [{"column_index": int, "value": str, "confidence": number}]}]}). - Успех =
error.code == ""(аerrorнеnull!):{"code": "", "message": ""}. Ошибка:codeнепустой (INVALID_DOCUMENT, …),data_ocrс пустыми полями иtable_structure: []. Проверка «errorне null» для реального ответа неверна. - В одном json может быть несколько таблиц разных форм (в
aep_3.jsonиaep_6.json— баланс и ОФР), у каждой свойlabel; тип документа задаётlabel. Таблицы не интерпретируются в Срезе 0 (Срез 3). confidence— числа 0–100 (OQ-13 закрыт для scan-gateway); поля организации/ИНН/даты — шумный OCR-текст вform_name(вaep_6.jsondate.form_name— мусор, даты периодов — в строке-шапке таблицыrow_index 0). Разбор — Срез 3, здесь не интерпретируется.
Global Constraints
- Сервис без БД и фронта (DECISION-4). Раскладка шаблона, команды из
api/. Python 3.11 (uv venv --python 3.11), NATS без JetStream, один бакет S3. CI нет —make check/make check-full. - Контракт v1.0 в
../docs/contracts/не менять (файлы схем/примеров не трогать;raw_s3_key— по-прежнему одна строкаraw/{job_id}.json). Нормализация вstatementDocument— Срез 3, не делать:NullNormalizerостаётся заглушкой. - Горячие папки: протокол
HotFolder(расширить), режимыlocal/smb; каталогиSCAN_INPUT_DIR(Input),SCAN_OUTPUT_DIR(Output),SCAN_TMP_DIR(Tmp, новый). Файл во входящем каталоге —<job_id>_<original_name>. - Сырой ответ — в S3
raw/{job_id}.jsonдо нормализации; в БД не регистрируется. Новая структура склеенного ответа —{"job_id": str, "files": [{"name": "<имя файла>", "response": <разобранный json> | null, "parse_error": str | null}, ...]}, файлы по возрастанию имени; вfiles— все*.jsonкаталога ответа, другие файлы игнорируются. - Готовность: каталог ответа считается готовым, когда в нём есть хотя бы один
*.jsonи набор*.jsonс их размерами не менялсяSCAN_SETTLE_SECONDS(по умолчанию 2) — защита от недописанного набора; таймаут задания по-прежнемуmin(reply_deadline, старт + SCAN_TIMEOUT_SECONDS). - Результат (Срез 0,
NullNormalizer, вход — склеенный ответ): файл сerror.code ≠ ""илиparse_error ≠ null— «ошибочный». Все файлы ошибочные →status = error,error.code— код первого ошибочного файла по имени (дляparse_error—INVALID_FORMAT),error.message— его сообщение,metrics.warnings— по строке на каждый ошибочный файл (<имя>: <код>: <сообщение>). Есть хотя бы один успешный файл →status = error,error.code = NOT_IMPLEMENTED(нормализация — Срез 3), ошибочные файлы — вmetrics.warningsтак же. Успех никогда не принимается за ошибку поerror != null. - Эмулятор: по входящему
<uuid>_<name>.<ext>кладёт вOutput/<uuid>/все фикстуры<name>.jsonи<name>_*.jsonизSCAN_FIXTURES_DIR(не префикс<name>*:aep_3не должен забиратьaep_30*), имена в каталоге ответа —<uuid>.jsonдля<name>.jsonи<uuid>_<suffix>.jsonдля<name>_<suffix>.json, подстановкаfile_uid = <uuid>; пары нет — один ответINVALID_DOCUMENTкак раньше; запись набора — атомарно целиком (сначала во временный каталог эмулятора, затем rename каталога) либо с паузой между файлами — на усмотрение, но поведение должно быть покрыто тестом «готовность по settle». - Линтеры: ruff (120, E,F,I,UP,B,PL) + mypy; pytest-asyncio auto; маркеры
integration/fixtures. Язык комментариев/коммитов — русский, идентификаторы — английские; коммит заканчивается строкойCo-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>.
Задачи
Task 1: [R1] Обновить change под решения человека
- В
openspec/changes/slice-0-skeleton/: обновитьproposal.md(разделы «Горячие папки», «Воркер», «Нормализатор», «Эмулятор»),design.md(D-1 горячие папки, D-3/D-4 сырой ответ и склейка, D-5 нормализатор, D-6 эмулятор; новые решения — продолжением нумерации таблицы «Decisions made without discussion», сейчас последняя строка №53),specs/image-recognition-gateway/spec.md(требования «Передача файла…», «Сырой ответ…», «Результат по контракту» — склейка всех*.jsonкаталога, готовность по settle,SCAN_TMP_DIR, per-file ошибки вmetrics.warnings, успех =error.code == "") иspecs/scan-emulator/spec.md(набор файлов на задание); отметить вopen-questions-proposal.mdпункты про OQ-77/78/79/76 как решённые человеком 04.10.2026 (с фактами выше);tasks.md— добавить группу «7. Доработка по реальным ответам ИС» с задачами 7.0–7.5 (7.0 = Task 1 этого плана, 7.1–7.5 = Task 2–6... по числу задач плана). Проверка:openspec validate slice-0-skeleton --strict— valid; в spec нет упоминаний «читает ровно<job_id>.json» и.partвInput.
Task 2: [R2] Горячие папки: чтение всех файлов, готовность, служебный каталог
src/gateway/и настройки:SCAN_TMP_DIR(Tmp),SCAN_SETTLE_SECONDS(2); протоколHotFolder.poll(job_id)возвращаетNone(ответа нет/ещё не готов) либо списокAnswerFile(name: str, data: bytes)в порядке возрастания имени — только*.jsonкаталога<output>/<job_id>/, с проверкой готовности по settle (набор имён и размеров не менялсяSCAN_SETTLE_SECONDS);LocalHotFolderиSmbHotFolder— временный файл записи (.part) создаётся в<root>/<SCAN_TMP_DIR>/(каталог создаётся, если нет; на той же ФС/шаре) и переименовывается вInput/<name>(атомарно);healthy()без изменений. Тесты:tmp_path— во время записи вInputнет.part, после put там только итоговый файл; poll: каталога нет →None; один json → после settle список из одного; два json → оба, по имени; файл дописывается (размер растёт) →Noneдо стабилизации; посторонний не-json файл игнорируется;SmbHotFolder— с подменённымsmbclient. Проверка:make checkзелёный.
Task 3: [R3] Склейка ответа, сырой файл, нормализатор, воркер
src/gateway/(илиsrc/normalizer/): функция склейкиmerge_answers(job_id, files) -> MergedResponseпо структуре из Global Constraints (разбор каждого файлаjson.loads, невалидный json →parse_error); воркер: опросpoll→raw/{job_id}.jsonв S3 = склеенный ответ (до нормализации;raw_s3_keyв конверте) →Normalizer.normalize(merged);NullNormalizerпо правилам Global Constraints (успех =error.code == ""; регрессионный тест на реальныхaep_6.jsonиaep_3.json, которые не должны трактоваться как ошибка, наaep_3_ofr.jsonиaep_5.json— как ошибкаINVALID_DOCUMENT;aep_3.json+aep_3_ofr.jsonвместе →NOT_IMPLEMENTEDс одной строкой warnings проaep_3_ofr.json; все файлы ошибочные → код первого);metrics.warningsв конверте результата. Тесты: unit наmerge_answers(порядок, невалидный json), нормализатор на реальных фикстурах (маркерfixtures), воркер с фейками (склеенный raw в S3 содержит все файлы; ошибка одного файла не роняет задание;parse_error→ warnings). Проверка:make checkзелёный.
Task 4: [R4] Эмулятор: набор файлов на задание
src/scan_emulator/: по входящему<uuid>_<name>.<ext>собрать изSCAN_FIXTURES_DIRвсе<name>.jsonи<name>_*.json(точное правило из Global Constraints), записать вOutput/<uuid>/под именами<uuid>.json/<uuid>_<suffix>.jsonсfile_uid = <uuid>; пары нет — прежнийINVALID_DOCUMENT;aep_3не забираетaep_30*. Тесты (маркерfixtures,tmp_path):aep_3.pdf→ два файла (<uuid>.jsonуспех,<uuid>_ofr.jsonошибка),aep_6.pdf→ один,aep_5.PDF→ один с ошибкой,aep_8.pdf→aep_8_form4.jsonкак раньше (правило<name>_*.jsonпокрываетaep_8_form4), неизвестное имя →INVALID_DOCUMENT; сквозной тест «put → эмулятор → poll (с settle) → merge» наaep_3. Проверка:make checkзелёный.
Task 5: [R5] Интеграционные тесты и документация
- Интеграционный тест (
tests/integration, маркерintegration): реальные NATS и MinIO, эмулятор и шлюз в процессе теста:../fixtures/ocr/aep_3.pdfвuploads/…→ ackprocessing, затем конвертerror NOT_IMPLEMENTEDсmetrics.warningsпроaep_3_ofr.json,raw/{job_id}.jsonв MinIO содержит два файла;aep_6.pdf→NOT_IMPLEMENTEDбез warnings;aep_5.PDF→INVALID_DOCUMENT. АктуализироватьREADME.mdиCLAUDE.md(раскладка каталога ответа,SCAN_TMP_DIR,SCAN_SETTLE_SECONDS, склеенный raw, успех =error.code == ""),.env.example/.env.test. Проверка:make check-full(с поднятым compose-проектомarm-ka:make up) — exit 0; вывод в отчёт.
Task 6: [R6] Проверка в реальном compose
- Без правок
../docs:docker compose -f ../docs/dev/docker-compose.yml --profile services up -d --buildнужных сервисов (scan-gateway,scan-emulator+ зависимости;--waitпо именам сервисов, не голый), затем сценарии через реальный compose: положитьaep_3.pdf,aep_6.pdf,aep_5.PDFиз../fixtures/ocrв MinIO и вставить задания способом из../docs/dev/README.md(«Проверка окружения»); зафиксировать выводы (в т.ч. содержимоеraw/<job_id>.json) в отчёт. В конце окружение остановить (down -v— заменено R-27, design #77:rm -sfvсобственных сервисов). Если../docs/dev/docker-compose.ymlтребует правки (например, переменныхSCAN_TMP_DIR/SCAN_SETTLE_SECONDS) — не править, а записать точный фрагмент вopen-questions-proposal.md.