Срез 0 [7.4]: README и CLAUDE.md — набор ответа, Tmp, settle, склеенный raw; уточнение D-5, отметки 7.4
Раскладка 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>
This commit is contained in:
parent
68389d5cc2
commit
877541db10
29
CLAUDE.md
29
CLAUDE.md
@ -3,8 +3,10 @@
|
||||
Общий контекст системы — `../CLAUDE.md` и `../docs/`. Этот файл — только про этот сервис.
|
||||
|
||||
## Что это
|
||||
Адаптер внешней ИС Сканирование (Content AI) для PDF/PNG/JPG: переименовать файл `UUID_имя`, положить в «Входящие»,
|
||||
забрать `Исходящие/UUID/UUID.json`, сохранить сырой ответ в S3 `raw/{job_id}.json` (в БД не регистрируется),
|
||||
Адаптер внешней ИС Сканирование (Content AI) для PDF/PNG/JPG: переименовать файл `UUID_имя`, положить в «Входящие»
|
||||
(через `SCAN_TMP_DIR`, по умолчанию `Tmp`: `.part` пишется туда, на той же ФС/шаре, и переименовывается), забрать **все**
|
||||
`Исходящие/UUID/*.json` (ИС кладёт по одному json на файл ответа), склеить и сохранить сырой ответ в S3
|
||||
`raw/{job_id}.json` (в БД не регистрируется),
|
||||
нормализовать в `../docs/contracts/recognition-result.schema.json` (`statementDocument`), опубликовать
|
||||
`recognition.result`. Таймаут → ошибка `TIMEOUT` по `min(reply_deadline, старт + SCAN_TIMEOUT_SECONDS=300)`, старт —
|
||||
запись файла во «Входящие». **Нет доступа к БД.**
|
||||
@ -12,9 +14,18 @@
|
||||
## Правила
|
||||
- Нормализация по фактическим ответам `../fixtures/ocr/aep_*.json`, а не по ТЗ (OQ-13, OQ-14): confidence → 0–100,
|
||||
поля организации/даты — по тому, что реально приходит.
|
||||
- Один PDF может содержать ББ и ОФР → два документа в результате.
|
||||
- Dev-режим: эмулятор ИС Сканирование, возвращающий `aep_N.json` по имени файла: `make emulator` (то же, что
|
||||
`python -m src.scan_emulator` из `api/`); в контейнере — тот же образ с `command: python -m src.scan_emulator`.
|
||||
- Набор ответа готов по settle: есть `*.json`, а имена и размеры не менялись `SCAN_SETTLE_SECONDS` (по умолчанию 2;
|
||||
отсчёт от первого опроса, увидевшего окончательный набор). Не ждать «один файл `<UUID>.json`».
|
||||
- Склеенный `raw/{job_id}.json`: `{"job_id", "files": [{"name", "response", "parse_error"}, ...]}`, все `*.json`
|
||||
каталога по возрастанию имени; нормализатор получает ровно эту структуру.
|
||||
- Успех файла ответа — `error.code == ""` (`error` при этом не `null`!), ошибка — непустой `error.code` или
|
||||
`parse_error`. Проверка `error is None` для реального ответа неверна. Ошибочные файлы — в `metrics.warnings`
|
||||
(`<имя>: <код>: <сообщение>`); все файлы ошибочны → код первого, есть успешный → `NOT_IMPLEMENTED` (Срез 0).
|
||||
- В одном json может быть несколько таблиц разных форм (баланс, ОФР); тип документа задаёт `label` таблицы. Таблицы не
|
||||
интерпретируются в Срезе 0 — разбор в Срезе 3.
|
||||
- Dev-режим: эмулятор ИС Сканирование: по входящему `<uuid>_<name>.<ext>` кладёт в `Output/<uuid>/` набор фикстур
|
||||
`<name>.json` + `<name>_*.json` как `<uuid>.json` / `<uuid>_<суффикс>.json` (атомарно, через `Tmp`): `make emulator`
|
||||
(то же, что `python -m src.scan_emulator` из `api/`); в контейнере — тот же образ с `command: python -m src.scan_emulator`.
|
||||
- Описание сервиса, протокол горячих папок, env, запуск и коды ошибок — `README.md`; требования и решения Среза 0 —
|
||||
`openspec/changes/slice-0-skeleton/` (`design.md`).
|
||||
|
||||
@ -36,13 +47,13 @@ SMB-сессия, S3, NATS, воркер; `GET /healthcheck`; gzip/proxy middlew
|
||||
`raisa_fastapi_protected_api`; шаблонные `core/nats_callback.py` и `README.md` заменены своими.
|
||||
|
||||
**Добавлено:**
|
||||
- `api/src/gateway/` — горячие папки: протокол `HotFolder` (`hotfolder.py`: `LocalHotFolder`, `inbox_name`), `smb.py`
|
||||
(`SmbHotFolder`, импортируется лениво), фабрика `create_hotfolder`; референс поллинга — `../reference-old-arm/api/src/core/PDFWorker`.
|
||||
- `api/src/gateway/` — горячие папки: протокол `HotFolder` (`hotfolder.py`: `LocalHotFolder`, `SettleTracker`, `inbox_name`),
|
||||
`smb.py` (`SmbHotFolder`, импортируется лениво), фабрика `create_hotfolder`; `merge.py` — склейка файлов ответа в `raw/`; референс поллинга — `../reference-old-arm/api/src/core/PDFWorker`.
|
||||
- `api/src/core/` — `worker.py` (задание: проверка → ack → S3 → горячая папка → опрос → `raw/` → нормализатор →
|
||||
результат; ошибки по таблице D-7), `nats.py` (core NATS без JetStream, queue group `scan-gateway`, публикация с
|
||||
повторами), `clients.py` (`S3Storage`), `health.py` (`nats`/`hotfolder` в `/healthcheck`).
|
||||
- `api/src/normalizer/` — `Normalizer`, `NullNormalizer` (Срез 0: ошибка ИС → её код, успех → `NOT_IMPLEMENTED`);
|
||||
настоящая нормализация — Срез 3.
|
||||
- `api/src/normalizer/` — `Normalizer`, `NullNormalizer` (Срез 0: разбор по файлам склейки — все ошибочны → код
|
||||
первого, есть успешный → `NOT_IMPLEMENTED`, ошибочные файлы → `metrics.warnings`); настоящая нормализация — Срез 3.
|
||||
- `api/src/contract/` + `api/contracts/` — pydantic-модели и байт-в-байт копии схем `../docs/contracts`; идентичность
|
||||
проверяет `api/tests/unit/test_contract.py` (входит в `make check`).
|
||||
- `api/src/scan_emulator/` — эмулятор ИС Сканирование (`SCAN_FIXTURES_DIR`, `SCAN_EMULATOR_DELAY`,
|
||||
|
||||
118
README.md
118
README.md
@ -23,9 +23,10 @@ recognition.image.request (NATS, queue group scan-gateway)
|
||||
2. ack: recognition.result, status=processing (до чтения S3 и до ожидания слота)
|
||||
3. слот (SCAN_MAX_INFLIGHT одновременных заданий)
|
||||
4. файл из S3 (s3.bucket/s3.key)
|
||||
5. горячая папка: <Input>/<job_id>_<original_name>
|
||||
6. опрос <Output>/<job_id>/<job_id>.json каждые SCAN_POLL_SECONDS, до дедлайна
|
||||
7. сырой ответ ──► S3 raw/{job_id}.json (как пришёл, до нормализации)
|
||||
5. горячая папка: <Input>/<job_id>_<original_name> (через <Tmp>/.<имя>.part и rename)
|
||||
6. опрос <Output>/<job_id>/ каждые SCAN_POLL_SECONDS до дедлайна; набор *.json готов, когда
|
||||
не менялся SCAN_SETTLE_SECONDS
|
||||
7. склейка всех *.json каталога ──► S3 raw/{job_id}.json (до нормализации)
|
||||
8. нормализатор ──► (done/needs_check) S3 results/{file_id}/{job_id}/result.json
|
||||
9. recognition.result: итоговый конверт (done | needs_check | error), в нём raw_s3_key
|
||||
```
|
||||
@ -51,18 +52,52 @@ ack (с `attempt` повторного запроса); файл в ИС вто
|
||||
завершающие точки и пробелы отбрасываются; кириллица и пробелы сохраняются; длинное имя укорачивается так, чтобы
|
||||
временный `.<имя>.part` умещался в 240 байт UTF-8 (расширение сохраняется). Если имени не осталось —
|
||||
`RECOGNITION_FAILED`.
|
||||
- Запись атомарна: сначала `.<имя>.part` в том же каталоге, затем переименование. Каталог `Input` создаётся при
|
||||
необходимости (без родителей), корень горячей папки сам не создаётся: нет корня — `RECOGNITION_FAILED`.
|
||||
- Ответ ИС: `<SCAN_OUTPUT_DIR>/<job_id>/<job_id>.json` (читается только этот файл). `job_id` проверяется на
|
||||
безопасность пути. Ответ, который ещё пишется (не JSON), перечитывается на следующем опросе.
|
||||
- Таймаут ожидания: `min(reply_deadline, момент записи файла + SCAN_TIMEOUT_SECONDS)`; опрос каждые `SCAN_POLL_SECONDS`.
|
||||
- Запись атомарна: сначала временный `.<имя>.part` в служебном каталоге `<SCAN_TMP_DIR>` (по умолчанию `Tmp`), затем
|
||||
переименование в `Input`. `Tmp` лежит рядом с `Input` и `Output`, на той же ФС/шаре, чтобы `rename` оставался
|
||||
атомарным, а ИС не забрала недописанный файл из `Input`. Каталоги `Input` и `Tmp` создаются при необходимости (без
|
||||
родителей), корень горячей папки сам не создаётся: нет корня — `RECOGNITION_FAILED`.
|
||||
- Ответ ИС — каталог `<SCAN_OUTPUT_DIR>/<job_id>/`: ИС кладёт туда **все** распознанные документы задания одновременно,
|
||||
по одному `*.json` на файл (`Output/<job_id>/*.json`). Шлюз читает **все** `*.json` каталога и склеивает их в один
|
||||
ответ; прочие файлы каталога игнорируются. `job_id` проверяется на безопасность пути. В одном json может быть
|
||||
несколько таблиц разных форм (баланс, ОФР); таблицы в Срезе 0 не интерпретируются (Срез 3).
|
||||
- Готовность набора — по стабилизации (settle): в каталоге есть хотя бы один `*.json`, а имена и размеры `*.json` не
|
||||
менялись `SCAN_SETTLE_SECONDS` (по умолчанию 2 с). Отсчёт идёт от первого опроса, который увидел окончательный
|
||||
набор (а не от времени изменения файлов), поэтому ответ приходит не раньше чем через `SCAN_SETTLE_SECONDS` после
|
||||
такого опроса; `0` — готов любой непустой набор. Появление или изменение файла сбрасывает отсчёт; набор, который так
|
||||
и не стабилизировался до дедлайна, даёт `TIMEOUT`.
|
||||
- Таймаут ожидания: `min(reply_deadline, момент записи файла + SCAN_TIMEOUT_SECONDS)`; опрос каждые `SCAN_POLL_SECONDS`
|
||||
(ожидание settle входит в этот срок).
|
||||
Ошибка чтения «Исходящих» не прерывает ожидание. Если срок вышел, пока задание ждало слот, файл в ИС не отправляется.
|
||||
- Повторной отправки в ИС нет (OQ-15): после `TIMEOUT` входящий файл остаётся в каталоге, поздний ответ ИС теряется
|
||||
(сырой файл остаётся на шаре).
|
||||
- Эмулятор ИС в `local`-режиме: для файла `<uuid>_<имя>` ждёт `SCAN_EMULATOR_DELAY`, пишет ответ из фикстуры
|
||||
(`<имя без расширения>.json`, затем первый по алфавиту `<имя без расширения>*.json`; `file_uid` подставляется), потом
|
||||
переименовывает входящий файл в `….done`. Нет фикстуры — ответ-ошибка `INVALID_DOCUMENT`. Dot-файлы и `.done`
|
||||
игнорируются.
|
||||
- Эмулятор ИС в `local`-режиме: для файла `<uuid>_<имя>` ждёт `SCAN_EMULATOR_DELAY` и кладёт в `Output/<uuid>/` весь
|
||||
набор фикстур: `<имя>.json` -> `<uuid>.json` и `<имя>_<суффикс>.json` -> `<uuid>_<суффикс>.json` (`<имя>` — имя
|
||||
входящего файла без расширения; именно `<имя>_*`, а не префикс `<имя>*`: `aep_3` не забирает `aep_30*`; в каждом
|
||||
файле `file_uid` = `<uuid>`). Набор собирается во временном каталоге `Tmp` и появляется в `Output/<uuid>/`
|
||||
атомарно целиком (rename каталога). Затем входящий файл переименовывается в `….done`. Нет фикстуры — один
|
||||
ответ-ошибка `INVALID_DOCUMENT`. Dot-файлы и `.done` игнорируются.
|
||||
|
||||
## Склеенный ответ ИС (`raw/{job_id}.json`)
|
||||
|
||||
В S3 до нормализации сохраняется **склейка** всех `*.json` каталога ответа (контракт v1.0 не меняется: `raw_s3_key` —
|
||||
по-прежнему одна строка `raw/{job_id}.json`):
|
||||
|
||||
```json
|
||||
{
|
||||
"job_id": "<job_id>",
|
||||
"files": [
|
||||
{"name": "<job_id>.json", "response": {"file_uid": "...", "error": {"code": "", "message": ""}, "data_ocr": {}}, "parse_error": null},
|
||||
{"name": "<job_id>_ofr.json", "response": {"file_uid": "...", "error": {"code": "INVALID_DOCUMENT", "message": "..."}}, "parse_error": null}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
`files` — все `*.json` каталога по возрастанию имени. Файл, который не разобрался как JSON, не роняет задание:
|
||||
`response = null`, `parse_error` — текст ошибки. Нормализатору передаётся ровно этот объект.
|
||||
|
||||
Формат файла ответа Content AI: `{"file_uid", "error": {"code", "message"}, "data_ocr": {...}}`. **Успех — это
|
||||
`error.code == ""`** (`error` при этом не `null`: `{"code": "", "message": ""}`), поэтому проверять `error != null`
|
||||
нельзя. Ошибка — непустой `error.code` (`INVALID_DOCUMENT`, ...).
|
||||
|
||||
## Переменные окружения
|
||||
|
||||
@ -89,7 +124,9 @@ ack (с `attempt` повторного запроса); файл в ИС вто
|
||||
| `SCAN_HOTFOLDER_PATH` | пусто | корень горячей папки в режиме `local` (обязателен, каталог должен существовать) |
|
||||
| `SCAN_INPUT_DIR` | `Input` | «Входящие» относительно корня |
|
||||
| `SCAN_OUTPUT_DIR` | `Output` | «Исходящие» относительно корня |
|
||||
| `SCAN_TMP_DIR` | `Tmp` | служебный каталог относительно корня (на той же ФС/шаре, что `Input`/`Output`): временный `.part` записи входящего файла |
|
||||
| `SCAN_POLL_SECONDS` | `5` | период опроса «Исходящих», секунды |
|
||||
| `SCAN_SETTLE_SECONDS` | `2` | набор `*.json` в `Output/<job_id>/` готов, когда имена и размеры не менялись столько секунд, считая от первого опроса, увидевшего окончательный набор (>= 0; `0` — сразу) |
|
||||
| `SCAN_TIMEOUT_SECONDS` | `300` | потолок ожидания ответа ИС, секунды (дополнительно к `reply_deadline`) |
|
||||
| `SCAN_MAX_INFLIGHT` | `4` | одновременно обрабатываемых заданий |
|
||||
| `SERVICE_NAME` | `scan-gateway` | `service.name` в конверте |
|
||||
@ -105,8 +142,8 @@ MinIO и NATS); те же значения подхватываются из `EN
|
||||
| Переменная | По умолчанию | Назначение |
|
||||
|---|---|---|
|
||||
| `SCAN_HOTFOLDER_PATH` | — (обязательна) | корень горячей папки; должен существовать, иначе код выхода 2 |
|
||||
| `SCAN_INPUT_DIR`, `SCAN_OUTPUT_DIR` | `Input`, `Output` | подкаталоги корня (как у шлюза) |
|
||||
| `SCAN_FIXTURES_DIR` | `../../fixtures/ocr` | каталог эталонных ответов ИС (`aep_N.json`); путь от `api/` |
|
||||
| `SCAN_INPUT_DIR`, `SCAN_OUTPUT_DIR`, `SCAN_TMP_DIR` | `Input`, `Output`, `Tmp` | подкаталоги корня (как у шлюза); в `Tmp` эмулятор собирает набор ответа перед rename в `Output/<uuid>/` |
|
||||
| `SCAN_FIXTURES_DIR` | `../../fixtures/ocr` | каталог эталонных ответов ИС (`aep_N.json`, `aep_N_<суффикс>.json`); путь от `api/` |
|
||||
| `SCAN_EMULATOR_DELAY` | `2` | задержка перед выдачей ответа, секунды (>= 0) |
|
||||
| `SCAN_EMULATOR_POLL_SECONDS` | `1` | период просмотра «Входящих», секунды (> 0) |
|
||||
|
||||
@ -121,10 +158,14 @@ MinIO и NATS); те же значения подхватываются из `EN
|
||||
| `FILE_READ_ERROR` | объект S3 не прочитан; `s3.bucket` не совпадает с бакетом сервиса | да |
|
||||
| `RECOGNITION_FAILED` | не записан файл во «Входящие» (горячая папка недоступна); недопустимое `original_name`; «Исходящие» не читались ни разу до дедлайна | да |
|
||||
| `TIMEOUT` | ИС не ответила до `min(reply_deadline, старт + SCAN_TIMEOUT_SECONDS)`; `reply_deadline` истёк до отправки файла | да |
|
||||
| `INVALID_FORMAT` | ответ ИС не разбирается как JSON (сырой ответ сохранён, `raw_s3_key` указан); корень ответа не объект | да |
|
||||
| код ошибки ИС | в ответе ИС есть `error`: код пробрасывается, если он входит в `ErrorCode` (например, `INVALID_DOCUMENT`) | да |
|
||||
| `NOT_IMPLEMENTED` | ответ ИС без ошибки (нормализатор Среза 0 — заглушка) | да |
|
||||
| `UNKNOWN_ERROR` | сырой ответ или результат не записан в S3; нормализатор упал; неизвестный код ИС; непредвиденная ошибка | да |
|
||||
| `INVALID_FORMAT` | все файлы ответа ошибочны, и у первого ошибочного файла не разобрался JSON (сырой ответ сохранён, `raw_s3_key` указан) или корень ответа не объект; склейка нарушена или пуста | да |
|
||||
| код ошибки ИС | **все** файлы ответа ошибочны (`error.code` непустой): код первого ошибочного файла по имени пробрасывается, если он входит в `ErrorCode` (например, `INVALID_DOCUMENT`); каждый ошибочный файл — строка `metrics.warnings` | да |
|
||||
| `NOT_IMPLEMENTED` | есть хотя бы один успешный файл (`error.code == ""`); нормализатор Среза 0 — заглушка. Ошибочные файлы набора — в `metrics.warnings` | да |
|
||||
| `UNKNOWN_ERROR` | сырой ответ или результат не записан в S3; нормализатор упал; неизвестный код ИС (в том числе `error.code` не строка: число, список, `true`); непредвиденная ошибка | да |
|
||||
|
||||
Файл ответа считается ошибочным, если его `error.code` непустой либо файл не разобрался как JSON (`parse_error`).
|
||||
`metrics.warnings` итогового конверта содержит по строке `<имя файла>: <код>: <сообщение>` на каждый ошибочный файл (и при
|
||||
`NOT_IMPLEMENTED`, и когда ошибочны все файлы); если ошибочных файлов нет, поле `metrics` не передаётся.
|
||||
|
||||
## Контракт NATS (кратко)
|
||||
|
||||
@ -134,7 +175,7 @@ MinIO и NATS); те же значения подхватываются из `EN
|
||||
- Запрос: subject `recognition.image.request`, подписка в queue group `scan-gateway`, `Nats-Msg-Id = <job_id>:<attempt>`.
|
||||
- Ответы — subject `recognition.result`: ack `status=processing`, `Nats-Msg-Id = ack:<job_id>:<attempt>`; итог
|
||||
`status = done | needs_check | error`, `Nats-Msg-Id = result:<job_id>:<attempt>`. `service.name = "scan-gateway"`.
|
||||
- Ключи S3: исходник — из запроса (`s3.key`), сырой ответ — `raw/{job_id}.json`, результат —
|
||||
- Ключи S3: исходник — из запроса (`s3.key`), сырой ответ (склейка файлов ИС) — `raw/{job_id}.json`, результат —
|
||||
`results/{file_id}/{job_id}/result.json`. Бакет один.
|
||||
- NATS без JetStream: переподключение и повторная подписка — на `nats-py`, ack и повторы публикации — на уровне
|
||||
приложения (`api/src/core/nats.py`).
|
||||
@ -167,19 +208,19 @@ curl -s localhost:8000/healthcheck
|
||||
|
||||
1. Выполнить «Разработку» выше: сервис и эмулятор работают, `/healthcheck` отдаёт `nats = ok`, `hotfolder = ok`.
|
||||
2. Положить файл в MinIO (консоль http://localhost:9001 или `mc`) в бакет `arm-ka`, например
|
||||
`uploads/<file_id>/<job_id>/aep_5.pdf` (имя файла должно совпадать с фикстурой эмулятора: `aep_5.*` -> `aep_5.json`,
|
||||
`aep_8.*` -> `aep_8_form4.json`).
|
||||
`uploads/<file_id>/<job_id>/aep_3.pdf` (имя файла должно совпадать с фикстурами эмулятора: `aep_3.*` -> набор
|
||||
`aep_3.json` + `aep_3_ofr.json`, `aep_6.*` -> `aep_6.json`, `aep_5.*` -> `aep_5.json`).
|
||||
3. Подписаться на `recognition.result` любым клиентом NATS.
|
||||
4. Опубликовать в `recognition.image.request` тело по образцу `api/contracts/examples/request-image.json`
|
||||
(свои `job_id` и `file_id` — UUID, `original_name`, `s3.key`, `reply_deadline` в будущем, `attempt` = 1) с заголовком
|
||||
`Nats-Msg-Id: <job_id>:1`.
|
||||
5. Ожидать: ack `processing`, через `SCAN_EMULATOR_DELAY` — конверт `error`. Для `aep_5`/`aep_8` это `INVALID_DOCUMENT`
|
||||
с сообщением ИС (обе реальные фикстуры — ответы-ошибки); для неизвестного имени — `INVALID_DOCUMENT` эмулятора.
|
||||
В бакете появится `raw/<job_id>.json`, `raw_s3_key` указан в конверте.
|
||||
6. Путь «успех -> `NOT_IMPLEMENTED`» требует успешной фикстуры: добавьте в `SCAN_FIXTURES_DIR` синтетический
|
||||
`<имя>.json` с `error: null`. `NullNormalizer` содержимое `data_ocr` не смотрит; непустой `data_ocr` в фикстуре
|
||||
`ok_doc.json` из `test_roundtrip.py` — требование тестовой фикстуры (форма ответа ИС), а не поведения
|
||||
`NullNormalizer`.
|
||||
5. Ожидать: ack `processing`, через `SCAN_EMULATOR_DELAY` (+ `SCAN_SETTLE_SECONDS`) — конверт `error`:
|
||||
- `aep_3` — `NOT_IMPLEMENTED` и `metrics.warnings` с одной строкой `<job_id>_ofr.json: INVALID_DOCUMENT: ...`
|
||||
(второй файл набора — ответ-ошибка);
|
||||
- `aep_6` — `NOT_IMPLEMENTED` без `metrics`;
|
||||
- `aep_5` (и `aep_8`) — `INVALID_DOCUMENT` с сообщением ИС (ответы-ошибки);
|
||||
- неизвестное имя — `INVALID_DOCUMENT` эмулятора.
|
||||
В бакете появится `raw/<job_id>.json` (склейка всех файлов набора), `raw_s3_key` указан в конверте.
|
||||
|
||||
### Проверки
|
||||
|
||||
@ -221,19 +262,20 @@ make check-full # check + test-integration + docker-build; перед PR
|
||||
|
||||
## Ограничения Среза 0
|
||||
|
||||
- `NullNormalizer` (`api/src/normalizer/`): ошибка ИС пробрасывается с её кодом, любой успешный ответ даёт
|
||||
`NOT_IMPLEMENTED`. Настоящая нормализация `data_ocr` в `statementDocument` — Срез 3.
|
||||
- Реальные фикстуры `aep_5.json` и `aep_8_form4.json` — ответы-ошибки ИС (`INVALID_DOCUMENT`; `data_ocr` — объект с
|
||||
пустыми полями (`form_name` пусто, `confidence 0.0`) и `table_structure: []`): успешных образцов нет, это открытый
|
||||
вопрос Среза 3.
|
||||
- `NullNormalizer` (`api/src/normalizer/`): если ошибочны все файлы ответа, код первого из них пробрасывается; если
|
||||
есть успешный файл (`error.code == ""`) — `NOT_IMPLEMENTED`; ошибочные файлы — в `metrics.warnings`. Таблицы
|
||||
`data_ocr` (в одном json их может быть несколько, разных форм: баланс, ОФР) не интерпретируются; настоящая
|
||||
нормализация в `statementDocument` — Срез 3.
|
||||
- Реальные фикстуры: `aep_3.json` (баланс и ОФР), `aep_6.json` — успешные ответы, `aep_3_ofr.json`, `aep_5.json`,
|
||||
`aep_8_form4.json` — ответы-ошибки (`INVALID_DOCUMENT`; `data_ocr` — объект с пустыми полями и `table_structure: []`).
|
||||
- Режим `smb` покрыт unit-тестами с подменённым `smbclient`; на реальной шаре ИС Сканирование не проверялся. Фактические
|
||||
имена каталогов и путь шары на стенде неизвестны — они настраиваются, а не зашиты в код.
|
||||
- Повторной отправки в ИС нет (OQ-15); падение шлюза после ack теряет задание до `reply_deadline` ядра.
|
||||
- Зависший вызов SMB (`poll`/`put`, до ~60 с по умолчанию `smbclient`) дедлайном воркера не прерывается: вызов идёт
|
||||
в потоке без собственного таймаута, поэтому `TIMEOUT` может прийти позже дедлайна на это время.
|
||||
- Недописанный ответ ИС: ответ не-JSON перечитывается через `SCAN_POLL_SECONDS` и принимается, когда стал JSON или
|
||||
перестал меняться между опросами; на дедлайне берётся последний прочитанный. Не-JSON ответ, принятый так, даёт
|
||||
`INVALID_FORMAT` (сырой ответ сохраняется в `raw/`).
|
||||
- Эмулятор: повреждённая фикстура (не JSON, не UTF-8 или корень не объект) не останавливает его — на такой файл
|
||||
он отвечает `INVALID_DOCUMENT` с сообщением о повреждённой фикстуре и пишет ошибку в лог.
|
||||
- Недописанный набор ответа ИС: защита — settle (`SCAN_SETTLE_SECONDS`), а не разбор содержимого. Если ИС дописывает
|
||||
файл дольше settle, будет принят неполный набор; файл, не разобравшийся как JSON, после склейки не перечитывается:
|
||||
`response = null`, `parse_error` в `raw/`, ошибка этого файла (`INVALID_FORMAT`) — в `metrics.warnings`.
|
||||
- Эмулятор: повреждённая фикстура (не JSON, не UTF-8 или корень не объект) не останавливает его — на её месте в наборе
|
||||
он кладёт ответ `INVALID_DOCUMENT` с сообщением о повреждённой фикстуре и пишет ошибку в лог.
|
||||
- Контейнер `scan-emulator` в compose отсутствует.
|
||||
|
||||
@ -449,6 +449,7 @@ class TestAnswerSet:
|
||||
assert (root / "Work").is_dir()
|
||||
assert not (root / "Tmp").exists() or list((root / "Tmp").iterdir()) == []
|
||||
|
||||
@pytest.mark.fixtures
|
||||
def test_gateway_poll_waits_for_settle_then_returns_whole_set(self, tmp_path: Path) -> None:
|
||||
"""Сквозной «put -> эмулятор -> poll (settle, ложные часы) -> merge_answers -> NullNormalizer» на `aep_3`."""
|
||||
if not REAL_FIXTURES.is_dir():
|
||||
|
||||
@ -49,7 +49,7 @@
|
||||
|
||||
### Task 5: [R5] Интеграционные тесты и документация
|
||||
|
||||
- [ ] Интеграционный тест (`tests/integration`, маркер `integration`): реальные NATS и MinIO, эмулятор и шлюз в процессе теста: `../fixtures/ocr/aep_3.pdf` в `uploads/…` → ack `processing`, затем конверт `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; вывод в отчёт.
|
||||
- [x] Интеграционный тест (`tests/integration`, маркер `integration`): реальные NATS и MinIO, эмулятор и шлюз в процессе теста: `../fixtures/ocr/aep_3.pdf` в `uploads/…` → ack `processing`, затем конверт `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
|
||||
|
||||
|
||||
@ -56,7 +56,7 @@ NATS без JetStream: сообщение доставляется один ра
|
||||
### D-5. Нормализатор — интерфейс; в Срезе 0 сводит ошибки по файлам и отвечает `NOT_IMPLEMENTED`
|
||||
`Normalizer.normalize(raw: object) -> NormalizeOutcome(status, documents, error, metrics, warnings)` (`raw` — склеенный ответ,
|
||||
разобранная структура D-4; JSON каждого файла разобран при склейке, не-объект на входе -> `INVALID_FORMAT`).
|
||||
`NullNormalizer` относит файл к «ошибочным», если `parse_error` не `null` либо `response.error.code` — непустая строка;
|
||||
`NullNormalizer` относит файл к «ошибочным», если `parse_error` не `null` либо `response.error.code` — непустая строка (код не строка, не `null` и не `""` — число, список, `true` — тоже ошибка с кодом `UNKNOWN_ERROR`, R-32, design #71);
|
||||
**успех файла — `error.code == ""`** (реальный успешный ответ ИС несёт `error = {"code": "", "message": ""}`, поэтому
|
||||
проверка «`error` не `null`» неверна; отсутствующий или `null` `error` ошибкой не считается). Если ошибочны **все**
|
||||
файлы — `status = error`, `error.code` — код первого ошибочного файла по имени (коды Content AI совпадают с перечнем
|
||||
@ -211,3 +211,4 @@ warnings; `aep_5.PDF` → `INVALID_DOCUMENT` с `raw_s3_key`; объект `raw/
|
||||
| 73 | 2026-10-04 | T2.1, T2.2, T2.4 (design #73): поправки по ревью Task 2 — T2.1: после чтения набора каталог ответа листается повторно (Local и SMB), снимок отличается от настоявшегося → `None` (набор настаивается заново); T2.2: `SmbHotFolder` — ENOTDIR из листинга `Output/<job_id>` → `None`; T2.4: тест сброса состояния settle при ENOENT/SHARING_VIOLATION/ENOTDIR | файл, появившийся или пропавший за время чтения, иначе дал бы неполный или устаревший набор; поведение SMB = Local | один лишний листинг на каждый выданный набор |
|
||||
| 74 | 2026-10-04 | R-34 (design #74): интеграционный `tests/integration/test_roundtrip.py` минимально переведён на формат склейки `raw/` и успех `error = {"code": "", "message": ""}` (без новых сценариев — они в 7.4) | иначе он проверял бы старый формат `raw/`; полноценная переработка — задача 7.4 | не прогнан: в поднятом compose нет бакета `arm-ka` (`NoSuchBucket`), проверка — 7.4 после `make up` |
|
||||
| 75 | 2026-10-04 | R-35 (design #75): решения исполнителя Task 4 (7.3): эмулятор читает `SCAN_TMP_DIR` (по умолчанию `Tmp`) через `EmulatorSettings.tmp_dir`, как шлюз, не завися от настроек шлюза; набор собирается в `<root>/<SCAN_TMP_DIR>/.<uuid>.answer` и целиком переименовывается в `Output/<uuid>/`; остаток прерванной сборки удаляется, ответ с тем же uuid, уже лежащий в `Output`, заменяется (rmtree + rename); повреждённая фикстура набора даёт ответ-ошибку `INVALID_DOCUMENT` на своём месте, остальные файлы набора остаются; фикстуры с одним `suffix` (`a.JSON`/`a.json`) — берётся первая по имени; `aep_8.pdf` теперь даёт `<uuid>_form4.json`, а не `<uuid>.json` (прежнее правило `<name>*`) | spec scan-emulator: набор появляется атомарно, rename каталога возможен только в пределах одной ФС, служебная папка корня уже есть у шлюза (`Tmp`) | повторная публикация uuid на миг убирает каталог; потребители, ждавшие `aep_8` как `<uuid>.json`, должны брать набор целиком |
|
||||
| 76 | 2026-10-04 | R-36 (design #76): решения исполнителя Task 5 (7.4): интеграционный `test_roundtrip.py` с `SCAN_SETTLE_SECONDS=0.2` и `SCAN_POLL_SECONDS=0.1` (`.env.test` держит settle = 0 для остальных тестов); эмулятору теста отдаются копии только `aep_3.json`, `aep_3_ofr.json`, `aep_5.json`, `aep_6.json` и синтетический `ok_doc.json` (не весь `fixtures/ocr`); сценарий `aep_5` берёт реальное имя `aep_5.PDF` (расширение в верхнем регистре) и проверяет, что единственный ошибочный файл попадает и в итоговую ошибку, и в `metrics.warnings` (D-5); проверка `raw/` общая для всех сценариев: имена файлов склейки = все `*.json` каталога ответа по возрастанию, тела совпадают с файлами; в сценарии `unknown` устаревшее ожидание `data_ocr = []` заменено на объект с `table_structure: []` (R-28) — расхождение обнаружено первым прогоном (в Task 4 тест не запускался, №74); T3.3: D-5 уточнён — не строковый, не `null`/`""` `error.code` (число, список, `true`) — тоже ошибка файла с `UNKNOWN_ERROR` (R-32); T4.1: сквозной тест `TestAnswerSet` на реальных фикстурах помечен `fixtures` | тест должен проверять то, что есть в коде и спеке, а не прежнюю форму ответа; settle 0.2 с не удлиняет тест заметно; полная копия `fixtures/ocr` подтянула бы лишние наборы | тесты `fixtures` пропускаются без репозитория `../fixtures`; интеграционные тесты делят NATS и queue group `scan-gateway` с другими экземплярами шлюза |
|
||||
|
||||
@ -99,7 +99,7 @@ design №54–66. Контракт v1.0 не меняется; нормализ
|
||||
`aep_3.pdf` → два файла, `aep_6.pdf` → один, `aep_5.PDF` → один с ошибкой, `aep_8.pdf` → `<uuid>_form4.json`, `aep_30*` не
|
||||
попадает в набор `aep_3`, неизвестное имя → `INVALID_DOCUMENT`; сквозной «put → эмулятор → poll (с settle) → склейка» на
|
||||
`aep_3`; `make check` зелёный
|
||||
- [ ] 7.4 Интеграционные тесты и документация: реальные NATS и MinIO — `aep_3.pdf` → `NOT_IMPLEMENTED` с warnings про
|
||||
- [x] 7.4 Интеграционные тесты и документация: реальные NATS и MinIO — `aep_3.pdf` → `NOT_IMPLEMENTED` с warnings про
|
||||
`<job_id>_ofr.json` и двумя файлами в `raw/`, `aep_6.pdf` → `NOT_IMPLEMENTED` без warnings, `aep_5.PDF` → `INVALID_DOCUMENT`;
|
||||
`README.md`, `CLAUDE.md`, `.env.example`, `.env.test` актуализированы (`SCAN_TMP_DIR`, `SCAN_SETTLE_SECONDS`, склеенный raw);
|
||||
проверка: `make check-full` (при поднятом `make up`) — exit 0, вывод в отчёт
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user