2026-10-04 01:21:59 +04:00

256 lines
63 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 |