diff --git a/README.md b/README.md index b533d8b..71d1db6 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,8 @@ ack (с `attempt` повторного запроса); файл в ИС вто - Входящий файл: `/_`. `original_name` приходит из запроса и не доверяется: берётся последний компонент пути, символы, недопустимые в Windows/SMB (`: < > " | ? *` и управляющие), заменяются на `_`, - завершающие точки и пробелы отбрасываются; кириллица и пробелы сохраняются. Если имени не осталось — + завершающие точки и пробелы отбрасываются; кириллица и пробелы сохраняются; длинное имя укорачивается так, чтобы + временный `.<имя>.part` умещался в 240 байт UTF-8 (расширение сохраняется). Если имени не осталось — `RECOGNITION_FAILED`. - Запись атомарна: сначала `.<имя>.part` в том же каталоге, затем переименование. Каталог `Input` создаётся при необходимости (без родителей), корень горячей папки сам не создаётся: нет корня — `RECOGNITION_FAILED`. diff --git a/openspec/changes/slice-0-skeleton/design.md b/openspec/changes/slice-0-skeleton/design.md index 9ba4405..1ef0c9e 100644 --- a/openspec/changes/slice-0-skeleton/design.md +++ b/openspec/changes/slice-0-skeleton/design.md @@ -150,3 +150,8 @@ D-7 и таймаут (с укороченными настройками). `Smb | 46 | 2026-10-04 | Задача 5.3 (подробно — 15 решений в отчёте исполнителя): задание, получившее слот после `reply_deadline`, отвечает `TIMEOUT` без записи файла и чтения S3 (ack уже опубликован); ответ ИС, не являющийся JSON, перечитывается через `SCAN_POLL_SECONDS` и принимается, когда стал JSON или перестал меняться (на дедлайне — последний прочитанный; затем `INVALID_FORMAT`); дедупликация в двух структурах (`_active` не вытесняется, `_done` — LRU `DONE_CACHE_SIZE`); `SCHEMA_MISMATCH` не регистрирует `job_id`; `error.message` ≤ 4000 символов, суррогаты заменяются; `raw/` пишется с `application/json` (не-JSON — `application/octet-stream`); нормализатор вызывается через `to_thread`; `stop()` не публикует конверты отменённых заданий (D-2); сбой SMB-сессии виден через `healthy()` (listdir с теми же учётными данными), без флага; проба `hotfolder`: таймаут 2 с, кэш 5 с, одна проба за раз; NATS подключается в фоновой задаче lifespan, ошибка `connect`/`subscribe` -> `nats = down` до перезапуска; порядок остановки: отмена подключения -> `worker.stop()` -> drain NATS -> S3 -> SMB; `/healthcheck` всегда 200 | экономия ИС; недописанный ответ на шаре без атомарного rename; ограничения размера публикации | поздний результат отброшен; неподвижная ошибка NATS требует перезапуска пода; зависший `poll` по SMB (до 60 с) не прерывается дедлайном (T12.2) | | 47 | 2026-10-04 | Задача 5.4: общая обвязка тестов воркера — `api/tests/worker_helpers.py` и `api/tests/unit/conftest.py`; ответ ИС для освобождения одного слота пишет тест (не `process_once`); отсутствие утечки слотов проверяется поведением (повторный `start()`); отмена воркера во время `to_thread(hotfolder.put)` не прерывает запись в потоке (файл уже в ИС, конверта нет, D-2) | эмулятор ответил бы на все четыре файла сразу; образец arm-core | возможна повторная отправка в ИС после рестарта (OQ-15) | | 48 | 2026-10-04 | Задача 6.1: интеграционный тест поднимает настоящий lifespan `create_app` и эмулятор в процессе теста против compose NATS/MinIO (tmp-каталог горячей папки); синтетический успешный ответ `ok_doc.json` строится по форме §11 `05-domain-rules.md`; сценарии 1–2 пропускаются без `../fixtures`, недоступная инфраструктура — падение; тест ждёт подписку шлюза по фоновой задаче `nats-connect`; `ResourceWarning` aiohttp о теле > 1 МиБ отфильтрован в одном модуле (загрузка скана 1,29 МБ) | контейнера `scan-emulator` ещё нет (R-7); fixtures не содержат успешных ответов (R-10) | интеграционный тест может забрать чужие запросы из общей queue group `scan-gateway` на общем NATS (T14.1) | +| 49 | 2026-10-04 | R-21: финальная волна исправлений = ровно список финального ревью (C-1, I-1, T12.1, T12.4, T7.3+T7.5, T2.2, неточности документов, строки в предложении для docs); I-2 (compose-фрагмент не применён) и I-3 (replay с `attempt+1` для `job_id` из LRU) оставлены человеку | второй волны нет; I-2 и I-3 меняют спеку/другой репозиторий | остальные находки — follow-up в `open-questions-proposal.md` | +| 50 | 2026-10-04 | C-1: пакет `src` не создаёт приложение при импорте (`api/src/__init__.py` без импортов; `run_server.py` импортирует `src.app`); эмулятор стартует без `OPENBAO__*` и завершается кодом 2 при отсутствии корня горячей папки | R-11: контейнеру эмулятора не нужны настройки шлюза; обнаружено финальным ревью, тесты работали с полным `.env.test` | любой код, импортировавший `src:app`, должен перейти на `src.app:app` | +| 51 | 2026-10-04 | I-1: `make up` после `up -d --wait postgres nats minio` выполняет `run --rm minio-init` (идемпотентно) | на свежем томе бакет `arm-ka` иначе не создаётся и `make check-full` падает на `NoSuchBucket` | `tabular-parser` — тот же пробел (чужой репозиторий, в предложении) | +| 52 | 2026-10-04 | T12.1: `SCHEMA_MISMATCH` не публикуется для `job_id` (нормализованного через `uuid.UUID`), который в работе или в LRU | тот же `Nats-Msg-Id`, что у настоящего результата, привёл бы к потере настоящего результата при дедупликации ядром | повторный битый запрос того же задания остаётся без ответа (лог warning) | +| 53 | 2026-10-04 | T7.3: `inbox_name` укорачивает основу имени так, чтобы `._<имя>.part` ≤ 240 байт UTF-8 (по code points, расширение ≤ 16 символов сохраняется, `surrogatepass`); `put` подавляет `OSError` при удалении `.part` | ENAMETOOLONG на ext4/NTFS давал ложный `RECOGNITION_FAILED` на допустимое имя; имя для ИС значения не имеет | эмулятор не найдёт фикстуру по усечённому имени (> ~200 байт — нереалистично) | diff --git a/openspec/changes/slice-0-skeleton/open-questions-proposal.md b/openspec/changes/slice-0-skeleton/open-questions-proposal.md index b63e5ca..3080f5e 100644 --- a/openspec/changes/slice-0-skeleton/open-questions-proposal.md +++ b/openspec/changes/slice-0-skeleton/open-questions-proposal.md @@ -118,7 +118,7 @@ python -m src.scan_emulator` завершается кодом 2 с сообще |---|---|---|---| | 1 | `PUBLISH_RETRIES` = число повторов после первой попытки (1+3 = 4 попытки) | R-13 (design #37), T11.1; `api/src/core/config.py` | Согласовать с tabular-parser (там может быть «3 попытки всего»); зафиксировать в `03-architecture.md` одной фразой | | 2 | `03-architecture.md` («Схема `schema_version`»): «неизвестный MAJOR -> `SCHEMA_MISMATCH` и ack»; спека change'а и воркер — без ack | R-16 (design #41), T12.13 | Привести документ в соответствие (ack не публикуется, конверт `error` — если из тела извлекаются `job_id` и `file_id`) либо пересмотреть правило и воркер: ack на отвергнутый запрос защищает ядро от зацикливания повторов доставки, цена — лишнее сообщение и расхождение с R-16 | -| 3 | Реальные фикстуры `fixtures/ocr/aep_5.json`, `aep_8_form4.json` — ответы-ошибки ИС (`error.code = INVALID_DOCUMENT`, поля `data_ocr` пусты) | O-1, R-10 (design #32) | Для Среза 3 (OQ-13/14) нужны образцы **успешных** ответов Content AI; пока успех проверяется синтетическим ответом, `NullNormalizer` на реальных фикстурах пропускает ошибку ИС | +| 3 | Реальные фикстуры `fixtures/ocr/aep_5.json`, `aep_8_form4.json` — ответы-ошибки ИС (`error.code = INVALID_DOCUMENT`, `data_ocr` — объект с пустыми полями и `table_structure: []`) | O-1, R-10 (design #32) | Для Среза 3 (OQ-13/14) нужны образцы **успешных** ответов Content AI; пока успех проверяется синтетическим ответом, `NullNormalizer` на реальных фикстурах пропускает ошибку ИС | | 4 | §11 `05-domain-rules.md` описывает `data_ocr[]` (список), а в фикстурах `data_ocr` — объект с `label/organization/inn/units/date/table_structure` | O-1, T3.2 | Выяснить фактический формат на стенде ИС и поправить §11 или фикстуры; решение нужно до нормализатора Среза 3 | | 5 | Шлюз читает ровно `//.json` (спека); старый код читал все файлы каталога `Output//` | T8.5 | Проверить на стенде ИС в Срезе 3: имя ответа, наличие дополнительных файлов, момент готовности | | 6 | Временный файл `.<имя>.part` лежит в `Input` (атомарная запись `.part` + rename, R-5) — реальная ИС без маски может забрать недописанный файл | R-5, T7/T8 | Вариант: служебный подкаталог рядом (`Input/.tmp` или корень) + `rename` в `Input`; либо уточнить у владельцев ИС маску/поведение |