Создание с кодом GSK-/GSZ-NNNNNN, изменение name/created_date, удаление только пустой группы
(ASSUMPTION(OQ-34), закрытые строки состава удаляются вместе с группой), добавление члена
(та же ГК, kind группы, MEMBER_OF_OTHER_GROUP / MEMBER_EXISTS), исключение SCD2. Видимость
группы — через явный JOIN ГК и company_groups_visible (T4.1). Аудит MEMBERSHIP_CHANGE.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
`office_for_create` применяется к `regional_office_id` только при фактической смене филиала: тело с тем же
значением — no-op (Review Focus 1), правка `name` в том же запросе проходит. Тест на ЮЛ в ГК своего филиала
с чужим `regional_office_id`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
Репозиторий, сервис и эндпоинты /api/v1/directories/{regional-offices,company-groups}:
код GK-NNNNNN из последовательности, снимки в company_group_history, no-op PATCH
без истории и аудита, поиск q по name/crm_id/code с экранированием LIKE, видимость
по филиалу (чужая ГК = 404 как несуществующая), аудит GROUP_CHANGE/ASSIGNMENT_CHANGE.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
Закрывает T3.1: предупреждение «Включён dev-эмитент…» из src.core.auth.logger
подавляется через monkeypatch в фикстуре app. Локальный autouse в
test_me_and_dev_token.py оставлен: его _build_app вызывает build_verifier без
фикстуры app.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
Первый модуль src/domain/ (чистые функции, без I/O, mypy strict): VisibilityScope,
scope_for/scope_of, SQL-фильтры company_groups_visible/legal_entities_visible,
проверки company_group_is_visible/legal_entity_is_visible, office_for_create
(чужой филиал → 422 OFFICE_NOT_ALLOWED). Код роли не читается; business_segment
не фильтрует (ASSUMPTION(OQ-86)). Принадлежность ЮЛ ГК филиала — подзапросом IN,
а не JOIN (число строк не меняется, нет конфликта с JOIN вызывающего).
Тесты: unit без БД (компиляция Select под PostgreSQL, все ветки) + один
integration-тест семантики фильтров и их согласованности с проверками объектов.
Отмечены шаги Task 4 в плане и 2.1 в tasks.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
Фабрики ГК/ЮЛ/ГСК/членства/пользователя (flush, без commit), фикстуры seeded/app/auth_client
в conftest (override сессии через begin_nested), test_me_and_dev_token переведён на общие
фикстуры (сочетания флагов остались на локальном _build_app), test_factories.py проверяет фабрики.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
- get_async_session: одна транзакция на запрос (commit при успехе, rollback при исключении), R-1;
объявляется только через SessionDep (scope="function") — commit до отправки ответа, одна сессия
на все зависимости запроса; current_user и dev-эмитент переведены на SessionDep.
- DomainError/NotFoundError/ConflictError/DomainValidationError + обработчик
{"detail": {"code", "message", ...}}; ответы HTTPException и ошибок БД не меняются.
- RequestAudit/get_request_audit: AuditService.record с user_id, ip_address, user_agent запроса.
- schemas/common.py: Page[T], PageParams (limit 1..200, по умолчанию 50; offset >= 0), ErrorDetail.
- tests/test_uow_and_errors.py: формат ошибок, пагинация, аудит, scope сессии во всех маршрутах,
настоящий commit/rollback на временной БД, commit до http.response.start, ошибка commit -> 409.
- План: Task 2 отмечена; tasks.md: 1.2 отмечен.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
- RelatedGroupMember.kind (related_group_kind, NOT NULL) между legal_entity_id и valid_from, как в DBML;
частичный уникальный индекс uq_related_group_member_entity_kind_current (legal_entity_id, kind) WHERE is_current —
ЮЛ одновременно в одной ГСК и одной ГСЗ.
- AuditAction: LEGAL_ENTITY_CHANGE (после GROUP_CHANGE), USER_CHANGE (после USER_ROLE_CHANGE).
- Последовательности company_group_code_seq / related_group_code_seq в metadata (design D-5).
- Миграция 0002_dbml_v5_14: related_group_member пересоздаётся с переносом строк (kind из группы), чтобы порядок
колонок совпал с DBML и create_all; ALTER TYPE ADD VALUE ×2; downgrade пересоздаёт audit_action v5.13.
- DBML_VERSION = 5.14, пути к data-model-v5.14.dbml (Makefile, dbml_parity), тесты: откат на шаг, перенос строк,
уникальность gsk+gsz, офлайн-SQL до head.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
tests/test_exchange_lifespan_nats.py (integration, NATS и PostgreSQL из
.env.test, уникальный subject результатов, queue group arm-core):
- настоящий lifespan при NATS_ENABLED=true: /healthcheck -> nats=ok,
подписка видна в мониторинге NATS (/connz) с qgroup arm-core, публикатор
lifespan переводит задание в published, inbox принимает ack и результат;
остановка гасит задачи обмена до nats.stop(), закрывает пул БД и не
оставляет висящих задач;
- два NatsConnection с настоящим handle_message: результат обрабатывает
ровно один инстанс, задание и s3_object меняются один раз; повторная
доставка того же сообщения ничего не меняет.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Вывод loguru из негативных тестов больше не засоряет прогон: pytest
перехватывает stderr и показывает его только для упавших тестов (секция
«Captured stderr call»). Ни один тест не зависел от отключённого
перехвата; полный прогон зелёный.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
В api/.env.example задан SERVER__HOST=127.0.0.1 (и SERVER__PORT=8000):
при DEBUG и AUTH_DEV_ISSUER эндпоинт dev-токенов выдаёт токен любому, кто
знает email. Значение приложения по умолчанию (0.0.0.0) не меняется.
.env.test — те же ключи; новый тест сверяет набор ключей .env.example и
.env.test. README: рекомендация в быстром старте и строка таблицы.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Команды уровня кластера в тестах (создание и удаление временных БД, роль
в тесте предвыданной схемы) подключались к рабочей БД armka. Теперь —
к служебной БД postgres того же сервера (service_url): тестам не нужна
существующая armka. Проверено прогоном с несуществующей БД в
OPENBAO__DATABASE__DATABASE_URL.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
job_id из сообщения, не прошедшего контракт, — данные отправителя; без
экранирования перевод строки в нём позволял подделать строку лога.
Теперь repr(job_id[:64]), как в рулинге R-18 для kid токена.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
OUTBOX_POLL_SECONDS и ACK_TIMEOUT_SECONDS — PositiveInt, OUTBOX_MAX_REPUBLISH —
NonNegativeInt: 0 и отрицательные значения периода и таймаута, отрицательное
число повторов отвергаются при старте. Значения по умолчанию прежние.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- OPENBAO__SMB__* необязательны (по умолчанию пусто); при пустом SMB_HOST
сессия SMB не регистрируется (DEBUG «SMB не настроен»), в .env.example
и .env.test SMB выключен — в логе dev-запуска больше нет трейсбэка SMB.
- Задан SMB_HOST: register_session и reset_connection_cache вызываются
через asyncio.to_thread и не блокируют цикл событий.
- Остановка: задачи обмена -> NATS -> S3 -> SMB -> engine.dispose();
каждый шаг в своём try/except с логом step=..., ошибка одного шага не
отменяет следующие. Пул соединений БД закрывается последним (M-3).
- README: «что в логе при запуске», таблица настроек SMB, порядок
остановки.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Обработчики IntegrityError (409) и SQLAlchemyError (500) молча отдавали
ответ. Теперь каждая такая ошибка — одна запись ERROR: путь и метод
запроса, классы ошибок SQLAlchemy и драйвера, SQLSTATE, SQL-текст с
плейсхолдерами (усечён до 200 символов) и стек вызовов. Текст исключения
в лог не идёт: str() ошибки asyncpg/SQLAlchemy содержит параметры запроса
и DETAIL PostgreSQL со значениями ключей (email, ИНН). Ответы прежние.
JwksVerifier.verify: TypeError/ValueError, превращаемые в AuthError,
теперь пишут warning в том же санированном виде repr(str(exc))[:200].
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Все nullable jsonb-колонки ORM (audit_event.before_value/after_value,
ex_snapshot_item.payload, recognition_job.group_schema/metrics,
osv_entry.original_values) объявлены через nullable_jsonb() =
JSONB(none_as_null=True): Python None сохраняется как SQL NULL, IS NULL
находит строку. DDL не меняется; сверка с DBML и миграции зелёные.
Обходы для не-объекта в metrics (CASE по jsonb_typeof в публикаторе,
merge_metrics в inbox) оставлены: строки до исправления и сырой SQL
могут содержать jsonb null. Тесты: SQL NULL после AuditService.record
без before/after, metrics фабрики None -> SQL NULL, повтор публикации и
слияние метрик для SQL NULL и jsonb null, перечень колонок с флагом.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Строка «Контракты» перечисляет три реальные схемы recognition-* и копии в api/contracts/; ex-payload-v1 помечен
как будущий (Срез 7). В раскладке — src/api/endpoints. В docstring app.py убрано утверждение, что middleware
РАИСА подключается при DEBUG=False (пакет лишь условно импортируется).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Цели docker-build, web-check, check-infra (подсказка «make up»); .env.test переведён на 5432/4222/9000 (R-19),
test_config и README обновлены. Цикл миграций входит в test-integration и отдельным шагом не дублируется.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
handle_message(msg, *, session_factory, mapper, now): разбор конверта по контракту v1.0, одна короткая транзакция
с SELECT … FOR UPDATE (ждёт блокировку публикатора/вахтёра); ack published → processing; результат — дедуп по
result_msg_id, финальные статусы (R-22) не трогаются, регистрация s3_object (results), слияние метрик с сохранением
republish_count. Подписка app.py получает handler с фабрикой сессий и NoopMapper; шаблонный core/nats_callback.py
удалён (R-3). audit_event не пишется (R-7).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- services/recognition_exchange.py: publish_pending — одно задание на короткую транзакцию
(SELECT … LIMIT 1 FOR UPDATE OF recognition_job SKIP LOCKED → публикация → UPDATE → COMMIT):
queued → published (request_msg_id = <job_id>:<attempt>, request_subject, published_at, reply_deadline —
5 мин по умолчанию, requested_at); published без ack дольше ACK_TIMEOUT → повтор с тем же Nats-Msg-Id и
attempt, metrics.republish_count (jsonb ||, прочие ключи сохраняются); после OUTBOX_MAX_REPUBLISH → dlq/NO_ACK;
запрос не собирается по контракту → error/INVALID_REQUEST; первая NatsPublishError откатывает задание и
прерывает проход; нет соединения — проход пропускается. published с прошедшим reply_deadline оставлен вахтёру.
- expire_deadlines — один UPDATE processing/published с reply_deadline < now → timeout/TIMEOUT.
- run_publisher_loop / run_watchdog_loop (30 с), ExchangeTasks: запуск в lifespan вместе с NatsConnection,
остановка до nats.stop(). audit_event не пишется (R-7).
- Тесты: интеграционные против PostgreSQL и NATS (схема запроса, повторы и dlq на инжектируемых часах, ack после
первого повтора, вахтёр, ошибка публикации, два конкурентных прохода, циклы), lifespan; фабрика CommittedJobs.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
MinioSettings.MINIO_SECURE (по умолчанию true — https, как в шаблоне и на платформе) передаётся в Minio(secure=...);
.env.example и .env.test — false (MinIO из docker compose слушает http). README: трейсбэк S3 из раздела
«что в логе» убран, переменная добавлена в таблицу конфигурации.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- NatsConnection: supervisor первого подключения в фоне (старт приложения не ждёт NATS), nats-py
переподключается сам (max_reconnect_attempts=-1, reconnect_time_wait=2) и восстанавливает подписки;
экспоненциальный backoff 1→30 с, если connect завершился исключением; логирование ошибок без шума
(первая ошибка серии — warning, дальше не чаще раза в минуту).
- publish(subject, payload, msg_id): заголовок Nats-Msg-Id + flush; нет соединения или нет подтверждения —
NatsPublishError. subscribe_results(handler): queue group arm-core, исключения обработчика логируются.
- build_nats_auth перенесён из app.py; ошибка учётных данных — NatsConfigError, категория config_error,
без повторов (T4.1). Первое подключение повторяется (T4.2): прежний await connect в lifespan при
недоступном NATS висел бесконечно (nats-py с max_reconnect_attempts=-1 повторяет первое подключение сам).
- app.py: подключение переехало в core/nats.py (R-3), lifespan запускает/останавливает NatsConnection,
подписка — заглушка services/recognition_exchange.handle_message (полная обработка — задача 24).
- Тесты: юнит (поддельный клиент, инжектируемый sleep, отсутствие JetStream в src/) и интеграционные
против NATS без JetStream (заголовок, queue group без дублей, недоступный сервер).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Dev-эмитент подключается только при AUTH_DEV_ISSUER и DEBUG; статический тест
«нет кодов ролей» расширен на src/api/** и src/services/**.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Без jsonable_encoder ValueError из field_validator давал 500 вместо 422;
HTTP_422_UNPROCESSABLE_ENTITY помечена устаревшей в starlette.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Пользователь, филиал, роли и права читаются одним запросом; роль is_active=false прав не даёт;
email без учёта регистра, неоднозначность — 403 (R-17).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
I-1: в jwt.decode передаётся PyJWK (а не голый ключ), рассогласование alg и типа ключа даёт 401,
а не TypeError/500; страховка на (TypeError, ValueError).
I-2: cache_keys=False — кэш ключей по kid не имеет срока жизни и держал бы отозванный ключ;
остаётся кэш набора JWKS на 300 с.
R-17: Claims.email = strip().lower().
R-18: запредельный exp не даёт 500; пустой sub отвергается; kid из заголовка пишется в лог через repr и усекается.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Dev HS256 включается только при AUTH_DEV_ISSUER и DEBUG одновременно; РАИСА — RS256/ES256 по
публичному ключу или JWKS с кэшем. PyJWT[crypto] добавлен в requirements.txt как прод-зависимость.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Двухфазный загрузчик: валидация yaml целиком до записи (SeedError), затем upsert по
натуральному ключу без удалений; run_seed(session, *, dev, seed_dir) без commit, CLI коммитит.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
PostgreSQL проверяет право CREATE на базу раньше IF NOT EXISTS: роль миграций, которой платформа выдала готовую
схему dkb без этого права, падала на CREATE SCHEMA IF NOT EXISTS. env.py (онлайн) проверяет pg_namespace и создаёт
схему только при отсутствии; офлайн-SQL и upgrade() ревизии 0001 — DO-блок с той же проверкой. Тест: схема заранее
во владении отдельной роли, CREATE на базу отозван у PUBLIC — upgrade/check/downgrade/upgrade проходят.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Схема dkb создаётся и фиксируется до context.configure (онлайн) и печатается первой в офлайн-SQL.
URL: -x dburl > sqlalchemy.url, заданный вызывающим кодом (не заглушка driver://) > настройки.
Async-движок из URL напрямую (без configparser-интерполяции %).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
I-1: табличный UniqueConstraint ORM сверяется как уникальный индекс по колонкам
(флаг колонки unique=True — по-прежнему в свойствах колонки); любое CHECK в ORM —
«отсутствует в DBML».
I-2: у совпавших целей FK сравниваются ON DELETE / ON UPDATE (нет в DBML = NO ACTION),
DEFERRABLE и INITIALLY.
I-3: неизвестный класс типа отрисовывается repr с параметрами (VARCHAR(length=50),
Uuid(as_uuid=False)) и никогда не совпадает с именем типа DBML.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>