From 6bb111db389a8584d7dadc0da131d0c29825854e Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sun, 4 Oct 2026 19:44:42 +0400 Subject: [PATCH] =?UTF-8?q?chore(openspec):=20=D0=B0=D1=80=D1=85=D0=B8?= =?UTF-8?q?=D0=B2=20change=20slice-0-skeleton?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Срез 0 реализован и влит (PR #3), change перенесён в openspec/changes/archive, спеки — в openspec/specs. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2 --- .../.openspec.yaml | 0 .../2026-10-04-slice-0-skeleton}/design.md | 0 .../open-questions-proposal.md | 0 .../2026-10-04-slice-0-skeleton}/proposal.md | 0 .../specs/tabular-recognition-service/spec.md | 0 .../2026-10-04-slice-0-skeleton}/tasks.md | 0 .../specs/tabular-recognition-service/spec.md | 105 ++++++++++++++++++ 7 files changed, 105 insertions(+) rename openspec/changes/{slice-0-skeleton => archive/2026-10-04-slice-0-skeleton}/.openspec.yaml (100%) rename openspec/changes/{slice-0-skeleton => archive/2026-10-04-slice-0-skeleton}/design.md (100%) rename openspec/changes/{slice-0-skeleton => archive/2026-10-04-slice-0-skeleton}/open-questions-proposal.md (100%) rename openspec/changes/{slice-0-skeleton => archive/2026-10-04-slice-0-skeleton}/proposal.md (100%) rename openspec/changes/{slice-0-skeleton => archive/2026-10-04-slice-0-skeleton}/specs/tabular-recognition-service/spec.md (100%) rename openspec/changes/{slice-0-skeleton => archive/2026-10-04-slice-0-skeleton}/tasks.md (100%) create mode 100644 openspec/specs/tabular-recognition-service/spec.md diff --git a/openspec/changes/slice-0-skeleton/.openspec.yaml b/openspec/changes/archive/2026-10-04-slice-0-skeleton/.openspec.yaml similarity index 100% rename from openspec/changes/slice-0-skeleton/.openspec.yaml rename to openspec/changes/archive/2026-10-04-slice-0-skeleton/.openspec.yaml diff --git a/openspec/changes/slice-0-skeleton/design.md b/openspec/changes/archive/2026-10-04-slice-0-skeleton/design.md similarity index 100% rename from openspec/changes/slice-0-skeleton/design.md rename to openspec/changes/archive/2026-10-04-slice-0-skeleton/design.md diff --git a/openspec/changes/slice-0-skeleton/open-questions-proposal.md b/openspec/changes/archive/2026-10-04-slice-0-skeleton/open-questions-proposal.md similarity index 100% rename from openspec/changes/slice-0-skeleton/open-questions-proposal.md rename to openspec/changes/archive/2026-10-04-slice-0-skeleton/open-questions-proposal.md diff --git a/openspec/changes/slice-0-skeleton/proposal.md b/openspec/changes/archive/2026-10-04-slice-0-skeleton/proposal.md similarity index 100% rename from openspec/changes/slice-0-skeleton/proposal.md rename to openspec/changes/archive/2026-10-04-slice-0-skeleton/proposal.md diff --git a/openspec/changes/slice-0-skeleton/specs/tabular-recognition-service/spec.md b/openspec/changes/archive/2026-10-04-slice-0-skeleton/specs/tabular-recognition-service/spec.md similarity index 100% rename from openspec/changes/slice-0-skeleton/specs/tabular-recognition-service/spec.md rename to openspec/changes/archive/2026-10-04-slice-0-skeleton/specs/tabular-recognition-service/spec.md diff --git a/openspec/changes/slice-0-skeleton/tasks.md b/openspec/changes/archive/2026-10-04-slice-0-skeleton/tasks.md similarity index 100% rename from openspec/changes/slice-0-skeleton/tasks.md rename to openspec/changes/archive/2026-10-04-slice-0-skeleton/tasks.md diff --git a/openspec/specs/tabular-recognition-service/spec.md b/openspec/specs/tabular-recognition-service/spec.md new file mode 100644 index 0000000..c0957d2 --- /dev/null +++ b/openspec/specs/tabular-recognition-service/spec.md @@ -0,0 +1,105 @@ +# tabular-recognition-service Specification + +## Purpose +Stateless-сервис разбора табличных файлов (XLS/XLSX/XLSB): принимает задания из NATS без JetStream, подтверждает их, +читает исходник из S3, отдаёт результат в S3 и конверт в NATS по контракту v1.0, не имея доступа к БД и фронта. +В Срезе 0 разбор заменён заглушкой. + +## Requirements + +### Requirement: Сервис не зависит от БД и фронта +Репозиторий MUST NOT содержать каталоги `web/`, `alembic/`, `src/db/`, `src/models/`, `src/repositories/`; зависимости +`SQLAlchemy`, `asyncpg`, `alembic`, `smbprotocol`, `raisa_fastapi_protected_api` MUST NOT присутствовать в +`requirements.txt`; настройки `OPENBAO.DATABASE` и `OPENBAO.SMB` MUST NOT требоваться для старта. + +#### Scenario: Старт с минимальным окружением +- **WHEN** сервис запускается с `.env`, содержащим только `OPENBAO__MINIO__*`, `OPENBAO__NATS__NATS_URL`, `DEBUG=true` +- **THEN** `GET /healthcheck` отвечает 200 с полями `uptime`, `version`, `nats` + +### Requirement: Подтверждение запроса до обработки +Сервис SHALL быть подписан на `recognition.tabular.request` в queue group `tabular-parser`. Получив запрос, проходящий +`recognition-request.schema.json`, сервис SHALL до чтения файла из S3 опубликовать в `recognition.result` конверт +`status = processing` с `job_id`, `file_id`, `service {name: "tabular-parser", version}` и заголовком +`Nats-Msg-Id = ack::`. Повторный запрос с `job_id`, который уже обрабатывается или обработан этим +процессом, SHALL получить повторный ack и MUST NOT обрабатываться второй раз. + +#### Scenario: Ack раньше результата +- **WHEN** опубликован запрос из `docs/contracts/examples/request-tabular.json` +- **THEN** первым сообщением сервиса в `recognition.result` является конверт `processing`, и только затем — результат + +#### Scenario: Повторный запрос во время обработки +- **WHEN** тот же запрос доставлен второй раз, пока первый обрабатывается +- **THEN** публикуется второй ack `processing`, файл из S3 повторно не читается, результат публикуется один раз + +### Requirement: Результат по контракту +После подтверждения сервис SHALL прочитать объект `s3.key` из бакета `s3.bucket`, вызвать парсер, записать файл результата +(по `recognition-result.schema.json`) в `results/{file_id}/{job_id}/result.json` и опубликовать конверт результата (по +`recognition-result-message.schema.json`) с заголовком `Nats-Msg-Id = result::`. При неудаче публикации +сервис SHALL повторить её до трёх раз с паузой, затем залогировать ошибку. + +#### Scenario: Запрос с заглушкой парсера (Срез 0) +- **WHEN** запрос подтверждён и объект существует в `uploads/` +- **THEN** в `recognition.result` появляется конверт `status = error`, `error.code = NOT_IMPLEMENTED`, `job_id`/`file_id` + из запроса, `metrics.duration_ms ≥ 0`; файл результата может отсутствовать + +#### Scenario: Повтор публикации +- **WHEN** первая публикация конверта результата завершается ошибкой соединения, вторая — успешно +- **THEN** конверт опубликован один раз с тем же `Nats-Msg-Id`, обработка не повторяется + +### Requirement: Ошибки транспорта превращаются в результат +Сервис SHALL отвечать конвертом `status = error` в случаях: тело не проходит схему или `MAJOR` версии ≠ 1 → +`SCHEMA_MISMATCH`; объект S3 не читается → `FILE_READ_ERROR`; исключение парсера → `UNKNOWN_ERROR` (сообщение без +трассировки). Сервис MUST NOT падать по этим причинам. `SCHEMA_MISMATCH` SHALL публиковаться (без ack), когда из тела +читаемы `job_id` и `file_id` (uuid), а `attempt` — из тела или из заголовка запроса `Nats-Msg-Id = :`, +и этот `job_id` сервису ещё не известен (не в работе и не среди обработанных). Иначе валидный конверт собрать нельзя +либо он ушёл бы под тем же `Nats-Msg-Id`, что и настоящий результат: сервис SHALL записать в лог ошибку +(`error_category=schema_mismatch`, причина) — уровнем error, если идентификаторы не читаемы, уровнем warning, если +`job_id` уже известен, — и MUST NOT публиковать ни ack, ни результат; ядро обнаружит такое задание по `dlq`/`timeout` +(явная запись в контракт — открытый вопрос «невалидный запрос без читаемых `job_id`/`file_id`/`attempt`» в +`open-questions-proposal.md`). FILE_READ_ERROR и UNKNOWN_ERROR публикуются всегда: сервис MUST NOT молчать по ним. + +#### Scenario: Нет объекта в S3 +- **WHEN** `s3.key` запроса указывает на несуществующий объект +- **THEN** публикуется конверт `error.code = FILE_READ_ERROR`, процесс продолжает работу + +#### Scenario: Несовместимая версия +- **WHEN** запрос содержит `schema_version = "2.0"` +- **THEN** публикуется конверт `error.code = SCHEMA_MISMATCH`, ack не публикуется + +#### Scenario: Невалидный запрос без читаемых идентификаторов +- **WHEN** тело запроса не JSON либо в нём не читаются `job_id` или `file_id`, либо `attempt` нет ни в теле, ни в + заголовке `Nats-Msg-Id` +- **THEN** в лог пишется ошибка `error_category=schema_mismatch`, ни ack, ни результат не публикуются, процесс + продолжает работу + +#### Scenario: Невалидный запрос с уже известным `job_id` +- **WHEN** запрос не проходит схему, а его `job_id` уже в работе или среди обработанных +- **THEN** в лог пишется предупреждение `error_category=schema_mismatch`, ни ack, ни результат не публикуются; + обработка или результат задания с этим `job_id` не меняются + +### Requirement: Парсер — заменяемый интерфейс +Разбор файла SHALL выполняться через единый интерфейс «байты + тип файла → документы/метрики/ошибка», не знающий о +NATS и S3; воркер MUST NOT зависеть от конкретной реализации. В Срезе 0 реализация SHALL возвращать `NOT_IMPLEMENTED` +для всех типов; каталог `api/rules/` SHALL существовать с README о назначении словарей. + +#### Scenario: Подмена реализации в тесте +- **WHEN** в тесте воркеру передан парсер, возвращающий один `osvDocument` +- **THEN** файл результата с этим документом записан в S3, конверт имеет `status = done` и `result_s3` на этот файл + +### Requirement: Копия контракта идентична источнику +`api/contracts/*.schema.json` SHALL быть байт в байт равны файлам `../docs/contracts/`; pydantic-модели SHALL валидировать +все `../docs/contracts/examples/*.json` и порождать JSON, проходящий схему. Проверка входит в `make check`. + +#### Scenario: `make check` +- **WHEN** выполняются тесты контракта при доступном `../docs` (или `ARM_KA_DOCS`) +- **THEN** идентичность подтверждена, примеры валидны; при недоступном docs тест идентичности пропущен с предупреждением + +### Requirement: Соединение с NATS по правилам шаблона, без JetStream +При пустых `NATS_SEED`/`NATS_JWT` сервис SHALL подключаться без учётных данных, иначе — с nkeys/JWT. Сервис MUST NOT +требовать stream'ов или consumer'ов JetStream; при разрыве соединения SHALL переподключаться автоматически и восстанавливать +подписку; `/healthcheck` SHALL отражать состояние (`nats = ok | down`; `off` — NATS выключен флагом +`NATS_ENABLED=false`). + +#### Scenario: NATS перезапущен +- **WHEN** контейнер NATS перезапущен во время работы сервиса +- **THEN** в течение 20 с после старта NATS подписка восстановлена, `nats = ok`, новый запрос обрабатывается -- 2.47.2