Срез 0 реализован и влит (PR #3), change перенесён в openspec/changes/archive, спеки — в openspec/specs. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
68 KiB
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(конверты сообщений, ackprocessing, файл результата,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 grouparm-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 |
| 98 | 2026-10-04 | R-25: сценарий audit-trail «Системное событие» переформулирован под R-7: WHEN — «системный процесс (без пользователя) вызывает сервис аудита» (THEN прежний); под требованием — NOTE, что обмен через recognition_job в Срезе 0 аудит не пишет | прежний WHEN («consumer результатов фиксирует изменение задания») противоречил рулингу R-7 (design #20), спека и код расходились | при появлении аудита обмена в Срезе 2+ сценарий придётся вернуть к конкретному источнику |
| 99 | 2026-10-04 | R-26: open-questions-proposal.md хранится в openspec/changes/slice-0-skeleton/ |
DoD требует занести открытые вопросы; ../docs ведёт другой контроллер; файл архивируется вместе с change и не теряется вместе с ledger |
вопросы не попадут в реестр 07-open-questions.md, если владелец ../docs не откроет docs-PR |
| 100 | 2026-10-04 | FIX-wave (I-1): тип nullable jsonb — функция nullable_jsonb() в models/base.py (JSONB(none_as_null=True)), сверка ORM↔DBML не менялась (тип определяется по классу), добавлены самотест компаратора и тест-перечень всех nullable jsonb-колонок с флагом |
JSONB.__init__ без аннотаций — mypy strict запрещает вызов в моделях; одна точка вместо шести копий |
новая jsonb-колонка без nullable_jsonb() — тест-перечень упадёт и подскажет |
| 101 | 2026-10-04 | FIX-wave (I-2): ошибка БД в обработчиках 409/500 логируется одной записью logger.error без текста исключения (не logger.exception): путь, метод, классы ошибок SQLAlchemy и драйвера, SQLSTATE, SQL с плейсхолдерами (≤ 200 символов), стек без сообщения |
проверено: str() ошибки asyncpg содержит параметры запроса и DETAIL со значениями ключей (email, ИНН) — персональные данные в логе |
при разборе инцидента значения ключей придётся восстанавливать по SQLSTATE и стеку |
| 102 | 2026-10-04 | FIX-wave (I-3): SMB: SMBSettings = SMBSettings() (по образцу ServerSettings шаблона); reset_connection_cache тоже только при заданном SMB_HOST и через asyncio.to_thread; порядок остановки — задачи обмена → NATS → S3 → SMB → engine.dispose(); старт ждёт регистрацию SMB (в потоке), а не уводит её в фон |
без SMB сбрасывать нечего; блокирующие вызовы smbclient не должны держать цикл событий; фоновая регистрация усложнила бы остановку | настроенный, но молчащий SMB задержит старт до тайм-аута smbclient (60 с) |
| 103 | 2026-10-04 | FIX-wave (I-5): пункты финального ревью (д) срок ответа tabular и (е) append-only на уровне БД записаны как дополнения к предл. 11 и 9, а не новыми номерами; новые вопросы — предл. 16–21 в разделе B2 | реестр не должен получать дубли | при переносе в docs дополнения надо слить с исходными строками |
| 104 | 2026-10-04 | FIX-wave (I-7): в тесте реального lifespan подменяется app_module.NatsConnection на partial(NatsConnection, result_subject=<уникальный>) — настоящие _create_nats и _subscribe_results выполняются; движок БД — заглушка; OUTBOX_POLL_SECONDS=1; queue group проверяется по мониторингу NATS (/connz?subs=detail, порт 8222) только если он доступен, изменение в БД проверяется всегда |
общий recognition.result в compose не трогаем; реальный пул БД сессии pytest закрывать нельзя |
без мониторинга (другой порт) queue group подтверждается только косвенно — тестом двух инстансов |
| 105 | 2026-10-04 | FIX-wave (M-9, M-11, M-4): команды уровня кластера в тестах (включая CREATE ROLE в тесте предвыданной схемы) — через служебную БД postgres; в .env.example и .env.test добавлены SERVER__HOST=127.0.0.1 и SERVER__PORT=8000, новый тест сверяет набор ключей двух файлов (раньше такого теста не было); OUTBOX_MAX_REPUBLISH=0 допустим (сразу dlq после ack-таймаута) |
тестам не нужна существующая armka; ключи двух env-файлов расходились бы незаметно; 0 повторов — осмысленная настройка |
нет |