From 94ab9e90d19e4b56374fdf32256e64683885cd96 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Fri, 9 Oct 2026 17:23:21 +0400 Subject: [PATCH] =?UTF-8?q?openspec:=20=D0=B0=D1=80=D1=85=D0=B8=D0=B2=20ho?= =?UTF-8?q?tfolder-direct-write=20(#26)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- .../.openspec.yaml | 0 .../design.md | 0 .../proposal.md | 0 .../specs/image-recognition-gateway/spec.md | 0 .../tasks.md | 0 .../specs/image-recognition-gateway/spec.md | 44 ++++++++++--------- 6 files changed, 23 insertions(+), 21 deletions(-) rename openspec/changes/{hotfolder-direct-write => archive/2026-10-09-hotfolder-direct-write}/.openspec.yaml (100%) rename openspec/changes/{hotfolder-direct-write => archive/2026-10-09-hotfolder-direct-write}/design.md (100%) rename openspec/changes/{hotfolder-direct-write => archive/2026-10-09-hotfolder-direct-write}/proposal.md (100%) rename openspec/changes/{hotfolder-direct-write => archive/2026-10-09-hotfolder-direct-write}/specs/image-recognition-gateway/spec.md (100%) rename openspec/changes/{hotfolder-direct-write => archive/2026-10-09-hotfolder-direct-write}/tasks.md (100%) diff --git a/openspec/changes/hotfolder-direct-write/.openspec.yaml b/openspec/changes/archive/2026-10-09-hotfolder-direct-write/.openspec.yaml similarity index 100% rename from openspec/changes/hotfolder-direct-write/.openspec.yaml rename to openspec/changes/archive/2026-10-09-hotfolder-direct-write/.openspec.yaml diff --git a/openspec/changes/hotfolder-direct-write/design.md b/openspec/changes/archive/2026-10-09-hotfolder-direct-write/design.md similarity index 100% rename from openspec/changes/hotfolder-direct-write/design.md rename to openspec/changes/archive/2026-10-09-hotfolder-direct-write/design.md diff --git a/openspec/changes/hotfolder-direct-write/proposal.md b/openspec/changes/archive/2026-10-09-hotfolder-direct-write/proposal.md similarity index 100% rename from openspec/changes/hotfolder-direct-write/proposal.md rename to openspec/changes/archive/2026-10-09-hotfolder-direct-write/proposal.md diff --git a/openspec/changes/hotfolder-direct-write/specs/image-recognition-gateway/spec.md b/openspec/changes/archive/2026-10-09-hotfolder-direct-write/specs/image-recognition-gateway/spec.md similarity index 100% rename from openspec/changes/hotfolder-direct-write/specs/image-recognition-gateway/spec.md rename to openspec/changes/archive/2026-10-09-hotfolder-direct-write/specs/image-recognition-gateway/spec.md diff --git a/openspec/changes/hotfolder-direct-write/tasks.md b/openspec/changes/archive/2026-10-09-hotfolder-direct-write/tasks.md similarity index 100% rename from openspec/changes/hotfolder-direct-write/tasks.md rename to openspec/changes/archive/2026-10-09-hotfolder-direct-write/tasks.md diff --git a/openspec/specs/image-recognition-gateway/spec.md b/openspec/specs/image-recognition-gateway/spec.md index ee09274..860ab27 100644 --- a/openspec/specs/image-recognition-gateway/spec.md +++ b/openspec/specs/image-recognition-gateway/spec.md @@ -33,24 +33,23 @@ SHALL хватать `OPENBAO__MINIO__*`, `OPENBAO__NATS__NATS_URL`, `SCAN_HOTFO ### Requirement: Передача файла в ИС Сканирование через горячие папки После подтверждения сервис SHALL прочитать исходник из S3 (`s3.bucket/s3.key`) и записать его во входящий каталог как `.`, где `` — расширение `original_name` запроса в нижнем регистре; запрос без расширения SHALL -завершаться конвертом `RECOGNITION_FAILED` без записи в горячую папку. Временный файл записи SHALL лежать в служебном -каталоге `<корень>//` (по умолчанию `Tmp`, на той же шаре/ФС, что `Input`; создаётся при записи) и -переименовываться в `Input` атомарно **без замены**: если `/.` уже существует, файл SHALL считаться -доставленным, временный файл MUST NOT его перезаписывать. Во входящем каталоге MUST NOT появляться недописанные и -временные файлы. Затем сервис SHALL опрашивать исходящий каталог `//` каждые -`SCAN_POLL_SECONDS` до `min(reply_deadline, старт + SCAN_TIMEOUT_SECONDS)`. ИС кладёт в этот каталог все распознанные -документы задания — по одному `*.json` на документ, набор появляется целиком и больше не меняется; пустой или -отсутствующий каталог означает ожидание. Ответ SHALL считаться готовым, когда в каталоге есть хотя бы один `*.json`, а -набор имён и размеров `*.json` не менялся `SCAN_SETTLE_SECONDS` (по умолчанию 2); готовый набор SHALL включать **все** -`*.json` каталога по возрастанию имени, остальные файлы игнорируются. +завершаться конвертом `RECOGNITION_FAILED` без записи в горячую папку. Файл SHALL записываться сразу во входящий каталог +открытием на запись с усечением, как в старом АРМ (решение владельца 09.10.2026): временных файлов, служебного каталога +и переименования нет; если `/.` уже существует, он SHALL быть перезаписан содержимым объекта S3. +Затем сервис SHALL опрашивать исходящий каталог `//` каждые `SCAN_POLL_SECONDS` до +`min(reply_deadline, старт + SCAN_TIMEOUT_SECONDS)` (`SCAN_TIMEOUT_SECONDS` по умолчанию 1200). ИС кладёт в этот каталог +все распознанные документы задания — по одному `*.json` на документ, набор появляется целиком и больше не меняется; +пустой или отсутствующий каталог означает ожидание. Ответ SHALL считаться готовым, когда в каталоге есть хотя бы один +`*.json`, а набор имён и размеров `*.json` не менялся `SCAN_SETTLE_SECONDS` (по умолчанию 2); готовый набор SHALL +включать **все** `*.json` каталога по возрастанию имени, остальные файлы игнорируются. #### Scenario: Файл положен во входящий каталог - **WHEN** получен запрос для `aep_5.PDF` с `job_id = 3f1c…` -- **THEN** во входящем каталоге появляется `3f1c….pdf`, байт в байт равный объекту S3; во время записи во входящем каталоге нет временных файлов, а `Tmp/` существует +- **THEN** во входящем каталоге появляется `3f1c….pdf`, байт в байт равный объекту S3; каталог `Tmp/` не создаётся, временных файлов нет #### Scenario: Назначение уже существует -- **WHEN** запрос для `aep_5.pdf` с `job_id = 3f1c…` обрабатывается, а `Input/3f1c….pdf` уже лежит (задание повторено ядром после потери ack в другом процессе) -- **THEN** существующий файл не перезаписан, задание продолжается опросом `Output/3f1c…/`, конверт `RECOGNITION_FAILED` не публикуется +- **WHEN** запрос для `aep_5.pdf` с `job_id = 3f1c…` обрабатывается, а `Input/3f1c….pdf` уже лежит с другим содержимым (задание повторено ядром после потери ack в другом процессе) +- **THEN** `Input/3f1c….pdf` перезаписан и байт в байт равен объекту S3, задание продолжается опросом `Output/3f1c…/`, конверт `RECOGNITION_FAILED` не публикуется #### Scenario: Запрос без расширения - **WHEN** получен запрос с `original_name = "scan"` без точки @@ -68,6 +67,10 @@ SHALL хватать `OPENBAO__MINIO__*`, `OPENBAO__NATS__NATS_URL`, `SCAN_HOTFO - **WHEN** размер файла или число файлов в `Output/3f1c…/` менялось менее `SCAN_SETTLE_SECONDS` назад - **THEN** ответ не считается готовым, опрос продолжается до дедлайна +#### Scenario: Ошибка посреди записи +- **WHEN** запись `Input/3f1c….pdf` прервалась ошибкой ввода-вывода +- **THEN** опубликован конверт `RECOGNITION_FAILED`; недописанный файл удалён только при `SCAN_CLEANUP=true`, при `false` остаётся на месте + ### Requirement: Сырой ответ сохраняется в S3 до нормализации Все файлы ответа ИС SHALL быть склеены в один объект `{"job_id": "", "files": [{"name": "<имя файла>", "response": <разобранный json> | null, "parse_error": "<текст>" | null}, ...]}` (файлы по возрастанию имени) и записаны @@ -186,20 +189,19 @@ SHALL отражать состояние. ### Requirement: Очистка горячей папки под флагом При `SCAN_CLEANUP=true` после того, как готовый набор ответа прочитан и склеен в `raw/{job_id}.json` (независимо от -исхода нормализации), сервис SHALL удалить всё содержимое `//` (json-файлы ответа и любые прочие) и сам каталог ``, входящий файл -`/.`, его вариант `/..done` и временный `/...part`, если они -существуют. При `SCAN_CLEANUP=false` (по умолчанию) сервис MUST NOT удалять ничего, включая осиротевшие временные файлы (`.part`, ставший -целью успешной публикации, снимается самой записью — это завершение «rename без замены», а не очистка; ошибка его снятия -запись не прерывает, решение №26; запись собственного `.part` заново удаляет прежний остаток под тем же именем, решение №32). При -`TIMEOUT`, `FILE_READ_ERROR`, `RECOGNITION_FAILED` записи очистка MUST NOT выполняться. Ошибка удаления SHALL быть +исхода нормализации), сервис SHALL удалить всё содержимое `//` (json-файлы ответа и любые +прочие) и сам каталог ``, входящий файл `/.` и его вариант `/..done`, +если они существуют. При `SCAN_CLEANUP=false` (по умолчанию) сервис MUST NOT удалять ничего, включая недописанный при +ошибке записи входящий файл. При `TIMEOUT`, `FILE_READ_ERROR`, `RECOGNITION_FAILED` записи очистка MUST NOT выполняться +(при ошибке записи и `SCAN_CLEANUP=true` снимается только недописанный входящий файл). Ошибка удаления SHALL быть записана в лог одной строкой на задание и MUST NOT менять конверт, `metrics.warnings` и `/healthcheck`. #### Scenario: Очистка включена - **WHEN** `SCAN_CLEANUP=true`, ответ на `3f1c….pdf` прочитан из `Output/3f1c…/` -- **THEN** после публикации конверта в горячей папке нет `Output/3f1c…/`, `Input/3f1c….pdf`, `Input/3f1c….pdf.done` и `Tmp/.3f1c….pdf.part` +- **THEN** после публикации конверта в горячей папке нет `Output/3f1c…/`, `Input/3f1c….pdf` и `Input/3f1c….pdf.done` #### Scenario: Очистка выключена -- **WHEN** `SCAN_CLEANUP=false`, ответ прочитан, а в `Tmp/` остался осиротевший `.part` +- **WHEN** `SCAN_CLEANUP=false`, ответ прочитан - **THEN** все файлы остаются на месте, сервис ничего не удаляет и не пишет в лог о очистке (решение №27) #### Scenario: Нет прав на удаление