diff --git a/CLAUDE.md b/CLAUDE.md index b942918..3c32e6d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -19,4 +19,22 @@ Stateless-сервис разбора XLS/XLSX/XLSB: ОСВ по счетам ( - синтетика только для вариантов, которых нет в фикстурах (шапка вариант 2, Вал./Кол., xlsb, без outline level). ## Соглашения шаблона - +Тот же шаблон команды, что у `arm-core` («Шаблон FastAPI», `RSHB/DKB_ARMKA@ba172f8`); общие соглашения — +`../docs/context/03-architecture.md`, «Соглашения шаблона команды». Сейчас репозиторий — полная копия каркаса `arm-core`; +change `slice-0-skeleton` приводит его к сервису **без фронта и без БД**. Все команды — из `api/`. + +**Оставлено от шаблона:** `api/run_server.py` (точка входа платформы, `SERVER__HOST/PORT`), `api/src/app.py` (lifespan, +`GET /healthcheck`, gzip/proxy middleware, обработчики ошибок), `core/config.py` (pydantic-settings, `OPENBAO__MINIO__*`, +`OPENBAO__NATS__*`, `DEBUG`), `core/clients.py` (S3), `core/nats_callback.py`, `core/logger.py`, `core/exception_handlers.py`, +`common/exceptions.py`, `pytest.ini`, `requirements*.txt`, `raisa_config.yml`, `setup/setup.sh` + `pip.conf` (без npm-шага). + +**Вырезано (Срез 0):** `web/` целиком и `setup/.npmrc`; из `app.py` — mount SPA (`SPAStaticFiles`, `os.makedirs("web")`), +CORS, регистрация SMB-сессии, импорт `raisa_fastapi_protected_api`; `db/`, `models/`, `repositories/`, `alembic/`, +`alembic.ini`; из настроек — `OPENBAO.DATABASE`, `OPENBAO.SMB`; из `requirements.txt` — `asyncpg`, `SQLAlchemy`, `alembic`, +`smbprotocol`, `argon2-cffi`, `raisa_fastapi_protected_api`; шаблонный `README.md` заменён описанием сервиса. + +**Добавлено:** `api/src/parser/` (ОСВ §1 и формы §12 — чистые функции над книгой Excel; Срез 2), `api/src/contract/` +(pydantic-модели `recognition-result` v1.0; идентичность с `../docs/contracts` проверяется в CI), `api/rules/*.yaml` +(словари синонимов — в документах это `rules/*.yaml` относительно `api/`), `api/src/core/worker.py` (durable-подписка +`recognition.tabular.request` → S3 `uploads/` → парсер → S3 `results/` → `recognition.result`), `Dockerfile`, +`api/pyproject.toml` (ruff, mypy). Проверки: `pytest`, `ruff check . && ruff format --check .`, `mypy src`. diff --git a/openspec/changes/slice-0-skeleton/.openspec.yaml b/openspec/changes/slice-0-skeleton/.openspec.yaml new file mode 100644 index 0000000..6a87d2b --- /dev/null +++ b/openspec/changes/slice-0-skeleton/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-10-03 diff --git a/openspec/changes/slice-0-skeleton/design.md b/openspec/changes/slice-0-skeleton/design.md new file mode 100644 index 0000000..c1070ce --- /dev/null +++ b/openspec/changes/slice-0-skeleton/design.md @@ -0,0 +1,80 @@ +# Design: Срез 0 — скелет tabular-parser + +## Context + +См. `proposal.md`. Исходное состояние — копия каркаса `arm-core` (`124f63b`): FastAPI с lifespan (SMB, MinIO, NATS), +SPA-mount, Alembic, пустые пакеты `models/repositories/services`. Контракт v1.0 и правила `Nats-Msg-Id` — родительский +change в docs. Сервис должен остаться запускаемым платформой так же, как шаблон (`api/run_server.py`, `SERVER__*`, +`raisa_config.yml`). + +## Goals / Non-Goals + +**Goals:** сервис без БД/фронта; транспорт NATS ↔ S3 проверен интеграционным тестом; парсер — заменяемый интерфейс. +**Non-Goals:** см. proposal; масштабирование (несколько инстансов — queue-семантика pull-consumer'а даёт это бесплатно, но не тестируется). + +## Decisions + +### D-1. Вырезаем, а не переписываем +Удаляются каталоги и зависимости, перечисленные в proposal; `app.py` упрощается до: настройки, lifespan (MinIO, NATS, +запуск воркера), `/healthcheck`, обработчики ошибок, логирование. Остающиеся файлы шаблона не переименовываются — +чтобы diff с `arm-core` и будущие обновления шаблона были читаемы. + +### D-2. Воркер — pull-consumer в lifespan, одно сообщение за раз на инстанцию +`JetStream.pull_subscribe("recognition.tabular.request", durable="tabular-parser")`, цикл `fetch(1, timeout=5)`; +`ack_wait` 10 мин (большие ОСВ), `max_deliver` 3 (OQ-15). Ack — после успешной публикации конверта результата; при +падении до публикации сообщение вернётся (повтор ≤ 3), при третьем провале ядро получит таймаут по `reply_deadline`. +*Альтернатива* — push-consumer с очередью: сложнее контролировать параллелизм и ack. + +### D-3. Парсер — протокол `Parser.parse(data: bytes, file_type) -> ParseOutcome` +`ParseOutcome` = `status`, `documents` (модели контракта), `metrics`, `error`. `NullParser` всегда отдаёт +`error NOT_IMPLEMENTED` с сообщением «разбор XLS/XLSX/XLSB реализуется в Срезе 2». Воркер не знает о реализации; +Срез 2 подменяет фабрику. Так транспорт и парсер тестируются раздельно, парсер — без NATS/S3. + +### D-4. Ключи S3 и имя сервиса +Исходник читается по `request.s3.key` (роль `uploads`), результат пишется в `results/{file_id}/{job_id}/result.json` +того же бакета; `service = {name: "tabular-parser", version: settings.VERSION}`. Клиент S3 — `S3Storage` шаблона, +дополненный `get_bytes`/`put_json`. + +### D-5. Любая ошибка транспорта — результат, не молчание +Таблица: запрос не проходит pydantic → `SCHEMA_MISMATCH` (ack, чтобы не зациклить); `MAJOR` версии запроса ≠ 1 → +`SCHEMA_MISMATCH`; S3 `get` упал → `FILE_READ_ERROR`; исключение парсера → `UNKNOWN_ERROR` с текстом исключения +в `error.message` (без трассировки). Публикуется конверт без `result_s3`; `metrics.duration_ms` заполняется всегда. +Если не удалось даже опубликовать результат — nak с задержкой 5 с. + +### D-6. Соединение с NATS — как у ядра +Пустые `NATS_SEED/NATS_JWT` → без учётных данных (dev); иначе nkeys/JWT шаблона. Stream `RECOGNITION` сервис **не +создаёт** (ответственность ядра / платформы); при отсутствии stream воркер логирует ошибку и повторяет подписку каждые 10 с, +`/healthcheck` отдаёт `nats = down`. Флаг `NATS_ENABLED` (по умолчанию `true`) — чтобы unit-тесты поднимали приложение без NATS. + +### D-7. Тесты в два уровня +Unit (`tests/unit`): модели контракта и примеры docs; обработчик сообщения с фейками `S3Storage`/`Publisher` (протоколы) +— сценарии D-5. Интеграционные (`tests/integration`, маркер `integration`): compose-NATS/MinIO — реальный запрос из +примера `request-tabular.json` → результат в бакете и конверт в `recognition.result`. + +## Risks / Trade-offs + +- [`ack_wait` 10 мин при max_deliver 3 → до 30 мин «висящего» задания при падении инстанции] → ядро ограничивает + ожидание `reply_deadline`; значение `ack_wait` вынесено в настройку `NATS_ACK_WAIT_SECONDS`. +- [`NOT_IMPLEMENTED` в проде, если Срез 2 задержится] → код ошибки честный, ядро показывает его аналитику как «формат пока не поддерживается». +- [Удалённые зависимости понадобятся позже] → возвращаются по одной строке в `requirements.txt`. + +## Migration Plan + +Нет данных. Удаление каталогов — одним коммитом «chore: вырезан фронт и БД из шаблона» до функциональных изменений, +чтобы ревью было читаемым. Откат — revert коммита. + +## Open Questions + +Общие с родительским change (Nexus, версии Python, параметры NATS банка). Специфичных нет. + +## Decisions made without discussion + +| № | Дата | Решение | Обоснование | Цена ошибки | +|---|---|---|---|---| +| 1 | 2026-10-03 | D-1: вырезать фронт/БД/SMB, не переименовывая оставшееся | читаемый diff с arm-core | нет | +| 2 | 2026-10-03 | D-2: pull-consumer, `fetch(1)`, `ack_wait` 10 мин, `max_deliver` 3 | OQ-15, простота контроля параллелизма | смена параметров consumer'а | +| 3 | 2026-10-03 | D-3: парсер-заглушка отвечает `NOT_IMPLEMENTED` (код добавлен в контракт родительским change'ем) | честный статус вместо маскировки под `UNSUPPORTED_FORMAT` | нет | +| 4 | 2026-10-03 | D-4: результат в `results/{file_id}/{job_id}/result.json` того же бакета | схема ключей `03-architecture.md`, один бакет | смена шаблона ключа в одном месте | +| 5 | 2026-10-03 | D-5: транспортные ошибки превращаются в результат с кодом | ядро не должно ждать таймаута, если сервис жив | нет | +| 6 | 2026-10-03 | D-6: stream создаёт ядро, сервис только подписывается | одна точка управления инфраструктурой NATS | добавить `ensure_stream` и в сервис | +| 7 | 2026-10-03 | Каталог словарей — `api/rules/` (относительно рабочего каталога `api/`) | все команды шаблона идут из `api/` | перенос каталога | diff --git a/openspec/changes/slice-0-skeleton/proposal.md b/openspec/changes/slice-0-skeleton/proposal.md new file mode 100644 index 0000000..43b2b48 --- /dev/null +++ b/openspec/changes/slice-0-skeleton/proposal.md @@ -0,0 +1,58 @@ +# Proposal: Срез 0 — скелет tabular-parser + +**Срез:** 0 «Скелет» (`../docs/context/09-delivery-plan.md`). **Репозиторий:** tabular-parser. **Issue:** RSHB/arm-tabular-parser#1. +**Родительский change:** `../docs/openspec/changes/slice-0-skeleton` (контракт v1.0, compose, CI-шаблон). +**Соседние:** `../arm-core/openspec/changes/slice-0-skeleton`, `../scan-gateway/openspec/changes/slice-0-skeleton`. + +## Why + +Репозиторий сейчас — полная копия каркаса `arm-core` с фронтом, Alembic и моделями, то есть противоречит DECISION-4 +(сервис без БД, видит только NATS и S3). Прежде чем в Срезе 2 писать парсер ОСВ, нужен сервис, который уже умеет +принять запрос из NATS, прочитать файл из S3, положить результат и ответить по контракту v1.0 — чтобы парсер стал +чистой функцией «байты → документы», а транспорт был проверен end-to-end вместе с ядром. + +## What Changes + +- **Вырезать из шаблона** (список — `CLAUDE.md`, «Соглашения шаблона»): `web/`, `setup/.npmrc` и npm-шаги + `setup/setup.sh`; из `app.py` — mount SPA, CORS, SMB-сессия, импорт `raisa_fastapi_protected_api`; `db/`, `models/`, + `repositories/`, `alembic/`, `alembic.ini`; настройки `OPENBAO.DATABASE`, `OPENBAO.SMB`; зависимости `asyncpg`, + `SQLAlchemy`, `alembic`, `smbprotocol`, `argon2-cffi`, `raisa_fastapi_protected_api`; шаблонный `README.md`. +- **Инструменты**: `api/pyproject.toml` (ruff, mypy), `requirements-dev.txt` = `-r requirements.txt` + инструменты, + `Dockerfile` (строка убрана из `.gitignore`), `Makefile` (`up / test / lint`), `.env.example`, `.env.test`; + `Settings` читает `ENV_FILE`; `SERVICE_NAME = "tabular-parser"`. +- **Контракт**: вендоренные схемы `api/contracts/` + pydantic-модели `src/contract/` (запрос, конверт результата, файл + результата); тест идентичности с `../docs/contracts`. +- **Воркер** `src/core/worker.py`: durable pull-consumer `tabular-parser` на `recognition.tabular.request` + (`max_deliver` 3 — OQ-15), чтение исходника из S3 `uploads/…`, вызов парсера, запись `results/{file_id}/{job_id}/result.json`, + публикация конверта `recognition.result` с `Nats-Msg-Id = result::`, ack после публикации. +- **Парсер как интерфейс**: `src/parser/` — протокол `Parser` и `NullParser`, возвращающий `status = error`, + `error.code = NOT_IMPLEMENTED` для любого файла (Срез 2 заменяет реализацией §1/§12); `api/rules/README.md` — + место словарей синонимов. +- **Ошибки транспорта** всегда превращаются в результат: запрос не по схеме → `SCHEMA_MISMATCH`; файл не читается из + S3 → `FILE_READ_ERROR`; неожиданное исключение → `UNKNOWN_ERROR`. Ядро не ждёт таймаута, если сервис жив. +- **Healthcheck** `/healthcheck` шаблона дополняется полем `nats` (`ok`/`down`). +- **CI**: `.gitea/workflows/ci.yml` из шаблона docs (ruff, mypy, pytest c NATS/MinIO, идентичность контракта, сборка образа). + +## Capabilities + +### New Capabilities +- `tabular-recognition-service`: stateless-сервис, который принимает задания из NATS, обменивается файлами через S3 и + отвечает по контракту v1.0; в Срезе 0 — с парсером-заглушкой. + +### Modified Capabilities + + +## Impact + +- **Таблицы DBML:** сервис не пишет в БД. Косвенно: `recognition_job` ядра получает `status = error`, + `error_code = NOT_IMPLEMENTED`, `service_name = tabular-parser` из конверта. +- **Правила:** §1.0/§12 — только как контракт будущего парсера (интерфейс `Parser`), §9.4 — статусы задания. +- **Зависимости:** удаляются БД/SMB/фронт; добавляются `ruff`, `mypy`, `pytest*`, `jsonschema` (валидация примеров). +- **Внешние системы:** NATS JetStream (stream `RECOGNITION`), MinIO/S3 (бакет `arm-ka`). + +## Non-goals + +- Разбор ОСВ и форм ББ/ОФР (§1, §12) — Срез 2; property-тесты на `../fixtures/osv/**` — Срез 2. +- Словари `rules/*.yaml` — Срез 2 (здесь только каталог и README). +- Heartbeat `processing` (DBML: «опц. heartbeat») — не реализуется. +- Любая работа с БД, SMB, фронтом. diff --git a/openspec/changes/slice-0-skeleton/specs/tabular-recognition-service/spec.md b/openspec/changes/slice-0-skeleton/specs/tabular-recognition-service/spec.md new file mode 100644 index 0000000..c4da484 --- /dev/null +++ b/openspec/changes/slice-0-skeleton/specs/tabular-recognition-service/spec.md @@ -0,0 +1,72 @@ +# Spec Delta: tabular-recognition-service + +## Purpose + +Stateless-сервис разбора табличных файлов (XLS/XLSX/XLSB): принимает задания из NATS, читает исходник из S3, отдаёт +результат в S3 и конверт в NATS по контракту v1.0, не имея доступа к БД и фронта. В Срезе 0 разбор заменён заглушкой. + +## ADDED 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` durable pull-consumer'ом `tabular-parser`; для каждого запроса, +проходящего `recognition-request.schema.json`, SHALL прочитать объект `s3.key` из бакета `s3.bucket`, вызвать парсер, +записать файл результата (по `recognition-result.schema.json`) в `results/{file_id}/{job_id}/result.json`, затем +опубликовать конверт в `recognition.result` (по `recognition-result-message.schema.json`) с заголовком +`Nats-Msg-Id = result::` и `service.name = "tabular-parser"`, и только после публикации подтвердить запрос. + +#### Scenario: Запрос с заглушкой парсера (Срез 0) +- **WHEN** опубликован запрос из `docs/contracts/examples/request-tabular.json` с существующим объектом в `uploads/` +- **THEN** в `recognition.result` появляется конверт `status = error`, `error.code = NOT_IMPLEMENTED`, + `job_id`/`file_id` из запроса, `metrics.duration_ms ≥ 0`; файл результата может отсутствовать; запрос подтверждён + +#### Scenario: Ack только после результата +- **WHEN** публикация конверта завершается ошибкой +- **THEN** запрос не подтверждается (nak с задержкой), следующая доставка повторяет обработку; дубликат конверта + отбрасывается по `Nats-Msg-Id` + +### Requirement: Ошибки транспорта превращаются в результат +Сервис SHALL отвечать конвертом `status = error` и подтверждать запрос в случаях: тело не проходит схему или `MAJOR` +версии ≠ 1 → `SCHEMA_MISMATCH`; объект S3 не читается → `FILE_READ_ERROR`; исключение парсера → `UNKNOWN_ERROR` +(сообщение без трассировки). Сервис MUST NOT падать и MUST NOT молчать по этим причинам. + +#### Scenario: Нет объекта в S3 +- **WHEN** `s3.key` запроса указывает на несуществующий объект +- **THEN** публикуется конверт `error.code = FILE_READ_ERROR`, запрос подтверждён, процесс продолжает работу + +#### Scenario: Несовместимая версия +- **WHEN** запрос содержит `schema_version = "2.0"` +- **THEN** публикуется конверт `error.code = SCHEMA_MISMATCH`, запрос подтверждён + +### 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, проходящий схему. + +#### Scenario: CI сервиса +- **WHEN** выполняются тесты контракта при доступном `../docs` (или `ARM_KA_DOCS`) +- **THEN** идентичность подтверждена, примеры валидны; при недоступном docs тест идентичности пропущен с предупреждением + +### Requirement: Соединение с NATS по правилам шаблона +При пустых `NATS_SEED`/`NATS_JWT` сервис SHALL подключаться без учётных данных, иначе — с nkeys/JWT. Отсутствие stream +`RECOGNITION` SHALL приводить к повторным попыткам подписки каждые 10 с и `nats = down` в `/healthcheck`, а не к падению. + +#### Scenario: Stream появился позже +- **WHEN** сервис стартовал раньше ядра, создавшего stream +- **THEN** в течение 20 с после создания stream воркер подписан, `nats = ok` diff --git a/openspec/changes/slice-0-skeleton/tasks.md b/openspec/changes/slice-0-skeleton/tasks.md new file mode 100644 index 0000000..f0d4184 --- /dev/null +++ b/openspec/changes/slice-0-skeleton/tasks.md @@ -0,0 +1,54 @@ +# Tasks: Срез 0 — скелет tabular-parser + +Репозиторий — tabular-parser; команды — из `api/`. Зависимости: 3.1 ждёт контракт v1.0 (`docs` 1.1–1.4); +интеграционные тесты 4.x — compose инфраструктуры (`docs` 2.1). Каждая задача ≤ половины дня. + +## 1. Вырезать фронт и БД из шаблона + +- [ ] 1.1 Удалить `web/`, `setup/.npmrc`, npm-шаги из `setup/setup.sh`, `api/alembic/`, `api/alembic.ini`, `api/src/db/`, + `api/src/models/`, `api/src/repositories/`; из `.gitignore` убрать `Dockerfile`; проверка: `git status` показывает + только удаления и `.gitignore`; `python -c "import src"` пока падает ожидаемо (чинится в 1.2) +- [ ] 1.2 `src/app.py`: убрать mount SPA/`os.makedirs("web")`, CORS, SMB-сессию, импорт `raisa_fastapi_protected_api`; + `src/core/config.py`: убрать `DBSettings`, `SMBSettings`, оставить `MINIO`, `NATS`; добавить `ENV_FILE`, + `SERVICE_NAME="tabular-parser"`, `NATS_ENABLED=true`, `NATS_ACK_WAIT_SECONDS=600`, `NATS_MAX_DELIVER=3`; + `requirements.txt` без `asyncpg`, `SQLAlchemy`, `alembic`, `smbprotocol`, `argon2-cffi`, `raisa_fastapi_protected_api`; + проверка: `pip install -r requirements.txt` в чистом venv, `python run_server.py` стартует c `.env.example`, `/healthcheck` 200 +- [ ] 1.3 `README.md` — описание сервиса (назначение, команды, env, запуск против compose), `CLAUDE.md` — актуализировать + раздел «Соглашения шаблона» по факту вырезанного; проверка: все упомянутые файлы существуют + +## 2. Инструменты + +- [ ] 2.1 `api/pyproject.toml` (ruff: line-length 120, E,F,I,UP,B,PL; mypy с плагином pydantic), `requirements-dev.txt` + = `-r requirements.txt` + ruff, mypy, pytest, pytest-asyncio, pytest-env, httpx, jsonschema; проверка: `ruff check . && + ruff format --check . && mypy src` зелёные +- [ ] 2.2 `Dockerfile` (python:3.12-slim), `Makefile` (`up` — compose docs, `test`, `test-integration`, `lint`), + `.env.example`, `.env.test`; проверка: `docker build -t tabular-parser .` успешен + +## 3. Контракт + +- [ ] 3.1 `api/contracts/` — копии трёх схем + `examples/` из `../docs/contracts`; `src/contract/` — pydantic-модели + (`RecognitionRequest`, `RecognitionResultMessage`, `RecognitionResult`, `OsvDocument`, `StatementDocument`, деньги + как `Decimal` ↔ строка); `tests/unit/test_contract.py` — идентичность с docs (skip без docs), валидация примеров, + round-trip модели → JSON → схема; проверка: тесты зелёные + +## 4. Воркер и парсер-заглушка + +- [ ] 4.1 `src/parser/__init__.py`: протокол `Parser`, `ParseOutcome`, `NullParser` (`NOT_IMPLEMENTED`); `api/rules/README.md`; + `tests/unit/test_null_parser.py`; проверка: заглушка возвращает код и сообщение, не трогая входные байты +- [ ] 4.2 `src/core/clients.py`: `S3Storage.get_bytes(bucket, key)`, `put_json(bucket, key, obj)`; `src/core/nats.py`: + подключение (с/без nkeys), `subscribe_requests()` с повтором при отсутствии stream, `publish_result(envelope, + msg_id)`; проверка: unit-тесты с фейковым соединением; интеграционный тест подключения к compose-NATS +- [ ] 4.3 `src/core/worker.py`: цикл `fetch(1)` → `handle(msg)`: валидация запроса → S3 get → `Parser` → S3 put результата → + конверт → publish → ack; таблица ошибок D-5; `metrics.duration_ms`; запуск/остановка в lifespan при `NATS_ENABLED`; + `/healthcheck.nats`; проверка: unit-тесты `handle` с фейками — сценарии spec (NOT_IMPLEMENTED, FILE_READ_ERROR, + SCHEMA_MISMATCH, подмена парсера → `done`, ошибка публикации → nak) +- [ ] 4.4 `tests/integration/test_roundtrip.py` (маркер `integration`): положить файл в MinIO `uploads/…`, опубликовать + `request-tabular.json`, дождаться конверта в `recognition.result`; проверить `Nats-Msg-Id`, `error.code = NOT_IMPLEMENTED`, + ack запроса (consumer info: `num_ack_pending = 0`); проверка: зелёный против compose + +## 5. CI и завершение + +- [ ] 5.1 `.gitea/workflows/ci.yml` из `../docs/dev/ci/python-service.yml` (блок `env`: tabular-parser, порт 8001, без шагов + arm-core); проверка: зелёный run на ветке `feat/1-slice-0-skeleton` +- [ ] 5.2 Прогон DoD: `make up` → `make test` → `make test-integration` → ручная проверка из `docs/dev/README.md` + («проверка окружения»); `openspec validate slice-0-skeleton --strict`; результаты — в описание PR diff --git a/openspec/config.yaml b/openspec/config.yaml index 696699f..2f707a4 100644 --- a/openspec/config.yaml +++ b/openspec/config.yaml @@ -20,7 +20,7 @@ rules: - Любое допущение по открытому вопросу помечать ASSUMPTION(OQ-nn) specs: - Сценарии на реальных фикстурах из ../fixtures, синтетика — только для форматов, которых там нет - - Для парсеров: ни одного требования, завязанного на букву колонки, номер строки или имя листа + - "Для парсеров: ни одного требования, завязанного на букву колонки, номер строки или имя листа" design: - Вычисляемые объекты консолидации (сводная ОСВ, пары ВГО, шахматка) — чистые функции в domain/, без ORM-таблиц - Деньги — Decimal / numeric(18,2); единицы измерения (руб. / тыс. руб.) указывать явно @@ -37,4 +37,4 @@ operations: - После изменений прогонять линтеры и тесты репозитория; для web — tsc, biome, rsbuild build archive: guidance: - - Перед архивированием: DoD среза из ../CLAUDE.md выполнен, открытые вопросы занесены в 07-open-questions.md + - "Перед архивированием: DoD среза из ../CLAUDE.md выполнен, открытые вопросы занесены в 07-open-questions.md"