256 lines
63 KiB
Markdown
256 lines
63 KiB
Markdown
# Design: Срез 0 — скелет ядра arm-core
|
||
|
||
## Context
|
||
|
||
См. `proposal.md` — Why. Что задано и не обсуждается здесь:
|
||
|
||
- Шаблон команды (`../docs/context/03-architecture.md`, «Соглашения шаблона команды»): раскладка `api/src/`, pydantic-settings
|
||
с `OPENBAO__*`, схема `dkb`, Alembic с URL из настроек, DI ручными `Depends`, loguru, обработчики ошибок `{"detail": …}`,
|
||
SPA из `api/web/`, `DEBUG=true` отключает NATS/РАИСА.
|
||
- DBML v5.13 — источник имён и структур; поля вне DBML не вводятся.
|
||
- Контракт v1.0 — родительский change в `docs` (конверты сообщений, ack `processing`, файл результата, `Nats-Msg-Id`).
|
||
- Платформа (ответы 03.10.2026): Python **3.11**, PostgreSQL **16**, **NATS без JetStream**, Nexus недоступен до прода,
|
||
CI в Gitea нет — пайплайн платформы вне репозитория, параметры JWT РАИСА неизвестны.
|
||
- Инварианты CLAUDE.md: права — данные; единственный писатель в БД — ядро; append-only аудит; обмен только через `recognition_job`.
|
||
|
||
## Goals / Non-Goals
|
||
|
||
**Goals:** рабочая схема БД и миграции; пользователь с правами из БД через JWT; outbox/inbox на core NATS, доказанные
|
||
интеграционным тестом (включая потерю запроса и повторную публикацию); `make check-full`, который ловит расхождение
|
||
ORM ↔ DBML и невозвратные миграции.
|
||
|
||
**Non-Goals:** см. proposal. Дополнительно: не оптимизируем производительность outbox (одна инстанция, 5-секундный
|
||
опрос достаточно для Среза 0–3); не проектируем multi-tenant.
|
||
|
||
## Decisions
|
||
|
||
### D-1. ORM: модуль на часть DBML, enum'ы отдельно, имена — буквально из DBML
|
||
`src/models/`: `enums.py` (27 `enum.Enum` + фабрика `pg_enum(name)` → `sqlalchemy.Enum(..., name=…, schema="dkb",
|
||
native_enum=True, values_callable=lambda e: [m.value for m in e])`, значения — строки DBML, включая кириллические
|
||
`"ЮЛ"/"ИП"`), далее `directories.py` (часть 01), `access.py` (02), `accounting.py` (03), `files.py` (04),
|
||
`osv.py` (05), `statements.py` (06), `consolidation.py` (07), `kfo.py` (08), `audit_ex.py` (09 + `required_document_rule`).
|
||
Имена классов — CamelCase от имени таблицы (`RegionalOffice`), `__tablename__` — как в DBML. Типы: `uuid` → `UUID(as_uuid=True)`,
|
||
`varchar` → `String` (без длины, как в DBML), `text` → `Text`, `numeric(p,s)` → `Numeric(p, s)`, `timestamptz` →
|
||
`DateTime(timezone=True)`, `date` → `Date`, `smallint/int/bigint` → `SmallInteger/Integer/BigInteger`, `boolean` → `Boolean`,
|
||
`jsonb` → `JSONB`. Nullability и `default` — буквально из DBML (`default` → `server_default`). Константа
|
||
`DBML_VERSION = "5.13"` в `models/__init__.py`. *Альтернатива* — генерировать ORM из DBML скриптом: отвергнуто, DBML
|
||
содержит notes-семантику (partial unique, NULLS NOT DISTINCT), которую проще выразить руками и проверять скриптом.
|
||
|
||
### D-2. Миграция 0001 — одна ревизия, с нуля, откат до пустой схемы
|
||
`alembic/versions/0001_dbml_v5_13.py`: `CREATE SCHEMA IF NOT EXISTS dkb` (в `env.py`, до миграций), enum'ы → таблицы →
|
||
FK → индексы. Циклическая связь `stored_file.current_recognition_job_id ↔ recognition_job.file_id` — FK с `use_alter=True`.
|
||
Частичные уникальные индексы — `Index(..., unique=True, postgresql_where=text("is_current"))`; `document` —
|
||
`Index(..., unique=True, postgresql_nulls_not_distinct=True)` (PostgreSQL ≥ 15; на платформе 16). `downgrade()` удаляет
|
||
индексы, таблицы, enum'ы (`DROP TYPE`) и оставляет пустую схему. Автогенерация используется как черновик, итог правится
|
||
руками и проверяется шагом «upgrade → downgrade → upgrade» и `check_dbml_vs_orm.py` в `make check-full`.
|
||
|
||
### D-3. `check_dbml_vs_orm.py` — pydbml + сравнение метаданных, notes читаются по шаблонам
|
||
Парсер — библиотека `pydbml` (dev-зависимость). Сравниваются: множество таблиц; для каждой — колонки (имя, тип по таблице
|
||
соответствий D-1, nullability, PK, unique, default), FK (целевая таблица.колонка), индексы (колонки, unique), enum'ы (имя и
|
||
упорядоченный список значений). Notes интерпретируются только по трём шаблонам: `partial UNIQUE WHERE <expr>`,
|
||
`NULLS NOT DISTINCT`, `[pk]` на составных индексах; остальное — текст. Вывод — список расхождений `таблица.колонка: ожидалось /
|
||
фактически`, код возврата 1 при любом расхождении. Скрипт также запускается как pytest-тест (`tests/test_dbml_parity.py`).
|
||
*Если `pydbml` окажется неустановим* — fallback: собственный парсер подмножества DBML (Table/Enum/Indexes/Ref), ~200 строк.
|
||
|
||
### D-4. Seed — данные в файлах, upsert по натуральному ключу, разделение prod/dev
|
||
`api/seed/core/`: `regional_office.yaml` (только ГО), `roles.yaml`, `permissions.yaml`, `role_permissions.yaml`,
|
||
`chart_of_accounts.csv` (code, parent_code, name, kind, row_type, is_off_balance), `elimination_rules_v1.yaml` (правило +
|
||
счета сторон + исключения), `required_document_rules.yaml`. `api/seed/dev/`: `regional_offices.yaml` (2 РФ),
|
||
`users.yaml` (по одному на роль, email `<role>@armka.local`). `python -m scripts.seed [--dev]` — идемпотентно: upsert по
|
||
`code`/`inn`/`email`/`(statement_type, rule_no, version_no)`; удаление не делает. Коды прав — `<OBJECT>_<ACTION>`
|
||
(`DIRECTORY_EDIT`, `KFO_VERIFY`), при совпадении объекта и действия — одно слово (`EXPORT`). `role_permission.granted_by = NULL`
|
||
(системное действие, как в DBML для `*_by`). `elimination_rule.valid_from = 2024-01-01` — покрывает период фикстур.
|
||
|
||
### D-5. Аутентификация: `TokenVerifier` + dev-эмитент, РАИСА — каркас до ответа платформы
|
||
`src/core/auth.py`: `Claims(subject, email, exp)`; протокол `TokenVerifier.verify(token) -> Claims`;
|
||
`DevHs256Verifier` (секрет `AUTH_DEV_SECRET`, включается `AUTH_DEV_ISSUER=true`) и `JwksVerifier` (`AUTH_JWKS_URL` |
|
||
`AUTH_PUBLIC_KEY`, `AUTH_ISSUER`, `AUTH_AUDIENCE`, `AUTH_EMAIL_CLAIM` = `email`; алгоритмы RS256/ES256) на `PyJWT`.
|
||
Зависимости в `api/dependencies/auth.py`: `current_user` (Bearer → verify → `app_user` по email, `is_active`, иначе 403),
|
||
`require(code)` (403, если `code ∉ effective_permissions`). `effective_permissions` — один запрос по `user_role ⋈
|
||
role_permission ⋈ permission`. Dev-эмитент — `POST /api/v1/dev/token {email}` → HS256 JWT на 8 часов; роутер подключается
|
||
только при `AUTH_DEV_ISSUER=true` и `DEBUG=true` (в проде невозможен). Шаблонный middleware `raisa_fastapi_protected_api`
|
||
остаётся как в шаблоне (импорт при `DEBUG=false`; пакет недоступен вне банка до прода) — он защищает периметр, наш слой
|
||
извлекает личность. *Альтернатива* — только middleware РАИСА: отвергнуто, пакет недоступен и его API неизвестен.
|
||
|
||
### D-6. Outbox/inbox на core NATS — фоновые задачи в lifespan, ack и повторы на уровне приложения
|
||
JetStream на платформе нет: нет durable consumer'ов, серверного дедупа, redelivery, `max_deliver`. Схема:
|
||
- `src/core/nats.py`: подключение (пустые `NATS_SEED/NATS_JWT` → без учётных данных; иначе nkeys/JWT как в шаблоне),
|
||
`publish(subject, payload, msg_id)` с заголовком `Nats-Msg-Id`, подписка `recognition.result` с queue group `arm-core`
|
||
(несколько инстанций ядра получают результат по одному разу). Stream'ов и consumer'ов нет; переподключение — встроенное
|
||
в `nats-py` (`max_reconnect_attempts=-1`, `reconnect_time_wait=2`).
|
||
- `src/services/recognition_exchange.py`:
|
||
- `publish_pending()` — `SELECT … FROM recognition_job WHERE status='queued' OR (status='published' AND published_at <
|
||
now() - ACK_TIMEOUT_SECONDS) FOR UPDATE SKIP LOCKED LIMIT 50`; для `queued` — первая публикация (`published`,
|
||
`published_at`, `request_msg_id = <job_id>:<attempt>`, `request_subject`); для `published` без ack — повторная
|
||
публикация с тем же `Nats-Msg-Id` и обновлением `published_at`, счётчик повторов — в `metrics.republish_count`
|
||
(jsonb, поле контракта метрик ядра); при `republish_count ≥ OUTBOX_MAX_REPUBLISH` → `status = dlq`,
|
||
`error_code = NO_ACK`. `attempt` при этом не растёт — обработка не начиналась.
|
||
- `handle_message(msg)` — конверт → pydantic; `status = processing` → задание `published → processing`, `started_at`,
|
||
`service_name/version` (повторный ack игнорируется; ack для задания не в `published` — лог); иначе результат: job не
|
||
найден → лог; `result_msg_id == Nats-Msg-Id` → дубликат, игнор; иначе `s3_object` (роль `results`, `created_by =
|
||
service.name`) → обновление job (`status`, `finished_at`, `metrics`, `error_*`, `result_schema_version`,
|
||
`result_s3_object_id`, `result_msg_id`) → commit. Ошибка БД — лог и повтор по следующему сообщению не предусмотрен
|
||
(брокер не переотправит): задание дойдёт до `timeout` по `reply_deadline` и будет replay'нуто — осознанный компромисс
|
||
без JetStream.
|
||
- `expire_deadlines()` — `processing` (и `published`) с `reply_deadline < now()` → `timeout`.
|
||
- Период опроса `OUTBOX_POLL_SECONDS=5`, `ACK_TIMEOUT_SECONDS=30`, `OUTBOX_MAX_REPUBLISH=3`, вахтёр — 30 с.
|
||
- Запуск — `asyncio.create_task` в lifespan при `NATS_ENABLED=true` (в dev NATS нужен, шаблонный `DEBUG` его выключал —
|
||
добавлен явный флаг). Маппинг документов — протокол `ResultMapper` с реализацией `NoopMapper` (Срез 2 заменит).
|
||
*Альтернатива* — отдельный процесс-воркер: отвергнуто для Среза 0 (одна инстанция; SKIP LOCKED делает многоинстансность
|
||
безопасной позже без изменений). *Если JetStream появится* — `publish` и подписка переезжают на JS API, остальное без изменений.
|
||
|
||
### D-7. Тесты — PostgreSQL/NATS/MinIO из compose, маркеры `integration` и `fixtures`
|
||
`tests/conftest.py`: engine на `OPENBAO__DATABASE__DATABASE_URL` из `.env.test`; схема создаётся миграциями один раз на
|
||
сессию; каждый тест — в транзакции с откатом (`SAVEPOINT`-паттерн SQLAlchemy). `domain/` — без БД. Маркеры: `integration`
|
||
(нужны NATS/MinIO), `fixtures` (нужен `ARM_KA_FIXTURES`; пропуск с причиной). SQLite не используется: enum'ы, partial
|
||
unique, jsonb, `NULLS NOT DISTINCT`.
|
||
|
||
### D-8. Линтеры и проверки: ruff + mypy в `api/pyproject.toml`, `make check` / `make check-full`
|
||
`ruff`: `line-length = 120`, правила `E,F,I,UP,B,PL` (подмножество pylint), формат по умолчанию (black-совместимый).
|
||
`mypy`: `strict = false` на старте, `check_untyped_defs = true`, плагин pydantic; `src/models` и `src/domain` — `strict = true`.
|
||
Шаблон декларировал black/isort/pylint без конфигов; откат — замена файла конфига. CI в Gitea нет: `make check` (без
|
||
Docker) и `make check-full` (compose, миграции, DBML, `web`) — по `../docs/dev/checks.md`; `make check-full` перед PR.
|
||
|
||
### D-9. План счетов для seed — стандартный 94н + типовые субсчета 1С БП 3.0
|
||
Полный план конкретной конфигурации клиента недоступен (fixtures README: «план счетов из бандла макета» отсутствует).
|
||
Seed содержит синтетические счета 01–99 и забалансовые 001–011 (Приказ 94н) с `kind` по плану и все субсчета, на которые
|
||
ссылаются §4 (правила), §6 (шахматка) и `required_document_rule`; `row_type` — по глубине кода; `parent_code` — префикс.
|
||
Редкие субсчета клиентов добавляет администратор (ARCH-004, v5.6). Проверка: все счета `elimination_rules_v1.yaml` и
|
||
`required_document_rules.yaml` резолвятся в `chart_of_account` (OQ-47).
|
||
|
||
### D-10. API — префикс `/api/v1`, формат ошибок шаблона
|
||
Все новые роутеры под `APIRouter(prefix="/api/v1")` в `api/router.py`; ответы ошибок — `{"detail": …}` по-русски, как
|
||
в шаблонных обработчиках; 401 — нет/невалидный токен, 403 — пользователь не найден/неактивен/нет права.
|
||
|
||
### D-11. Dev без Nexus: Python 3.11, фильтр `raisa_*`, `optionalDependencies`
|
||
`Dockerfile` — `python:3.11-slim`; `make venv` — `python3.11 -m venv` + `grep -v '^raisa_' requirements.txt | pip install -r
|
||
/dev/stdin` + `requirements-dev.txt`; `Dockerfile` ставит так же. `requirements.txt` для платформы не меняется.
|
||
`web/package.json`: `raisa-ui`, `raisa-fastapi-protected-web` → `optionalDependencies` (с Nexus ставятся, без — пропускаются).
|
||
|
||
## Risks / Trade-offs
|
||
|
||
- [Без JetStream результат, опубликованный при простое ядра, теряется] → `reply_deadline` → `timeout` → replay (Срез 3+);
|
||
для Среза 0 это задокументированный компромисс, покрыт интеграционным тестом «ядро не слушало».
|
||
- [Повторная публикация без ack дублирует запрос работающему сервису] → сервисы держат множество `job_id` в работе и
|
||
отвечают повторным ack; тот же `Nats-Msg-Id`.
|
||
- [Автогенерация Alembic пропускает partial-индексы и enum-изменения] → финальная ревизия пишется руками; шаг
|
||
«upgrade → downgrade → upgrade» и сверка с DBML в `make check-full`.
|
||
- [`pydbml` не разбирает какую-то конструкцию DBML (кириллические enum-значения в кавычках)] → проверка на первом шаге
|
||
задачи 2.5; при провале — собственный парсер (D-3).
|
||
- [Dev-эмитент попадёт в прод] → роутер подключается только при `AUTH_DEV_ISSUER=true and DEBUG=true`; тест проверяет,
|
||
что при `DEBUG=false` маршрута нет.
|
||
- [Шаблон создаёт `settings` при импорте] → тесты всегда идут с `.env.test`; `ENV_FILE` читается в `Settings.model_config`.
|
||
- [Нет CI — проверки можно забыть] → `make check-full` в DoD каждой задачи и в описании PR.
|
||
|
||
## Migration Plan
|
||
|
||
Пустая БД → `alembic upgrade head` → `python -m scripts.seed [--dev]`. Откат — `alembic downgrade base` (все объекты
|
||
удаляются). Данных для переноса нет. В проде seed `core` выполняется при каждом деплое (идемпотентен), `--dev` — никогда.
|
||
|
||
## Open Questions
|
||
|
||
Остались (в родительском change): версия Node, параметры JWT РАИСА (нужны к первому деплою; до этого dev-эмитент),
|
||
появится ли JetStream (меняет только `core/nats.py`). Ни один не меняет структуру задач.
|
||
|
||
## Decisions made without discussion
|
||
|
||
| № | Дата | Решение | Обоснование | Цена ошибки |
|
||
|---|---|---|---|---|
|
||
| 1 | 2026-10-03 | D-1: раскладка ORM по частям DBML, `varchar` → `String` без длины | DBML не задаёт длины; меньше шума в сверке | добавить длины — правка типов в одном месте + миграция |
|
||
| 2 | 2026-10-03 | D-2: одна миграция 0001 на весь DBML, FK-цикл через `use_alter` | срез 0 стартует с пустой БД | нет — дальнейшие изменения идут новыми ревизиями |
|
||
| 3 | 2026-10-03 | D-3: сверка через `pydbml`, notes по трём шаблонам | писать полный парсер дорого | собственный парсер ~200 строк |
|
||
| 4 | 2026-10-03 | D-4: коды прав `<OBJECT>_<ACTION>`, `EXPORT` для совпадения; `valid_from` правил = 2024-01-01; seed разделён на core/dev | предсказуемость кодов; фикстуры 2024 г. | переименование seed-данных до первого деплоя бесплатно |
|
||
| 5 | 2026-10-03 | D-5: dev-эмитент HS256 за двумя флагами; РАИСА — конфигурируемый JWKS-каркас | пакет РАИСА недоступен, параметры неизвестны | дописать `RaisaVerifier` под реальные claims |
|
||
| 6 | 2026-10-03 | D-6: core NATS — queue group `arm-core`, ack `processing`, повторная публикация без ack 30 с × 3 → `dlq` с `error_code = NO_ACK`, счётчик в `metrics.republish_count`, `attempt` не растёт; ошибка БД при приёме результата не ретраится (доходит до `timeout`) | JetStream нет (ответ 03.10); DBML не имеет колонки для счётчика повторов | при JetStream — замена publisher/subscriber; при необходимости — колонка в DBML v5.14 |
|
||
| 7 | 2026-10-03 | D-6: `stored_file.status` в Срезе 0 не меняется, только `recognition_job` | жизненный цикл файла — Срез 2 | нет |
|
||
| 8 | 2026-10-03 | D-7: тесты только на PostgreSQL 16, транзакция с откатом | SQLite не воспроизводит схему | нет |
|
||
| 9 | 2026-10-03 | D-8: ruff + mypy вместо black/isort/pylint из бейджей README; `make check`/`make check-full` вместо CI | один инструмент, black-совместимый формат; CI в Gitea нет | замена конфига; YAML под пайплайн платформы при необходимости |
|
||
| 10 | 2026-10-03 | D-9: план счетов — 94н + типовые субсчета 1С, не полный план клиента | полного плана нет; ARCH-004 | дозагрузка CSV администратором или новым seed |
|
||
| 11 | 2026-10-03 | ASSUMPTION(OQ-46): все ОСВ расширенного подхода `is_mandatory = true` | иначе EXTENDED ничем не отличается от BASIC в чек-листе | правка одного yaml |
|
||
| 12 | 2026-10-03 | `Settings` читает `ENV_FILE`; `requirements-dev.txt` → `-r requirements.txt` + инструменты | шаблонный pytest.ini уже ссылается на `ENV_FILE`; dev-пины конфликтовали с prod | нет |
|
||
| 13 | 2026-10-03 | D-11: Python 3.11, фильтр `raisa_*` при dev-установке, npm `raisa-*` → `optionalDependencies` | ответы пользователя: 3.11, Nexus недоступен до прода | вернуть две строки в `dependencies` |
|
||
| 14 | 2026-10-03 | R-1: `Makefile` в корне arm-core (backend-цели `cd api`, web-цели `cd web`), venv — `api/.venv` | `check-full` включает `docker build` и `web`; spec quality-checks требует выполнения команд из `api/` — соблюдено | перенос файла и правка путей |
|
||
| 15 | 2026-10-03 | R-2: шаблонный код автоформатируется/чинится ruff отдельным коммитом (без смены поведения), остаточные правила — точечный `noqa`/per-file-ignores | `make lint` должен быть зелёным на каркасе | правка конфига |
|
||
| 16 | 2026-10-03 | R-3: подключение NATS переезжает из `app.py` в `src/core/nats.py` в задаче 5.3; `core/nats_callback.py` удаляется в 5.5 | не дублировать connect-логику между задачами 1.4 и 5.3 | небольшой рефакторинг |
|
||
| 17 | 2026-10-03 | R-4: `required_document_rule` — в `models/audit_ex.py` (D-1, секция DBML «ПРОЧЕЕ»), а не в `accounting.py` | design D-1 — источник; план в этом месте расходился | перенос класса |
|
||
| 18 | 2026-10-03 | R-5: seed — функция `run_seed(session, *, dev)` без commit, CLI коммитит | тесты идут в транзакции с откатом (D-7) | нет |
|
||
| 19 | 2026-10-03 | R-6: пути `api/dependencies/`, `api/endpoints/` из tasks.md = `src/api/dependencies/`, `src/api/endpoints/` | раскладка шаблона | нет |
|
||
| 20 | 2026-10-03 | R-7: обмен `recognition_job` не пишет `audit_event` в Срезе 0 | в `audit_action` нет действия для статусов задания; spec обмена аудит не требует | добавить действие в DBML v5.14 и вызовы |
|
||
| 21 | 2026-10-03 | R-8: правка `../docs` (`07-open-questions.md`) не выполняется исполнителем; текст допущений — в отчёте для PR в docs | `../docs` ведёт другой контроллер | отдельный PR в docs |
|
||
| 22 | 2026-10-03 | R-11: `PyJWT[crypto]` добавлен в `requirements.txt` (прод-зависимость), а не только в dev | `core/auth.py` импортирует `jwt` в проде | платформа ставит лишний пакет; обратное — падение при импорте |
|
||
| 23 | 2026-10-03 | R-12: подписка (5.4) вызывает `handle_message`, заглушку до 5.5 | порядок задач: lifespan раньше inbox | нет |
|
||
| 24 | 2026-10-03 | R-9: `MetaData(schema="dkb", naming_convention={ix, uq, ck, fk, pk})` в `models/base.py`; частичные и служебные индексы и слишком длинные FK — с явными `name=` ≤ 63 символов | имена объектов БД должны совпадать между ORM, миграцией 0001 и autogenerate (проверка пустой ревизии); downgrade удаляет по детерминированным именам; use_alter FK требует имени | переименование ограничений = новая ревизия (до первого деплоя бесплатно) |
|
||
| 25 | 2026-10-03 | R-14: ядро сверки ORM и DBML живёт в `api/scripts/` (`check_dbml_vs_orm.py`), CLI и тесты используют один код; `tests/dbml_parity.py` — тонкая обёртка | план требует скрипт в `scripts/` и тест; дублировать логику сверки нельзя | перенос файлов |
|
||
| 26 | 2026-10-03 | R-15: `CREATE SCHEMA dkb` выполняется только если схемы нет (проверка `pg_namespace`): DO-блок в офлайн-SQL и в ревизии 0001, условие в `env.py` | спека требует «создавать, если нет», а не безусловный `IF NOT EXISTS`; платформа может выдать роли миграций только права на схему | лишняя проверка |
|
||
| 27 | 2026-10-03 | R-13: в fix-раунд 1 задачи 6 вместе с Important включены minor T6.2 (нормализация `server_default`/`now()`) и T6.3 (проверка `postgresql_where`) | они блокировали переиспользование общего компаратора `dbml_parity` в задачах 7–9 | чуть шире fix-раунд |
|
||
| 28 | 2026-10-03 | R-16: БД сессии тестов — отдельная свежая `armcore_pytest_<hex>` на том же сервере (реквизиты из `.env.test`), мигрируется alembic один раз на сессию подпроцессом и удаляется в конце; `armka` тестами не используется; все БД-тесты помечены `integration` | dev-seed в `armka` ломал бы тесты «app_user пуст»; spec quality-checks требует `make check` без PostgreSQL. Отклонение от буквы D-7: URL тот же, имя БД другое | поменять имя БД в одной фикстуре |
|
||
| 29 | 2026-10-03 | R-17: `Claims.email` нормализуется `strip().lower()`; пользователь ищется по `func.lower(AppUser.email)`; более одного совпадения — 403 | `app_user.email` UNIQUE регистрозависим, SSO может отдать другой регистр; fail-closed при неоднозначности | два email, различающихся регистром, дадут 403 обоим |
|
||
| 30 | 2026-10-03 | R-18: в fix-раунд 1 задачи 16 включено security-hardening: Claims из странных типов (`exp` и др.) → `AuthError`, пустой `sub`, пин `cryptography` в `requirements.txt`, текст ошибки JWKS (содержит `kid`) в лог как `repr(str(exc))[:200]` | 500 вместо 401 и log injection недопустимы; `kid` отдельно недоступен, поэтому вместо `repr(kid)[:64]` — `repr` всего сообщения | чуть шире раунд; лимит 200 — константа `_LOG_TEXT_LIMIT` |
|
||
| 31 | 2026-10-03 | R-19: `api/.env.test` в финале указывает на стандартные порты compose (5432/4222/9000), а не на временные контейнеры контроллера (55432/54222/59000); переключено в задаче 25 | `make check-full` обязан идти против compose из `../docs/dev` | правка одного файла |
|
||
| 32 | 2026-10-03 | R-20: `Minio(...)` получает `secure` из нового поля `OPENBAO__MINIO__MINIO_SECURE` (bool, по умолчанию True); `.env.example`/`.env.test` — false для http-compose | поведение шаблона и платформы (TLS) сохраняется, локальный http работает без правки кода | одно поле настроек |
|
||
| 33 | 2026-10-03 | R-21: в fix-раунд разрешено включать дешёвые minor-замечания на тех же строках (для T21 — T21.1, T21.2, T21.5) | отдельный раунд на однострочные правки дороже самой правки | чуть шире fix-раунд |
|
||
| 34 | 2026-10-03 | R-22: финальные статусы inbox — `done, needs_check, error, timeout, dlq`; результат принимается для заданий в `queued`, `published`, `processing` | спека перечисляет done/error/timeout/dlq; `needs_check` — тоже результат сервиса, повтор для него — дубликат; потерянный ack не должен блокировать результат | поправить множество в одном месте |
|
||
| 35 | 2026-10-03 | R-23: `web/package-lock.json` добавлен в `.gitignore`; решение о коммите lock оставлено человеку | шаблон lock не содержит, `raisa-*` из Nexus вне банка дали бы иной lock; после web-check файл оставался untracked | web-сборка невоспроизводима по версиям (follow-up T26.2) |
|
||
| 36 | 2026-10-03 | T1: mypy strict задан явным списком флагов семейства strict в override для `src.models.*`, `src.domain.*`, а не `strict = true` | в mypy 2.4 `strict = true` внутри `[[tool.mypy.overrides]]` применяется глобально (проверено) | при необходимости настоящего `strict` перечень придётся вести вручную |
|
||
| 37 | 2026-10-03 | T1: для `miniopy_async` — override `follow_imports=skip` (Minio = Any) | mypy не видит реэкспорт `Minio` (attr-defined); правка кода или ignore на двух импортах хуже | S3-клиент не типизирован |
|
||
| 38 | 2026-10-03 | T1: в `requirements-dev.txt` нижние границы версий вместо точных пинов; dev-файл ставится через `grep -v '^-r'`, чтобы не втягивать `raisa_*` | платформенный `requirements.txt` не менялся; точные пины конфликтовали бы с платформой | mypy/ruff могут обновиться — воспроизводимость слабее |
|
||
| 39 | 2026-10-03 | T2: в `app.py` `kp = nkeys.from_seed(...) if NATS_SEED else None` + `assert` в `sig_cb` — временно до задачи 22 | `import src` падал с `ErrInvalidSeed` на пустом seed, проверка задачи была невозможна | при не-DEBUG и пустом seed подключение к NATS падало бы на assert до задачи 22 |
|
||
| 40 | 2026-10-03 | T2: `AUTH_*` строковые настройки — `""`, а не `None`; `AUTH_DEV_SECRET` ≥ 32 байт в `.env.example`/`.env.test`, `.env.test` коммитится | единообразие (потребители проверяют truthiness); минимум длины ключа HS256 | нет |
|
||
| 41 | 2026-10-03 | T3: `@biomejs/cli-linux-x64-musl` перенесён из devDependencies в optionalDependencies | на macOS `npm install` падал с EBADPLATFORM; на Linux musl пакет по-прежнему ставится | возврат строки снова ломает `npm install` на macOS |
|
||
| 42 | 2026-10-03 | T3: `.dockerignore` разигнорирован в `.gitignore` и исключает `docs`, `openspec`, `setup`, `.claude`; `package-lock.json` не коммитится | иначе `.dockerignore` не попадал бы в репозиторий; образу эти каталоги не нужны; шаблон lock не содержит | если сборке понадобится `setup/` — убрать из `.dockerignore` |
|
||
| 43 | 2026-10-03 | T4: ошибки учётных данных NATS (неполная пара) логируются, приложение стартует с `nats=down` (`RuntimeError` перехватывается в `_connect_nats`); при неудаче клиент не сохраняется | старт приложения не должен зависеть от NATS | неверная конфигурация видна только в логе и healthcheck |
|
||
| 44 | 2026-10-03 | T22: предпосылка T4.2 неверна — при `max_reconnect_attempts=-1` nats-py повторяет и первое подключение бесконечно, `connect()` не возвращается; прежний `await _connect_nats()` вешал старт без NATS (тесты T4 подменяли функцию). Теперь supervisor вызывает `connect` в фоне; backoff 1→30 с только при исключении `connect` | проверено пробой; повторы раз в 2 с (D-6) делает сам nats-py | для экспоненциальной паузы первого подключения пришлось бы править `options` nats-py |
|
||
| 45 | 2026-10-03 | T22: иерархия ошибок `NatsError` → `NatsConfigError`, `NatsNotConnectedError`, `NatsPublishError`; невалидный `NATS_SEED` — `config_error` без повторов; `on_connected` повторяется с backoff; `wait_ready(timeout)`; `stop()` делает `drain`; `app.state.nats_subs` удалён; `Minio` закрывает сессию при неудаче bucket_exists | подписка не должна молча отсутствовать; настройки не лечатся повтором | переименования; дополнительное поведение `stop()` |
|
||
| 46 | 2026-10-03 | T22: queue group `RESULT_QUEUE_GROUP = "arm-core"` в `src/core/nats.py`; AST-тест «нет JetStream в `src/`» — в юнит-наборе | группа — свойство ядра, не контракта; тест статический и должен идти в `make test` | нет |
|
||
| 47 | 2026-10-03 | T5: базовый класс enum — `enum.StrEnum`, имя члена = значению DBML (включая `ЮЛ`/`ИП` с `noqa: PLC2401`); `pg_enum(name)` + публичный реестр `ENUMS`, неизвестное имя — `KeyError` | ruff UP042 требует `StrEnum`; в коде нужна ровно DBML-строка | следствие: `str(member) == value`; смена имён — перенос по коду |
|
||
| 48 | 2026-10-03 | T6: составные unique из DBML `Indexes` — `Index(unique=True)` с именем `uq_...`, а не `UniqueConstraint`; в миграции `CREATE UNIQUE INDEX` | DBML-семантика — индекс, partial возможен только индексом, чётность с pydbml | низкая: для FK-ссылок не важно |
|
||
| 49 | 2026-10-03 | T6: `sort_order=-1` на `id` миксина; константа `SCHEMA` перенесена в `base.py`; тестовая БД `armcore_scratch_<hex>` с `DROP DATABASE ... WITH (FORCE)` (PG13+); `pytest_plugins` с `pg_helpers` | порядок колонок как в DBML (иначе `id` последним) | нет, влияет на порядок колонок в DDL |
|
||
| 50 | 2026-10-03 | T7/T8: явные сокращённые имена FK и индексов там, где шаблон R-9 > 63 символов (например `fk_financial_statement_value_column_id_fs_column`, `fk_consolidation_version_previous_version_id`, `uq_related_group_member_group_entity_current`, `uq_consolidation_lock_version_active`); use_alter-сторона цикла — `stored_file.current_recognition_job_id` | лимит идентификатора PostgreSQL; имена берутся из metadata и совпадают в миграции | низкая; при ручном написании миграций использовать те же имена |
|
||
| 51 | 2026-10-03 | T7: у `recognition_job.status` нет `server_default` (в DBML default'а нет); jsonb-колонки — `Mapped[Any] с nullable` | бриф приводил `queued` как пример, а не как DBML | добавить default можно только версией DBML |
|
||
| 52 | 2026-10-03 | T7/T8: тесты частей 01–06 проверяют подмножество таблиц; полный состав и `EXPECTED_TABLE_COUNT = 43` — в тесте последней части, привязанном к `load_dbml().tables` | иначе тест ломался бы с каждой следующей частью | нет |
|
||
| 53 | 2026-10-03 | T6/T8: `required_document_rule` — в `audit_ex.py` (R-4); формулировки tasks.md 2.2/2.4 и плана Task 8 остаются устаревшими | текст пунктов расходился с design D-1 | нет; формулировку поправит контроллер |
|
||
| 54 | 2026-10-03 | T9: `check_dbml_vs_orm` — код возврата 2 при отсутствии версии в первой строке DBML или неразбираемом файле; версия ищется только в первой строке; ручной разбор аргументов без argparse | ошибка использования (непригодный вход) отличается от расхождения (код 1); сообщения по-русски | смена кода в одной ветке `run_check` |
|
||
| 55 | 2026-10-03 | T9: правила сверки — табличный UNIQUE = unique index; любой CHECK в ORM = расхождение; явный NO ACTION равен отсутствию действия, RESTRICT нет; индексы группируются по набору колонок со сравнением свойств (`unique`, `WHERE`, `NULLS NOT DISTINCT`); порядок колонок — по общим колонкам; проверяется схема `dkb` | DBML-семантика; лишняя колонка не должна давать второй шумный дифф | ложные расхождения при иной трактовке |
|
||
| 56 | 2026-10-03 | T9: fallback `ENV_FILE` = абсолютный путь `api/.env.example` (только если `ENV_FILE` не задан и в cwd нет `.env`); `dbml-check` сразу включён в `check-full` | скрипт работает из любого cwd; D-8 требует DBML в `check-full` | нет |
|
||
| 57 | 2026-10-03 | T10: URL в `env.py` — `-x dburl` > `sqlalchemy.url` вызывающего кода (не заглушка `driver://`) > настройки; `ALEMBIC_DATABASE_URL` отвергнута | тесты и скрипты переопределяют БД без подмены env-файла; третий источник конфигурации не нужен | правка одной функции |
|
||
| 58 | 2026-10-03 | T10: онлайн `CREATE SCHEMA` — отдельной закоммиченной транзакцией до миграций; alembic в тестах — подпроцессом (`python -m alembic -x dburl=...`) | иначе соединение во внешней autobegin-транзакции; тест проверяет CLI-путь и не ломает логирование pytest | ~1 с на вызов; осиротевшая пустая схема безвредна |
|
||
| 59 | 2026-10-03 | T10: сверх брифа — тест «каталог после миграции == каталог `create_all` ORM» и офлайн-тест `--sql`; в ревизии сохранены `op.f(...)`; `downgrade` не дропает индексы отдельно; per-file-ignore `PLR0915` для `alembic/versions/*` | `compare_metadata` не сравнивает partial WHERE, NULLS NOT DISTINCT и порядок enum; `op.f` защищает от повторного применения naming_convention | связность тестовых модулей (импорт `_check_*`); список `TABLES` в ревизии дублирует `create_table` |
|
||
| 60 | 2026-10-03 | T11: `run_alembic` перенесён в `pg_helpers`; фабрика `tests/factories.py` (`make_osv_entry`), старые тесты 04–06 не переписаны; недоступность PG — `pytest.fail(pytrace=False)` на любое исключение CREATE DATABASE | не дублировать код; понятное сообщение вместо трейсбэка | неверные учётные данные тоже выдадут «PostgreSQL недоступен» (с текстом ошибки) |
|
||
| 61 | 2026-10-03 | T12: seed — пакет `scripts/seed_lib/` (4 файла), реэкспорт публичного API из `scripts.seed`; параметр CLI `--seed-dir`; три счётчика отчёта (вставлено, обновлено, без изменений) | модуль после форматирования превысил 400 строк; `--seed-dir` нужен автотесту CLI | только структура файлов |
|
||
| 62 | 2026-10-03 | T12: `role_permissions.yaml` — словарь «роль → список кодов прав», неизвестные поля отклоняются, код права по шаблону `<OBJECT>_<ACTION>` или `EXPORT`; права, убранные из yaml, не отзываются; существующие `role_permission` не обновляются (`granted_by`/`granted_at` не перезаписываются) | ловит опечатки; не затирать выдачи, сделанные администратором в UI (D-4) | для синхронизации потребуется явное решение; строгая валидация может отклонить будущие коды |
|
||
| 63 | 2026-10-03 | T13: субсчета плана счетов — строго по ссылкам §4/§6/чек-листа (+ родители); названия части субсчетов (66/67 .06/.22/.24/.26, 76.07.x, 76.08/.28/.38) выведены из роли в §4, а не из справочника 1С; `kind` субсчёта = kind родителя | D-9: все субсчета, на которые ссылаются; полного плана клиента нет | несуществующий субсчёт не резолвится до дозагрузки (ARCH-004); шахматка §6 не покажет дебетовое сальдо по 66.xx; неточные названия — правка CSV |
|
||
| 64 | 2026-10-03 | T13: валидатор CSV плана счетов строже задания — analytic-часть `[1-9]\d?`, счёт 00 отвергается, kind подсубсчёта в рантайме не сверяется с родителем (только тестом на файле репозитория); забалансовые 001–011 допускают субсчёт; порядок вставки — по глубине кода с предгенерацией uuid4 | администратор может загрузить свой CSV с иным kind | несогласованный kind в чужом CSV не ловится |
|
||
| 65 | 2026-10-03 | T14: составные статьи правил хранятся строкой `1450/1550` и `1310/1350` в `article_2_line` | «первая статья + comment» теряет информацию; comment не машиночитаем | потребитель, ждущий один код, получит составной — надо сплитить по `/` |
|
||
| 66 | 2026-10-03 | T14: правило 1 — сторона А `03, 76.07.9, 01` (01 резервный включён наравне с 03); сторона Б — `76.07.1`, исключения `76.07.5` и `76.07.9` на стороне 2; счёт без субсчёта (76, 66, 67, 80, 83) — маска «счёт и все субсчета» | буквальное чтение §4; как в spec-сценарии «Правило № 6» | если «резервный» означает приоритет, а не членство — реализовать в алгоритме; алгоритм, трактующий excluded только как потомка, проигнорирует вычитание |
|
||
| 67 | 2026-10-03 | T14: yaml правил — `statement_type`/`version_no`/`valid_from` на уровне файла (один файл = одна версия), `valid_to` не задаётся; чек-лист EXTENDED перечислен целиком (27), без наследования от BASIC; upsert без удалений (убранные счета и правила остаются в БД) | явность и отсутствие логики наследования; удаление — только вручную или миграцией | версия 2 потребует нового файла и расширения загрузчика; правка BASIC требует правки двух блоков; дрейф БД от файла |
|
||
| 68 | 2026-10-03 | T14: валидатор строже задания — отклоняет код счёта не строкой, «включён и исключён» на одной стороне, пустую сторону, неизвестные поля; общие помощники чтения вынесены в `seed_lib/readers.py` | защита от опечаток в справочниках; нет циклического импорта и файла > 400 строк | ужесточение может отклонить нестандартные записи |
|
||
| 69 | 2026-10-03 | T15: dev-РФ дописываются в `SeedData.regional_offices` (dev-файл не может объявить ГО или дублировать имя); у dev-пользователя ровно одна роль, `user_role` только добавляется; `business_segment = DKB`; `created_at`/`updated_at` — явно `now()`; защита `--dev` от прода не вводилась (только ASSUMPTION в docstring/help) | существующий upsert по `name` работает без изменений; триггера в БД нет; так велела инструкция задачи | снятая в файле роль остаётся назначенной; `--dev` на проде не заблокирован |
|
||
| 70 | 2026-10-03 | T16: `verify` — async (JWKS по сети через `to_thread`), уточнение D-5; сбой JWKS (сеть, неизвестный kid) → `AuthError` (401), деталь только в warning | требование задания; блокирующий вызов в event loop недопустим | задача 17 обязана `await`; недоступность SSO скрыта за 401 — мониторить по логу |
|
||
| 71 | 2026-10-03 | T16: при включённом dev-режиме (оба флага) — только dev-проверяющий, JWKS игнорируется; `AUTH_DEV_ISSUER=true` при `DEBUG=false` — dev не создаётся, warning, JWKS строится если задан | композитный проверяющий не нужен; dev-эмитент не должен работать вне DEBUG | нет |
|
||
| 72 | 2026-10-03 | T16: алгоритм статического ключа выводится из типа ключа (RSA → RS256, EC P-256 → ES256); иной тип и невалидный PEM → `ValueError`; заданные одновременно `AUTH_JWKS_URL` и `AUTH_PUBLIC_KEY` → `ValueError` | ключ одного типа не должен принимать токены другого алгоритма; fail-fast вместо молчаливого приоритета | ключи иных типов не поддержаны |
|
||
| 73 | 2026-10-03 | T16: `aud` и `iss` не проверяются, если `AUTH_AUDIENCE`/`AUTH_ISSUER` пусты; email из claim — `strip()`; `noqa: PLR0913` на `JwksVerifier.__init__` | иначе PyJWT отвергает любой токен с `aud`; параметры РАИСА неизвестны до первого деплоя | пустая настройка = проверка отключена (предложение OQ: обязательный `aud`) |
|
||
| 74 | 2026-10-03 | T17: `current_user` — единая зависимость, возвращает `Principal` (user, regional_office, roles, permissions); цепочка `verified_claims` (без БД) → `current_user`, поэтому 401 не открывает сессию БД; `require(code)` валидирует только непустоту кода | 401 не должен ходить в БД; отдельная `current_principal` избыточна | нет |
|
||
| 75 | 2026-10-03 | T17: роль с `is_active=false` не даёт прав и не попадает в `roles`; неактивный пользователь — 403 «учётная запись отключена»; филиал на активность не проверяется; схема `Bearer` без учёта регистра, ровно два токена | DBML `role.is_active`; спека молчит | если нужно иначе — права отключённой роли вернутся |
|
||
| 76 | 2026-10-03 | T17: `get_token_verifier` кэширует и результат `None` (ничего не настроено); `RuntimeError` конфигурации `build_verifier` не перехватывается (500, fail-fast); выборка пользователей с `LIMIT 2` и `ORDER BY id` | настройки читаются один раз на приложение; ошибка конфигурации не должна маскироваться | смена настроек требует перезапуска |
|
||
| 77 | 2026-10-03 | T18: dev-эндпоинт выпуска токена — «не найден или отключён» → 404 одним текстом; неоднозначный email → 409 без выпуска токена; `DevTokenRequest.email` — `str` (strip, lower), а не `EmailStr`; секрет < 32 байт → `RuntimeError`, пустой → `ValueError` | не раскрывать существование учётки; `email-validator` отвергает домен `.local` у dev-пользователей | зависимость `email-validator` в requirements не используется |
|
||
| 78 | 2026-10-03 | T18: исправлен шаблонный обработчик 422 (новая константа статуса, `jsonable_encoder(exc.errors())`); SPA-заглушка отдаёт 404 для любого пути под `/api/v1`; `MeResponse.email = user.email or ""` | `ValueError` в `ctx` ломал ответ 500-кой; спека требует 404 для выключенного dev-маршрута (было 405 или index.html) | GET несуществующего пути под `/api/v1` теперь 404, а не SPA (желаемо) |
|
||
| 79 | 2026-10-03 | T19: README описывает трейсбэк `ClientConnectorSSLError` к MinIO как штатный для dev и следует коду (NATS не зависит от DEBUG, middleware РАИСА не подключён), а не CLAUDE.md репозитория | правка `app.py` вне задачи; CLAUDE.md arm-core расходится с кодом | предложение `SERVER__HOST=127.0.0.1` в README осталось deferred |
|
||
| 80 | 2026-10-03 | T20: модели контракта принимают лишние поля (`extra="ignore"`), `strict=True`; `result_schema_version` по схеме допускает любой MAJOR (`^\d+\.\d+$`), в отличие от `schema_version` (`^1\.\d+$`) | схемы не содержат `additionalProperties: false`; модель строже схемы ломает обратно совместимый MINOR; схема возможно небрежна (вынесено контроллеру docs) | лишние поля теряются при round-trip; при нужде сквозного проброса — `extra="allow"` |
|
||
| 81 | 2026-10-03 | T20: деньги — `Decimal` (допускает null), вход строкой по `^-?\d+(\.\d{1,2})?$` (fullmatch), `null` или `Decimal`; `float`, `int`, `bool` отвергаются; UUID только каноническая запись 8-4-4-4-12; `integer` строгий (`5.0` отвергается, хотя jsonschema считает целым) | деньги не должны становиться float; модель не мягче схемы | отличие от jsonschema в сторону строгости (хвостовой `\n`, `5.0`) не покрыто тестами |
|
||
| 82 | 2026-10-03 | T20: «необязательное, но без null» поля (`result_s3`, `error` в конверте, `metrics`, `header_variant`) отвергают явный `null` (`OptionalNotNull`); `reply_deadline` — aware обязателен, приводится к UTC и сериализуется `...Z`; условные правила только по схеме; добавлены `ack_msg_id`/`result_msg_id` | модель не должна быть мягче схемы; `rfc3339-validator` не проверяет формат | модель строже валидатора на naive date-time |
|
||
| 83 | 2026-10-03 | T21: Decimal в jsonb аудита — строка формата `f` без экспоненты; собственный сериализатор `to_jsonable` вместо `jsonable_encoder` (тот даёт float); неизвестные типы → `TypeError`; float допустим, NaN/Inf → `ValueError`; `object_type` из пробелов пуст | деньги не должны становиться float | расхождение формата со старыми записями, если решат иначе |
|
||
| 84 | 2026-10-03 | T21: сканер удаления `AuditEvent` — best-effort, только внутримодульное отслеживание имён; тестовое событие — `RECOGNIZED_DATA_EDIT` / `AuditSection.RECOGNITION`; `noqa: PLR0913` на `record` (14 полей по спеке) | в `src/` нет обращений к `AuditEvent` вне `models/` и `services/audit.py`; в `audit_action` нет действия для статусов задания (R-7) | межмодульное удаление сканер не поймает; DB-level append-only — O-8, follow-up |
|
||
| 85 | 2026-10-03 | T23: невалидный запрос (сейчас `stored_file.original_name IS NULL`) → `status=error`, `error_code=INVALID_REQUEST`, `finished_at`, а не «лог и пропуск» | иначе задание выбиралось бы первым в каждом проходе (ORDER BY `requested_at` NULLS FIRST) и 50 битых блокировали бы outbox; `INVALID_REQUEST` — код ядра, в enum контракта его нет | починка и переотправка — ручной сброс в `queued`; замена строковой константы |
|
||
| 86 | 2026-10-03 | T23: `reply_deadline` по умолчанию = момент первой публикации + 5 мин для обоих pipeline, не меняется при повторах; `queued` с прошедшим сроком публикуется как есть; `published` с прошедшим сроком остаётся вахтёру | DBML note: OCR — 5 мин, для парсера значения нет; 3 повтора по 30 с (~2 мин) укладываются до dlq | константа `DEFAULT_REPLY_TIMEOUT`; одна лишняя публикация просроченного задания |
|
||
| 87 | 2026-10-03 | T23: время — часы приложения (инжектируемые `now`), не `now()` БД; проход при `not nats.is_connected` пропускается целиком, включая переводы в `dlq`; первая публикация берёт `Nats-Msg-Id` из текущего `attempt`, повтор — сохранённый `request_msg_id`; `published_at = NULL` в `published` считается давним; цикл — сначала проход, потом пауза | детерминированные тесты; без NATS нельзя отличить «сервис молчит» от «мы не можем публиковать»; готовность к replay | расхождение часов приложения и БД не учитывается |
|
||
| 88 | 2026-10-03 | T23: `ExchangeTasks` (`start`/`stop`) хранится в `app.state.exchange`, запуск привязан к созданию `NatsConnection`; фабрика `CommittedJobs` копит id для teardown | запуск ⇔ `NATS_ENABLED=true` | нет |
|
||
| 89 | 2026-10-03 | T24: нет заголовка `Nats-Msg-Id` → warning и производный id `result:<job_id>:<attempt>` (для ack — `ack:...`), а не отказ | выбор из брифа; сообщение не должно теряться | результат другой попытки без заголовка дедуплицируется по id текущей попытки |
|
||
| 90 | 2026-10-03 | T24: `file_id` конверта ≠ `recognition_job.file_id` → warning `file_mismatch`, сообщение не применяется | спека ищет задание только по `job_id`; регистрировать чужой файл результата опаснее, чем ждать timeout | законно иной `file_id` (по контракту не должен) — результат теряется до timeout |
|
||
| 91 | 2026-10-03 | T24: порядок в транзакции результата — s3_object → обновление задания → mapper → commit; исключение mapper'а откатывает всё | mapper видит итоговое состояние задания и может его уточнить (Срез 2, например понизить до needs_check) | Срез 2 должен знать, что `job` уже обновлён |
|
||
| 92 | 2026-10-03 | T24: результат без ack (`queued`/`published`) принимается, `started_at` остаётся NULL; `error_code`/`error_message` перезаписываются из конверта, `metrics` и `fields_*` — только если есть в конверте | момент начала обработки неизвестен, подставлять `finished_at` было бы выдумкой | отчёты по длительности должны учитывать NULL |
|
||
| 93 | 2026-10-03 | T24: ключ s3_object (bucket, key) совпал с существующим → переиспользование и warning (`ON CONFLICT DO NOTHING RETURNING`), роль не проверяется, `content_type`/`size_bytes`/`sha256` не заполняются; `needs_check` не принимается поверх финального `needs_check`; `handle_message` ловит `Exception` (не только `SQLAlchemyError`), `CancelledError` пробрасывается | S3 в Срезе 0 не читается; asyncpg бросает не-SQLAlchemy исключения при недоступной БД | роль существующей строки может не совпасть |
|
||
| 94 | 2026-10-03 | T24: уровни логов — WARNING для невалидного, неизвестного job, file_mismatch, ack не в published, результата для финального статуса и отсутствия заголовка; DEBUG для повторного ack и дубликата; в лог невалидного сообщения только `job_id` (≤ 64) и `loc: msg` без входных значений; упоминания `nats_callback` убраны из README и CLAUDE.md arm-core | не логировать входные данные; модуль удалён (R-3) | нет |
|
||
| 95 | 2026-10-03 | T25: цикл миграций не гоняется отдельным шагом `check-full` (входит в `test-integration`; цель `migration-cycle` — для ручного запуска); `check-infra` — скрипт `api/scripts/check_infra.py` перед `test-integration`, читает адреса из `.env.test`; изменён `test_config.py` (порты) | дубль добавил бы ~10 с; сообщение «поднимите compose командой make up» | цикл миграций не виден отдельной строкой в выводе `check-full` |
|
||
| 96 | 2026-10-03 | T26: живой обмен показан скриптом на ORM (не SQL), скрипт лежит рядом с отчётом и не добавлен в репозиторий; `result.json` в MinIO не загружался (NoopMapper только регистрирует `s3_object`); `make check-full` прогнан с `api/.env`, файл удалён | корректные enum и FK; содержимое результата Срез 0 не читает | Срез 2 дополнит демо загрузкой; демо-данные уничтожены `down -v` |
|
||
| 97 | 2026-10-04 | T27: OQ-46 и OQ-47 уже есть в реестре — вместо новых номеров предложены правки статусов; новые вопросы — `OQ-?? (предл. N)`, 15 штук; в CLAUDE.md `domain/` помечена «появится в Срезе 3+»; «обязательная проверка CI» заменена на `make dbml-check` (входит в `check-full`); рекомендации (триггер, `NULLS NOT DISTINCT`, обязательный `aud`) — предложения, а не принятые решения | дубли нарушали бы реестр; каталога `api/src/domain/` в Срезе 0 нет; CI в Gitea нет | перенумерация при переносе в docs; правка двух строк и одной строки CLAUDE.md |
|