Срез 0: скелет scan-gateway — без фронта и БД, горячие папки local/smb, воркер NATS без JetStream, эмулятор ИС Сканирование #3

Merged
aep merged 68 commits from feat/1-slice-0-skeleton into main 2026-10-04 18:41:20 +03:00
Owner

Issue #1 — реализация change slice-0-skeleton (48 коммитов после merge proposal #2). Не закрывает issue: закрытие — после применения compose-фрагмента в docs (см. ниже), openspec archive и команды «закрывай».

Что сделано

  • Вырезано из шаблона: web/, Alembic, db/, models/, repositories/, raisa_*; SMB оставлен (smbprotocol, сессия только при SCAN_HOTFOLDER_MODE=smb); README.md и CLAUDE.md переписаны.
  • Инструменты: api/pyproject.toml (ruff, mypy), Makefile (venv, lint, test, test-integration, check, check-full, up, emulator), Dockerfile (python:3.11-slim) и .dockerignore, .env.example / .env.test.
  • Контракт v1.0: копии схем и примеров в api/contracts/ (побайтово равны ../docs/contracts), pydantic-модели src/contract/, тесты.
  • Горячие папки: протокол HotFolder, LocalHotFolder (dev, тесты) и SmbHotFolder (smbclient, прод); фабрика create_hotfolder; имя файла <job_id>_<original_name>, запись атомарна (.part + rename).
  • Воркер на core NATS без JetStream: queue group scan-gateway; ack processing до чтения S3; LRU job_id; семафор SCAN_MAX_INFLIGHT=4; опрос ответа каждые 5 с до min(reply_deadline, старт + 300 с); сырой ответ в S3 raw/{job_id}.json до нормализации; результат и конверт с повторами публикации; транспортные ошибки всегда становятся результатом (SCHEMA_MISMATCH, FILE_READ_ERROR, RECOGNITION_FAILED, INVALID_FORMAT, TIMEOUT, UNKNOWN_ERROR); корректная остановка; /healthcheck с полями nats и hotfolder.
  • Нормализатор — интерфейс: NullNormalizer пробрасывает ошибки ИС, успех → NOT_IMPLEMENTED.
  • Эмулятор ИС Сканирование: python -m src.scan_emulator; фикстуры ../fixtures/ocr/<name>*.json, подстановка file_uid, неизвестное имя → INVALID_DOCUMENT, .done после обработки.

Проверки

make check-full на HEAD, exit 0: ruff, format, mypy чисто; 636 unit + 8 integration passed (реальные NATS и MinIO); docker build без Nexus (30 с холодная сборка). Живой прогон: /healthcheck 200 (nats ok, hotfolder ok); запрос для aep_5.pdf → ack processing → результат INVALID_DOCUMENT с raw_s3_key; объект raw/<job_id>.json в MinIO, .done в горячей папке; SIGTERM — штатное завершение. openspec validate --strict — valid.

Rulings I made

Полный список — openspec/changes/slice-0-skeleton/design.md, таблица «Decisions made without discussion» (№10–53). Ключевые: R-7 интеграционный тест запускает шлюз и эмулятор в процессе теста (контейнера scan-emulator в compose ещё нет) — рискованный, отсюда C-1 (эмулятор не стартовал без настроек шлюза, исправлено); R-8 argon2-cffi остаётся (транзитив miniopy-async); R-10 успех проверяется на синтетическом ответе: обе реальные фикстуры aep_5/aep_8_form4 — сами ответы-ошибки ИС; R-13 PUBLISH_RETRIES=3 = 1 попытка + 3 повтора; R-16 SCHEMA_MISMATCH без ack (согласовано с tabular-parser); R-17 дедуп по job_id (LRU); R-19 request.s3.bucket ≠ настроенного → FILE_READ_ERROR; R-20 дедлайн считается от записи файла.

Deferred (в follow-up issue при закрытии)

I-3 (replay с attempt+1 для job_id из LRU получает только ack — решение человека), T12.5 (штатная остановка не публикует конверты отменённых заданий: деплой превращает задания в работе в timeout), зависший poll/put по SMB до 60 с (нужен connection_timeout), json.loads ответа ИС в event loop, NATS_URL в логах, S3Storage.get_bytes без лимита размера, устаревший индекс codegraph и ещё ~60 minor по ревью задач (полный перечень в отчёте контроллера, ключевые — в open-questions-proposal.md).

Что нужно решить человеку

  • I-2: требование «эмулятор из того же образа в compose» доставляется PR в docs (профиль services); openspec archive этого change — только после применения фрагмента и проверки docker compose --profile services up -d --wait scan-gateway scan-emulator.
  • Предложения по контракту/OQ/compose — openspec/changes/slice-0-skeleton/open-questions-proposal.md.
  • Параллельные интеграционные тесты arm-core и шлюза делят NATS и queue group scan-gateway: не запускать одновременно.

Связанные

RSHB/arm-ka-docs (контракт v1.0, compose; PR после фазы 2) — тест идентичности контракта читает ../docs, docs должен влиться раньше или одновременно. RSHB/arm-core PR #3, RSHB/arm-tabular-parser PR #3.

🤖 Generated with Claude Code

https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2

Issue #1 — реализация change `slice-0-skeleton` (48 коммитов после merge proposal #2). Не закрывает issue: закрытие — после применения compose-фрагмента в docs (см. ниже), `openspec archive` и команды «закрывай». ## Что сделано - **Вырезано из шаблона:** `web/`, Alembic, `db/`, `models/`, `repositories/`, `raisa_*`; SMB оставлен (`smbprotocol`, сессия только при `SCAN_HOTFOLDER_MODE=smb`); `README.md` и `CLAUDE.md` переписаны. - **Инструменты:** `api/pyproject.toml` (ruff, mypy), `Makefile` (`venv`, `lint`, `test`, `test-integration`, `check`, `check-full`, `up`, `emulator`), `Dockerfile` (`python:3.11-slim`) и `.dockerignore`, `.env.example` / `.env.test`. - **Контракт v1.0:** копии схем и примеров в `api/contracts/` (побайтово равны `../docs/contracts`), pydantic-модели `src/contract/`, тесты. - **Горячие папки:** протокол `HotFolder`, `LocalHotFolder` (dev, тесты) и `SmbHotFolder` (`smbclient`, прод); фабрика `create_hotfolder`; имя файла `<job_id>_<original_name>`, запись атомарна (`.part` + rename). - **Воркер на core NATS без JetStream:** queue group `scan-gateway`; ack `processing` до чтения S3; LRU `job_id`; семафор `SCAN_MAX_INFLIGHT=4`; опрос ответа каждые 5 с до `min(reply_deadline, старт + 300 с)`; сырой ответ в S3 `raw/{job_id}.json` до нормализации; результат и конверт с повторами публикации; транспортные ошибки всегда становятся результатом (`SCHEMA_MISMATCH`, `FILE_READ_ERROR`, `RECOGNITION_FAILED`, `INVALID_FORMAT`, `TIMEOUT`, `UNKNOWN_ERROR`); корректная остановка; `/healthcheck` с полями `nats` и `hotfolder`. - **Нормализатор — интерфейс:** `NullNormalizer` пробрасывает ошибки ИС, успех → `NOT_IMPLEMENTED`. - **Эмулятор ИС Сканирование:** `python -m src.scan_emulator`; фикстуры `../fixtures/ocr/<name>*.json`, подстановка `file_uid`, неизвестное имя → `INVALID_DOCUMENT`, `.done` после обработки. ## Проверки `make check-full` на HEAD, exit 0: ruff, format, mypy чисто; 636 unit + 8 integration passed (реальные NATS и MinIO); `docker build` без Nexus (30 с холодная сборка). Живой прогон: `/healthcheck` 200 (`nats ok`, `hotfolder ok`); запрос для `aep_5.pdf` → ack `processing` → результат `INVALID_DOCUMENT` с `raw_s3_key`; объект `raw/<job_id>.json` в MinIO, `.done` в горячей папке; SIGTERM — штатное завершение. `openspec validate --strict` — valid. ## Rulings I made Полный список — `openspec/changes/slice-0-skeleton/design.md`, таблица «Decisions made without discussion» (№10–53). Ключевые: R-7 интеграционный тест запускает шлюз и эмулятор в процессе теста (контейнера `scan-emulator` в compose ещё нет) — рискованный, отсюда C-1 (эмулятор не стартовал без настроек шлюза, исправлено); R-8 `argon2-cffi` остаётся (транзитив `miniopy-async`); R-10 успех проверяется на синтетическом ответе: обе реальные фикстуры `aep_5`/`aep_8_form4` — сами ответы-ошибки ИС; R-13 `PUBLISH_RETRIES=3` = 1 попытка + 3 повтора; R-16 `SCHEMA_MISMATCH` без ack (согласовано с tabular-parser); R-17 дедуп по `job_id` (LRU); R-19 `request.s3.bucket` ≠ настроенного → `FILE_READ_ERROR`; R-20 дедлайн считается от записи файла. ## Deferred (в follow-up issue при закрытии) I-3 (replay с `attempt+1` для `job_id` из LRU получает только ack — решение человека), T12.5 (штатная остановка не публикует конверты отменённых заданий: деплой превращает задания в работе в `timeout`), зависший `poll`/`put` по SMB до 60 с (нужен `connection_timeout`), `json.loads` ответа ИС в event loop, `NATS_URL` в логах, `S3Storage.get_bytes` без лимита размера, устаревший индекс codegraph и ещё ~60 minor по ревью задач (полный перечень в отчёте контроллера, ключевые — в `open-questions-proposal.md`). ## Что нужно решить человеку - **I-2:** требование «эмулятор из того же образа в compose» доставляется PR в `docs` (профиль `services`); `openspec archive` этого change — только после применения фрагмента и проверки `docker compose --profile services up -d --wait scan-gateway scan-emulator`. - Предложения по контракту/OQ/compose — `openspec/changes/slice-0-skeleton/open-questions-proposal.md`. - Параллельные интеграционные тесты arm-core и шлюза делят NATS и queue group `scan-gateway`: не запускать одновременно. ## Связанные `RSHB/arm-ka-docs` (контракт v1.0, compose; PR после фазы 2) — тест идентичности контракта читает `../docs`, docs должен влиться раньше или одновременно. `RSHB/arm-core` PR #3, `RSHB/arm-tabular-parser` PR #3. 🤖 Generated with [Claude Code](https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2) https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
aep added 50 commits 2026-10-04 04:16:48 +03:00
docs/superpowers/plans/2026-10-03-slice-0-skeleton*.md сгенерирован 1:1 из openspec/changes/slice-0-skeleton/tasks.md
(заголовки «Task N» для скриптов subagent-driven-development, Global Constraints из спеки и CLAUDE.md).
.gitignore: .codegraph/, .superpowers/, .bounded, .DS_Store.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Удалены web/, setup/.npmrc, api/alembic/, api/alembic.ini, api/src/db/,
api/src/models/, api/src/repositories/. Из setup/setup.sh убраны npm-шаги.
Из .gitignore убрана строка Dockerfile (образ сервиса будет под git).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- app.py: create_app(settings), SMB-сессия только при SCAN_HOTFOLDER_MODE=smb, без SPA/CORS/РАИСА/nkeys;
  /healthcheck отдаёт uptime и version
- config.py: ENV_FILE, get_settings() с lru_cache, параметры горячих папок/NATS, блок OPENBAO__SMB необязателен,
  валидация режимов local/smb; убраны DBSettings и OPENBAO.DATABASE
- exception_handlers.py: убрана зависимость от sqlalchemy
- requirements.txt: убраны asyncpg, SQLAlchemy, alembic, argon2-cffi(+bindings), raisa_*, greenlet, Mako
- api/.env.example: минимальный local-режим против compose; unit-тесты config/app

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
validate_name/validate_job_id вынесены из LocalHotFolder для повторного использования в SmbHotFolder.
Дополнительно отвергаются `: < > " | ? *` и управляющие символы 0x00-0x1F (в put, answer_path и inbox_name);
пробелы и кириллица в именах допустимы.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
SmbHotFolder: UNC-корень \\host\SMB_PATH (или полный UNC в SMB_PATH), put — `.<name>.part` + smbclient.replace,
poll — open_file(rb), ENOENT (в т.ч. нет каталога) -> None, healthy — listdir корня без исключений.
create_hotfolder(settings): local -> LocalHotFolder, smb -> SmbHotFolder с ленивым импортом модуля (R-6);
отсутствие импорта src.gateway.smb в режиме local проверяется в отдельном интерпретаторе.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
T8.1: inbox_name заменяет `: < > " | ? *` и управляющие символы на `_`, убирает завершающие точки/пробелы;
ValueError только если имени не осталось. validate_name/put остаются строгими.
T8.2: SmbHotFolder.poll возвращает None на STATUS_SHARING_VIOLATION/STATUS_DELETE_PENDING (ИС ещё пишет ответ),
монопольное открытие сохранено.
T8.3: при ошибке mkdir, кроме EEXIST/ENOENT, put продолжает, если smbclient.path.isdir(Input); иначе исходная ошибка.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
process_once(root, fixtures, delay): новые <uuid>_<имя> -> ответ из фикстуры (точное имя, затем префикс,
file_uid подставляется) либо INVALID_DOCUMENT, атомарная запись ответа, затем переименование в .done.
Цикл python -m src.scan_emulator с корректной остановкой по SIGINT/SIGTERM.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Пара фикстур a_b-1.json / a_b.json: префикс по алфавиту раньше точного имени, поэтому тест падает,
если шаг «сначала <name>.json» убран. Убран посторонний комментарий в соседнем тесте.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Ошибка ИС пробрасывается с кодом из ответа, неизвестный код/вид ошибки -> UNKNOWN_ERROR с исходным
представлением, успех -> NOT_IMPLEMENTED, не объект -> INVALID_FORMAT. Пакет добавлен в strict-список mypy.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Ошибки не проглатываются: S3ObjectNotFoundError (NoSuchKey) и S3ReadError для чтения, S3WriteError для записи,
исходная причина — в __cause__. JSON пишется в UTF-8 без экранирования, Content-Type application/json.
Шаблонные put_object/get_object удалены (не использовались); удалён и шаблонный nats_callback.py — его
единственный импорт S3Storage, сам он нигде не используется (слой NATS — следующим коммитом в src/core/nats.py).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
NatsGateway: подключение с nkeys/JWT или без учётных данных, NATS_ENABLED=false — без подключения,
автопереподключение nats-py, subscribe_requests в queue group scan-gateway, publish с Nats-Msg-Id и
повторами (PUBLISH_RETRIES попыток всего) -> PublishFailed, publish_result для конвертов ack/result.
src.core.clients и src.core.nats — в strict-списке mypy. Интеграционные тесты против compose-NATS/MinIO:
queue group, заголовок Nats-Msg-Id, roundtrip S3, отсутствующий ключ.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
publish делает первую попытку и до retries повторов (retries=0 — одна попытка), PublishFailed.attempts —
фактическое число попыток. Комментарий PUBLISH_RETRIES в config.py уточнён. Докстринги: PublishFailed не значит
«не доставлено» (T11.3), колбэк подписки должен возвращаться быстро (T11.4).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
on_request проверяет тело (SCHEMA_MISMATCH без ack, R-16), дедуплицирует по job_id
(задания в работе + LRU DONE_CACHE_SIZE, R-17) и запускает задачу: ack до чтения S3 и
до ожидания слота, семафор SCAN_MAX_INFLIGHT, handle. handle: S3 (один бакет, R-19) ->
горячая папка через asyncio.to_thread (R-15) -> опрос до min(reply_deadline, старт +
SCAN_TIMEOUT_SECONDS) (R-14, R-20) -> raw/{job_id}.json до нормализации (R-18) ->
нормализатор -> results/{file_id}/{job_id}/result.json -> конверт. Таблица ошибок D-7,
публикация через NatsGateway с повторами, PublishFailed логируется. Текст ошибки
очищается от одиночных суррогатов и ограничен 4000 символами (T10.1, T10.2).

Тесты: фейки S3/NATS с общим журналом событий, настоящий LocalHotFolder и process_once
эмулятора; каждый конверт проверяется JSON Schema контракта. Фикстура log_lines в
conftest перехватывает loguru. src.core.worker и src.core.health — в строгом mypy.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
src/core/health.py: HealthProvider — nats ok/down/disabled по is_connected; hotfolder —
healthy() в потоке под wait_for (2 с) с кэшем 5 с, зависшая проба не запускается
повторно (T8.4). Сбой регистрации SMB-сессии виден естественно: SmbHotFolder.healthy()
обращается к шаре с теми же учётными данными (T2.3).

app.py: в lifespan S3Storage, NatsGateway, Worker(NullNormalizer); подключение к NATS
и подписка — фоновой задачей (connect ждёт сервер), её сбой — logger.exception и
nats=down без падения HTTP; остановка отменяет подключение, останавливает воркер,
закрывает NATS и S3. Объекты lifespan — в app.state.

test_app.py: тесты lifespan переведены на NATS_ENABLED=false и tmp-корень (иначе фоновое
подключение шло бы в compose NATS), проверка полей healthcheck расширена nats/hotfolder.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Фейки S3/NATS, шпион горячей папки, Env, эмулятор ИС и eventually вынесены из test_worker.py
без изменения логики, чтобы ими пользовался отдельный тест параллелизма. make_env получил
подмену sleep (пауза между опросами горячей папки).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Сценарий spec «Пять заданий подряд» переписан детерминированно: паузы опроса горячей
папки управляются тестом (SteppedSleep), ожидания — по событиям с тайм-аутом. Проверяется:
пять ack'ов до освобождения слота, ровно четыре файла и четыре чтения S3 при занятых слотах,
пятый файл — только после итога освобождённого задания, у всех пяти — итоговый конверт.
Добавлены строгая очерёдность при SCAN_MAX_INFLIGHT = 1 и остановка воркера при очереди
на слот (T12.9): задачи отменены, утечек задач и слотов нет, новых файлов нет.
Прежний test_five_jobs_with_four_slots удалён из test_worker.py (без дублирования).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Lifespan приложения и эмулятор ИС в процессе теста (R-7) против compose NATS/MinIO, горячая папка в tmp-каталоге.
Три сценария (R-10): aep_5.pdf (реальная фикстура) — INVALID_DOCUMENT от ИС, повтор во время ожидания и после
результата даёт ещё один ack без второго файла; ok_doc.pdf (синтетический успех) — NOT_IMPLEMENTED; unknown.pdf —
INVALID_DOCUMENT эмулятора. Проверяются схема конверта, service.name, Nats-Msg-Id, raw/<job_id>.json байт в байт,
входящий файл против объекта S3, /healthcheck, остановка без незавершённых задач и ошибок в логе.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
README.md описывает сервис как он есть: границы (без БД и фронта), поток обработки,
протокол горячих папок, переменные окружения шлюза и эмулятора, коды ошибок,
контракт NATS, запуск (dev, полный круг против compose и эмулятора, проверки),
/healthcheck, Docker-образ и ограничения Среза 0. CLAUDE.md: «Соглашения шаблона»
переписаны по фактическому дереву api/, устаревший SCAN_EMULATOR=1 заменён на
`make emulator` / `python -m src.scan_emulator`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Добавлена цель docker-build (IMAGE ?= scan-gateway:dev, контекст — корень репозитория).
check-full вызывает шаги последовательно через $(MAKE), поэтому порядок и остановка на
первой ошибке гарантированы и при make -j. README/CLAUDE.md и help приведены в соответствие,
отмечены задача 15 плана и пункт 6.2 tasks.md.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- open-questions-proposal.md: compose-фрагмент scan-emulator/scan-gateway, таблица предложений по контракту и OQ,
  чеклист для человека (файлы в ../docs не менялись)
- .env.example: SCAN_EMULATOR_POLL_SECONDS; уточнён комментарий PUBLISH_RETRIES (повторы после первой попытки)
- отмечена задача 16 плана и пункт 6.3 tasks.md

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`api/src/__init__.py` оставлен только с docstring: `python -m src.scan_emulator` больше не импортирует `src.app`
и не требует `OPENBAO__*`. `run_server.py` импортирует `app` из `src.app`, `get_settings` — из `src.core.config`.
Регрессионный тест: subprocess эмулятора с окружением только PATH/ENV_FILE/SCAN_HOTFOLDER_PATH -> код 2,
сообщение про корень горячей папки, без ValidationError.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
На свежем томе `up -d --wait postgres nats minio` не запускал одноразовый `minio-init`, и интеграционные тесты падали
на NoSuchBucket. Добавлен шаг `docker compose run --rm minio-init` (идемпотентно, `--ignore-existing`, код выхода
пробрасывается); комментарий и help цели поправлены.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Конверт `result:<job_id>:<attempt>` битого запроса совпадал по Nats-Msg-Id с настоящим результатом, и ядро
(дедуп по result_msg_id) отбросило бы настоящий итог. `_reject` нормализует job_id (`uuid.UUID`, нижний регистр)
и при `_seen` только пишет предупреждение. Тест: битый запрос с тем же job_id во время ожидания ИС и после итога,
в том числе UUID в верхнем регистре — опубликованы только ack и настоящий итог.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Запрос с одиночным суррогатом в `original_name` отвергается схемой (pydantic `string_unicode`), а не «не ломает
публикацию»: тест переименован в `..._is_schema_mismatch` и проверяет код SCHEMA_MISMATCH, отсутствие ack и `put`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
T7.3: имя из 124+ кириллических символов давало `.part` длиннее 255 байт -> ENAMETOOLONG на ext4 и SMB/NTFS
-> ложный RECOGNITION_FAILED. `inbox_name` обрезает основу по code points так, чтобы `.<job_id>_<имя>.part`
занимал не больше 240 байт UTF-8, сохраняя расширение (до 16 символов после последней точки); короткие имена не
меняются, ValueError — по-прежнему только для пустого имени, `.` и `..`.
T7.5: `LocalHotFolder.put` подавляет OSError при удалении `.part` (`contextlib.suppress`), наружу выходит исходное
исключение; в `smb.py` это уже было.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Функция осталась от шаблона и нигде не вызывается (grep по api/, включая тесты).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
README: настройки шлюза читаются при импорте `src.app`, пакет `src` и эмулятор их не читают (после C-1);
`data_ocr` реальных фикстур — объект с пустыми полями и `table_structure: []`; непустой `data_ocr` в `ok_doc.json` —
требование тестовой фикстуры, не NullNormalizer; в «Ограничения Среза 0» — зависший вызов SMB и поздний TIMEOUT
(T12.2/T8.4), перечитывание недописанного ответа (T3.5), битая фикстура эмулятора.
CLAUDE.md: TIMEOUT по `min(reply_deadline, старт + SCAN_TIMEOUT_SECONDS=300)`; заглушки шаблона перечислены.
design.md, строка 32: формулировка про `data_ocr` реальных фикстур.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
open-questions-proposal.md: SCAN_GATEWAY_PORT=8002 для `../docs/dev` (T16.1); проверки после применения
compose-фрагмента — `up -d --wait scan-gateway scan-emulator` с кодом 0 и строки лога эмулятора в формате `run()`
(T16.2, I-2); пометка, что фрагмент рабочий только после C-1 (проверено `docker run`, код 2); I-3 — replay
с `attempt + 1` для задания из LRU; `make up` в tabular-parser без `minio-init` (только сообщить); в «Что осталось
человеку» — не архивировать до проверки фрагмента, решение по I-3, приоритет T12.5.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
aep added 18 commits 2026-10-04 15:19:14 +03:00
Решения человека 04.10.2026 (OQ-76/77/78/79) и факты о формате ответа по новым фикстурам aep_3 и aep_6:
data_ocr — объект, успех = error.code == "", таблиц в одном json несколько.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
Склейка всех json каталога ответа (settle), SCAN_TMP_DIR вне Input, успех = error.code == "",
per-file ошибки в metrics.warnings, набор файлов в эмуляторе; решения №54-62 в design.md;
OQ-76/77/78/79 отмечены решёнными; группа 7 в tasks.md.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Заменённые решения помечены в design.md (№32, 35, 43, 48), добавлены №63-66 (R-22, R-23, R-25, R-28),
маркеры R-n (design #m); уточнены правила результата (UNKNOWN_ERROR, не-объект, коды в warnings),
сценарии таймаута по settle, имена файлов <job_id>*.json, ответ эмулятора «нет пары» с data_ocr-объектом.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- HotFolder.poll(job_id) -> list[AnswerFile] | None: все *.json каталога <output>/<job_id>/ по возрастанию
  имени, когда набор не пуст и имена/размеры не менялись SCAN_SETTLE_SECONDS (SettleTracker: состояние по
  job_id, внедряемые часы, сброс при выдаче/опустении, предел 1024 задания, потокобезопасно);
- put: временный .<name>.part в <корень>/<SCAN_TMP_DIR>/ (создаётся), rename в Input (Local и SMB);
- настройки SCAN_TMP_DIR (Tmp) и SCAN_SETTLE_SECONDS (2, >= 0); в .env.test settle = 0 (R-23);
- воркер: переходная прослойка — берётся первый файл набора (склейка всех — Task 3, R-22).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- T2.1: после чтения набора каталог ответа листается заново (Local и SMB); если снимок (имена и
  размеры) отличается от настоявшегося — None, набор настаивается заново.
- T2.2: SmbHotFolder — ENOTDIR из листинга Output/<job_id> (на месте каталога файл) — None,
  как у LocalHotFolder.
- T2.4: тест SMB — ENOENT / SHARING_VIOLATION / ENOTDIR во время настаивания сбрасывают
  накопленное состояние settle (pending_jobs() == 0).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- Исправлена критическая ошибка: NullNormalizer считал ответ ошибкой при `error is not None`,
  а у реального успеха ИС error = {"code": "", "message": ""} — aep_3.json и aep_6.json давали
  UNKNOWN_ERROR. Успех файла теперь — error.code == "" (отсутствующий/null/не-объектный error
  и error без кода ошибкой не считаются); регрессия — на реальных фикстурах (маркер fixtures).
- src/gateway/merge.py: merge_answers(job_id, files) -> MergedResponse
  {"job_id", "files": [{"name", "response", "parse_error"}]} по возрастанию имени; не JSON —
  parse_error файла; dump_merged — UTF-8 (с ASCII-экранированием при одиночном суррогате).
- Воркер: опрос отдаёт готовый набор → склейка → raw/{job_id}.json (application/json) до
  нормализации → Normalizer.normalize(склейка). Перечитывание «не JSON» убрано (R-24).
- NullNormalizer: все файлы ошибочны — код/сообщение первого по имени (parse_error и
  не-объект — INVALID_FORMAT, код вне контракта — UNKNOWN_ERROR с исходным кодом); есть
  успешный — NOT_IMPLEMENTED; ошибочные файлы — строки `<имя>: <код ИС>: <сообщение>`.
- NormalizeOutcome.warnings (R-25) → metrics.warnings конверта и файла результата; пустые
  опускаются; не больше 100 строк по 1000 символов, лишние — сводной строкой (R-30).
- Тесты: test_merge.py, test_normalizer.py переписан под склейку, test_worker.py — склейка в
  raw, ошибка одного файла, parse_error в warnings, все ошибочны, реальные aep_3+aep_3_ofr и
  aep_6, settle и таймаут ненастоявшегося набора; SUCCESS_ANSWER — в форме реального успеха.
  Интеграционный test_roundtrip.py минимально переведён на формат склейки (прогон — Task 5).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- spec «Ответ из одного файла»: набор передаётся не позднее SCAN_SETTLE_SECONDS +
  SCAN_POLL_SECONDS после первого опроса, увидевшего окончательный набор (R-29).
- design D-5: сигнатура NormalizeOutcome с warnings; воркер дописывает их в metrics.warnings.
- Decisions made without discussion: №67 R-29, №68 решения исполнителя Task 2, №69–72
  R-30..R-33 (предел warnings, склейка в src/gateway/merge.py, крайние случаи NullNormalizer,
  слияние warnings с metrics), №73 T2.1/T2.2/T2.4, №74 R-34 (адаптация test_roundtrip.py).
- Отметки: Task 3 в плане, 7.2 в tasks.md.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Для `<uuid>_<name>.<ext>` эмулятор собирает все `<name>.json` и `<name>_*.json` из SCAN_FIXTURES_DIR (не префикс
`<name>*`) и кладёт их в `Output/<uuid>/` как `<uuid>.json` / `<uuid>_<suffix>.json` с `file_uid = <uuid>`. Набор
пишется в `<root>/<SCAN_TMP_DIR>/.<uuid>.answer` и переименовывается в каталог ответа целиком (R-26). Ответ «нет пары» —
`data_ocr` объектом с пустыми полями (R-28). Решения исполнителя — design №75 (R-35).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Сквозные сценарии с реальными NATS и MinIO: aep_3 (два файла, warnings про _ofr.json, склеенный raw),
aep_6 (NOT_IMPLEMENTED без metrics), aep_5.PDF (INVALID_DOCUMENT, повтор запроса), unknown (заглушка эмулятора).
settle 0.2 с, SCAN_TMP_DIR передаётся эмулятору. Исправлено устаревшее ожидание data_ocr в сценарии unknown (R-28).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Раскладка Output/<job_id>/*.json (склеиваются все файлы), SCAN_TMP_DIR, SCAN_SETTLE_SECONDS, структура raw/{job_id}.json,
успех = error.code == "", ошибочные файлы в metrics.warnings, правило набора эмулятора. D-5: не строковый error.code
тоже ошибка (UNKNOWN_ERROR, R-32). Решения R-36 (design #76). Тест TestAnswerSet на реальных фикстурах помечен fixtures.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Профиль services (arm-core, scan-gateway из этого worktree, scan-emulator): aep_3.pdf → error NOT_IMPLEMENTED
с metrics.warnings про <job_id>_ofr.json, aep_6.pdf → error NOT_IMPLEMENTED без metrics, aep_5.PDF →
error INVALID_DOCUMENT; raw/<job_id>.json — склейка набора, тела совпадают с фикстурами; Tmp пуст, .part нет.
Compose правки не требует (умолчания Tmp/settle 2 с). Устаревшие места ../docs/dev/README.md — точными
заменами в open-questions-proposal.md, пункт 20. Уборка — rm -sfv своих сервисов и том hotfolder вместо
down -v (R-27). Отметки: tasks 7.5, план Task 6.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- 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>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Author
Owner

Доработка по ответам владельца ИС (04.10.2026), +18 коммитов в этой ветке

Решения человека: OQ-78 — ИС кладёт все распознанные документы одновременно в каталог <job_id>, шлюз читает все json и склеивает; OQ-79 — временный файл записи уходит из Input в служебный каталог; OQ-76/77 — добавлены реальные образцы aep_3 (две json: успех баланс+ОФР и INVALID_DOCUMENT по ОФР) и aep_6 (успех), PR в fixtures: RSHB/arm-ka-fixtures#1.

Что изменено:

  • Горячие папки: poll возвращает все *.json каталога <job_id>; готовность по стабилизации набора (SCAN_SETTLE_SECONDS=2); временный файл — Tmp/ (SCAN_TMP_DIR), в Input попадает только итоговый файл.
  • Склейка: raw/{job_id}.json = {"job_id", "files":[{name, response, parse_error}]}; ошибка одного файла не отменяет остальные (metrics.warnings); контракт v1.0 не менялся.
  • Исправлена ошибка нормализатора-заглушки: успех у реальной ИС — error = {"code": "", "message": ""}, а не null; прежняя проверка «error не null» принимала настоящий успех за ошибку. Регрессионные тесты на реальных aep_3, aep_6, aep_5, aep_3_ofr, aep_8_form4 (маркер fixtures).
  • Эмулятор отдаёт набор файлов на задание (<name>.json и <name>_*.json, атомарно целиком); aep_3 не забирает aep_30*.

Проверка: make check-full exit 0 (745 unit + 10 integration, docker build). Реальный compose (профиль services): aep_3.pdf → error NOT_IMPLEMENTED + warning про <job_id>_ofr.json; aep_6.pdf → NOT_IMPLEMENTED без warnings; aep_5.PDF → INVALID_DOCUMENT; raw/ в MinIO побайтно совпадает с фикстурами (md5), Tmp пуст, .part во Input нет.

Новые решения без обсуждения (design №54–80): структура склейки, готовность по settle (отсчёт от первого опроса, итог не позднее SETTLE + POLL), metrics.warnings до 100 строк, ответ эмулятора «нет пары» как реальные ответы-ошибки, атомарная публикация набора эмулятором, при сбоях чтения файла ответа код итога остаётся TIMEOUT с числом ошибок в тексте.

Deferred (в follow-up issue): T1.12, T2.3 (на реальной SMB-шаре сверять smbclient.stat().st_size), T2.5, T3.2, T3.4, T3.5, T4.2–T4.5, T5.2, T5.4, T6.5, T6.7, O-3, O-7.

## Доработка по ответам владельца ИС (04.10.2026), +18 коммитов в этой ветке Решения человека: **OQ-78** — ИС кладёт все распознанные документы одновременно в каталог `<job_id>`, шлюз читает все json и склеивает; **OQ-79** — временный файл записи уходит из `Input` в служебный каталог; **OQ-76/77** — добавлены реальные образцы `aep_3` (две json: успех баланс+ОФР и `INVALID_DOCUMENT` по ОФР) и `aep_6` (успех), PR в fixtures: RSHB/arm-ka-fixtures#1. Что изменено: - Горячие папки: `poll` возвращает все `*.json` каталога `<job_id>`; готовность по стабилизации набора (`SCAN_SETTLE_SECONDS=2`); временный файл — `Tmp/` (`SCAN_TMP_DIR`), в `Input` попадает только итоговый файл. - Склейка: `raw/{job_id}.json` = `{"job_id", "files":[{name, response, parse_error}]}`; ошибка одного файла не отменяет остальные (`metrics.warnings`); контракт v1.0 не менялся. - **Исправлена ошибка нормализатора-заглушки:** успех у реальной ИС — `error = {"code": "", "message": ""}`, а не `null`; прежняя проверка «`error` не null» принимала настоящий успех за ошибку. Регрессионные тесты на реальных `aep_3`, `aep_6`, `aep_5`, `aep_3_ofr`, `aep_8_form4` (маркер `fixtures`). - Эмулятор отдаёт набор файлов на задание (`<name>.json` и `<name>_*.json`, атомарно целиком); `aep_3` не забирает `aep_30*`. Проверка: `make check-full` exit 0 (745 unit + 10 integration, `docker build`). Реальный compose (профиль `services`): `aep_3.pdf` → `error NOT_IMPLEMENTED` + warning про `<job_id>_ofr.json`; `aep_6.pdf` → `NOT_IMPLEMENTED` без warnings; `aep_5.PDF` → `INVALID_DOCUMENT`; `raw/` в MinIO побайтно совпадает с фикстурами (md5), `Tmp` пуст, `.part` во `Input` нет. Новые решения без обсуждения (design №54–80): структура склейки, готовность по settle (отсчёт от первого опроса, итог не позднее `SETTLE + POLL`), `metrics.warnings` до 100 строк, ответ эмулятора «нет пары» как реальные ответы-ошибки, атомарная публикация набора эмулятором, при сбоях чтения файла ответа код итога остаётся `TIMEOUT` с числом ошибок в тексте. Deferred (в follow-up issue): T1.12, T2.3 (на реальной SMB-шаре сверять `smbclient.stat().st_size`), T2.5, T3.2, T3.4, T3.5, T4.2–T4.5, T5.2, T5.4, T6.5, T6.7, O-3, O-7.
aep merged commit 8bb10949ec into main 2026-10-04 18:41:20 +03:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: RSHB/arm-scan-gateway#3
No description provided.