GET /legal-entities/{id}/files (include_superseded, по убыванию uploaded_at),
GET /files/{id}, GET /documents/{id} (group_schema листа, warnings — R-6),
GET /documents/{id}/entries (limit ≤ 2000, по умолчанию 500),
POST /documents/{id}/confirm (DOCUMENT_EDIT; блокировка файла, затем документа;
409 FILE_SUPERSEDED / ALREADY_CONFIRMED / DOCUMENT_NOT_CONFIRMABLE; аудит
DOCUMENT_CONFIRM). Видимость — через ЮЛ-владельца, чужое — 404 как несуществующее.
Список файлов без N+1: файлы, текущие задания и документы — пакетными запросами.
JobBrief.mapping_error — причина ERROR файла по маппингу.
Тесты: сценарии documents-api, гонка подтверждения с автозаменой (Review Focus 5),
make_entries_from_result (R-2), реальная проверка уникальности документа (T2.2).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
Ревью T8.1: новый файл с ОСВ 60/62/66 заменяет три прежних файла с version_no 1, 3, 2 по
возрастанию id (максимум в середине порядка блокировки) → три FILE_REPLACE, version_no = 4.
Мутации `locked[0]` / `locked[-1]` вместо max тест ловит.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
`tests/test_mapping_golden.py` (integration + fixtures, рулинг R-1, tasks 3.4): все
`../fixtures/golden/expected_osv_parse/**/*.json` → файл ЮЛ в PROCESSING с заданием `done`
→ `map_file` в откатываемой транзакции с seed. Инварианты спеки на каждом документе: строк =
строк снимка без TOTAL, у строк уровня > 0 есть родитель, шесть сумм равны снимку, `account_id`
= план счетов seed для нормализованного кода, вне плана — NULL. 82 снимка, 85 документов,
13510 строк, маппинг ~4 с (бюджет 60 с); 19 кодов строк-счетов вне плана seed.
Путь теста в tasks.md 3.4 приведён к рулингу R-1 (`tests/test_mapping_golden.py`).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
`supersede_previous` вызывается в `map_file` после `PROCESSED`, в той же транзакции (D-10):
файлы того же ЮЛ (не SUPERSEDED, не сам новый) с документом тех же type/счёт/год/квартал →
SUPERSEDED, `superseded_by_file_id`, `updated_at`; новому — `version_no` = max + 1;
одно системное событие `FILE_REPLACE` на заменённый файл (user_id NULL, инициатор загрузки,
перечень документов и `confirmed=true|false` в description). Документы и строки заменённых
файлов сохраняются. Автозамены одного ЮЛ сериализуются advisory-блокировкой, заменяемые
файлы блокируются FOR UPDATE в порядке id, документы перечитываются после блокировки.
Тесты: сценарий спеки «Новая ОСВ 60 за 1 кв. 2025» (CONFIRMED и RECOGNIZED), другой
счёт/год/квартал, другое ЮЛ, идемпотентность после сбоя, цепочка v1→v2→v3, одно событие на файл.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
R-15 (design №28, находка T7.1): после повторяемой неудачи process_once возвращает False — run_mapping_loop
спит MAPPING_POLL_SECONDS перед следующей попыткой; кратковременный сбой S3/БД больше не сжигает все
MAPPING_MAX_ATTEMPTS за миллисекунды. Неповторяемая ошибка, исчерпание попыток (файл ERROR) и успешный
маппинг — по-прежнему True.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
services/result_mapping.py: pick_candidate (FOR UPDATE OF job SKIP LOCKED, файл PROCESSING и задание текущее),
проверка ключа results/{file_id}/{job_id}/ и бакета, чтение S3 вне транзакции, map_file одной транзакцией
(очистка документов/строк файла, документ на лист ОСВ, повтор счёта — предупреждение и NEEDS_CHECK первого листа,
строки пакетами по 1000, резолв контрагента COUNTERPARTY+BU/SUM (R-3), нарастающая по Q1 (D-9), group_schema,
metrics.mapping_warnings, PROCESSED через set_file_status_for_job). Неповторяемые ошибки — сразу ERROR с
metrics.mapping_error; прочие — откат и mapping_attempts, на MAPPING_MAX_ATTEMPTS — ERROR. run_mapping_loop —
третья задача ExchangeTasks при подключённом S3 (иначе предупреждение).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
CounterpartyIndex.load — два запроса по всему справочнику без policy (включая закрытые ЮЛ);
resolve — ИНН, затем точное имя, затем normalize_alias по именам и алиасам, >1 кандидата → None;
неизвестный ИНН не блокирует поиск по имени. owner_names — для counterparty_check.
Отмечены шаги Task 6 и пункт 3.1 tasks.md.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
domain/osv_mapping.py — чистые функции без БД и S3 (design D-6…D-9):
normalize_account_code / account_root (ASSUMPTION(OQ-103)), build_entries
(без TOTAL, дерево по parent_row_number, сироты, account_id по плану счетов,
EntryDraft.counterparty_id = None по R-3), document_fields (DocumentDraft),
counterparty_check, document_status (OQ-05), opening_matches (±0.01, OQ-02).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
T4.1: публикатор, переводя задание в финальный error/INVALID_REQUEST, теперь в той же транзакции (под тем же
FOR UPDATE) ставит файлу текущего задания ERROR (spec primary-files «Жизненный цикл статуса файла»); файл
чужого задания не меняется. Тест test_invalid_request_marks_job_error_and_pass_continues[current|non_current];
CommittedJobs.make_file_job принимает original_name.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
- services/file_status.py: set_file_status_for_job(session, job, status, *, now, only_from=None) и пакетный
set_file_status_for_jobs для вахтёра; переход только при current_recognition_job_id = job.id, PROCESSED и
SUPERSEDED не перезаписываются (D-5, R-5).
- inbox: ack -> файл UPLOADED->PROCESSING; error -> ERROR; done/needs_check -> UPLOADED->PROCESSING (R-4: ack мог
потеряться), из PROCESSING файл ждёт цикла маппинга; inbox не читает S3 и не создаёт документы.
- вахтёр: UPDATE recognition_job ... RETURNING id и UPDATE stored_file -> ERROR в той же транзакции;
публикатор при dlq -> файл ERROR.
- удалены ResultMapper, NoopMapper, _map_result, _MapperError и параметр mapper у handle_message; app.py без mapper.
- тесты: статусы файла (ack/error/done/needs_check/без ack/timeout/dlq), чужое задание файл не трогает (Review
Focus 1), откат статуса файла вместе с транзакцией сообщения; CommittedJobs.make_file_job/add_job/file.
- tasks.md 2.2, шаги Task 4 в плане.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
POST /api/v1/legal-entities/{id}/files (DOCUMENT_EDIT, видимость ЮЛ → 404): тип по расширению
со сверкой сигнатуры (domain/file_types.py), лимит UPLOAD_MAX_BYTES → FILE_TOO_LARGE, дедуп по
sha256 в пределах ЮЛ (кроме SUPERSEDED) с рекомендательной блокировкой (ЮЛ, sha256) до конца
транзакции запроса, s3_object → put_object → stored_file(UPLOADED) → recognition_job(queued) →
current_recognition_job_id, аудит FILE_UPLOAD. Каждый файл — SAVEPOINT: частичный успех
(201/422), отказ S3 на файле — STORAGE_UNAVAILABLE только для него; без S3 — 503.
safe_filename/CONTENT_TYPES/read_limited вынесены в common/filenames.py (импорт справочников
их переиспользует). Схемы UploadResult/FileOut/JobBrief/DocumentBrief, PrimaryFilesService и
зависимость для Task 9–10. tasks.md 2.1.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
- OsvDataType.NU после BU, DBML_VERSION = 5.15, Makefile/dbml_parity/docstring'и на data-model-v5.15.dbml;
- миграция 0003_dbml_v5_15: ADD VALUE 'NU' AFTER 'BU' + lower(app_user.email); downgrade пересоздаёт osv_data_type
(регистр email не возвращается);
- email хранится в нижнем регистре: users_admin.create_user и seed (_apply_users); с UNIQUE по строчному email гонка
создания в другом регистре теперь ловится индексом (тест test_api_constraint_races обновлён);
- тесты: enum NU, откат на шаг, lower email на миграции, seed/админка, вставка NU, фиктивная v5.16.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
Схема recognition-result и примеры result-* скопированы байт в байт из docs/contracts (v1.1);
OsvRow.extra добавлен в pydantic-модель; в тест примеров добавлен result-osv-needs-check.json.
CONTRACT_VERSION не менялся: ядро — потребитель, принимает любой MINOR 1.x, запрос не менялся.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
- reference-data-seed: матрица role_permission целиком — только в пустую
таблицу, в заполненную — лишь пары ролей/прав, вставленных тем же запуском
(сценарии: снятое право, новые роль и право, все связи сняты); `--dev` только
при DEBUG=true (код 2 до обращения к БД).
- audit-trail: требование «События входа» (LOGIN/LOGIN_FAILED, раздел SESSION)
— исключение из «той же транзакции» с обоснованием, обрезка полей, доверенные
прокси; строки в таблице событий.
- authn-authz: ссылка на аудит входа; требование «Прод-конфигурация
HTTP-поверхности» — /docs, /redoc, /openapi.json только при DEBUG, CORS по
CORS_ORIGINS, 405 на неверный метод, 404 под /api/v1, GET / без редиректа.
`openspec validate --specs`: 9 passed, 0 failed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
Формулировка уточнена в докстрингах `services/session_audit.py` и
`api/dependencies/auth.py`, README «Безопасность dev-режима» и CLAUDE.md; тест
событий входа дополнен запросом со схемой Basic.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
`*` вместе с credentials спецификация CORS запрещает (Starlette тогда отражает
Origin запроса); фронт передаёт токен заголовком Authorization, cookie не
нужны. README «Конфигурация» уточнён.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
При непустой `role_permission` вставляются пары, у которых роль или право
вставлены этим же прогоном (`_upsert` возвращает вставленные ключи): их
администратор снять не мог. Прочие пары по-прежнему не трогаются — снятые
права не возвращаются. Убрана мёртвая ветка `unchanged` (R-9): при пустой
таблице совпадений быть не может, при заполненной сверка не нужна.
Тесты: новое право и новая роль во временном seed-каталоге → повторный seed
выдаёт только их пары, снятая связь не возвращается; администратор снял все
связи → матрица заливается заново. README, CLAUDE.md, комментарий
`role_permissions.yaml` и докстринг `seed.py` уточнены.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
`ProxyHeadersMiddleware(trusted_hosts=settings.FORWARDED_ALLOW_IPS)` вместо
`["*"]`: по умолчанию `127.0.0.1`, список через запятую или `*`. Иначе любой
клиент подставлял свой адрес в `audit_event.ip_address` (в том числе в
LOGIN_FAILED). `.env.example`/`.env.test`, README «Конфигурация» и
«Безопасность dev-режима» дополнены; compose `docs/dev` не менялся.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
`clip` в `services/session_audit.py` применяется в `record_login` (общая точка
LOGIN/LOGIN_FAILED): заголовок User-Agent и email из токена присылает клиент,
без ограничения запись в журнал была бы произвольной длины. Тесты: помощник,
запись `SessionAudit` без БД и длинный User-Agent при 401 через API.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
`_apply_role_permissions` (`scripts/seed_lib/apply.py`) загружает матрицу из
`role_permissions.yaml` только при пустой `role_permission`; иначе шаг
пропускается целиком с причиной в отчёте (`TableStat.skipped_reason`,
«role_permission: пропущено — …»). Права, снятые администратором, не
возвращаются; `role` и `permission` по-прежнему upsert'ятся.
Тесты: повторный прогон — «пропущено», матрица та же; снятая связь после
повторного seed не возвращается. README «Администрирование» и раздел seed
переписаны под новое поведение.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
`_today()` в `services/legal_entities.py` берёт московскую дату
(`common/clock.py`: `moscow_date`, `MOSCOW_TZ` = Europe/Moscow; без базы
часовых поясов — фиксированный UTC+3, Москва без перехода на летнее время с
2014 г.). Тесты на границе суток: 23:30 МСК (20:30 UTC) — ещё 4-е, 00:30 МСК
(21:30 UTC того же дня) — уже 5-е, и 4-е становится прошлым. README: даты
перевода/вывода — по Москве.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
Раздел SESSION, object_type app_user, IP и user-agent запроса
(`services/session_audit.py`):
- LOGIN — выдача dev-токена (в транзакции запроса) и первый успешный
`current_user` пользователя после старта процесса (`SessionAudit.logged_in`,
в памяти процесса; параллельные первые запросы — одно событие);
- LOGIN_FAILED — 401 (токен не принят: user_id пуст, причина без токена) и 403
аутентификации (не зарегистрирован / отключён / неоднозначен: email в
description, user_id — если найден). `AuthenticationFailedError` (подкласс
HTTPException) обрабатывается после отката транзакции запроса, запись — в
отдельной транзакции `session_transaction`, поэтому остаётся в журнале.
Без заголовка Authorization и 403 «недостаточно прав» — не события входа;
ошибка записи аудита входа ответ не меняет.
Тесты: `test_login_audit.py` (в т.ч. фиксация при откате запроса на
`committed_app`); счётчики следов импорта не учитывают события SESSION.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
`api/.env.test`, `api/.env.example` и прочие env-файлы больше не попадают в
образ (как у tabular-parser и scan-gateway): настройки контейнера задаются
переменными окружения.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
Причина редиректа: `FastAPI(root_path="/")` — путь маршрута для `GET /` после
вычитания root_path пуст, монтирование SPA его не находит, и роутер Starlette
отвечал 307 на `//`. Теперь `root_path` нормализуется (`/` → "", хвостовой `/`
снимается) — `_normalize_root_path` существовал, но не применялся.
`SPAStaticFiles` для путей `api/...` сверяется с маршрутами приложения: путь
совпал, метод нет (`Match.PARTIAL`) — 405 с заголовком `Allow` и русским
`detail`; иначе — прежний 404 (dev-маршруты при выключенных флагах — 404).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
Приложение собирает `create_app(settings, web_dir=...)`: при DEBUG=false нет
`/docs`, `/redoc`, `/openapi.json` (SPA-заглушка на эти пути отвечает 404, а не
index.html); CORS — `cors_options`: DEBUG — `*` с credentials, иначе
`CORS_ORIGINS` (пусто — middleware не подключается, credentials только для
списка без `*`). Настройка `CORS_ORIGINS` читает JSON-список или строку через
запятую. `.env.example` и README «Конфигурация» дополнены.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
CLI `python -m scripts.seed --dev` сверяется с `DEBUG` (`check_dev_allowed`) до
чтения seed и подключения к БД; при `DEBUG=false` — код возврата 2 и сообщение
«--dev доступен только при DEBUG=true (dev-пользователи не для прода)».
`run_seed(session, dev=True)` защиты не имеет (её вызывают тесты).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
- M-8: CompanyGroupCreate и LegalEntityCreate отвергают неизвестные поля (422, объект не создаётся); тесты.
- T11.3: safe_filename отбрасывает и категорию Cf (U+202E, U+200B) до обрезки длины; тесты.
- M-2: README «Ошибки API» — пример с ALIAS_TAKEN, INN_EXISTS без дополнительных полей.
- M-1: README — требование к ingress по размеру тела для /directories/import.
- M-4: README — зависимость формы пользователя от DIRECTORY_VIEW, /me не перечитывается, ASSIGNMENT_CHANGE (№35).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
Реестр имя ограничения PostgreSQL -> (код, постоянный текст) в common/constraint_conflicts.py;
integrity_error_handler для unique_violation (23505) по известному ограничению отдаёт
{"detail": {"code", "message"}}, иначе прежняя общая строка. Имя берётся из полей драйвера
(asyncpg __cause__.constraint_name, psycopg diag), не из текста ошибки; resolved_code пишется в лог.
Тесты: unit обработчика/реестра (ключи есть в Base.metadata), интеграционные гонки (ГК/ЮЛ crm_id,
ИНН, алиас, email, членство, откат и следующий запрос, FK остаётся общим 409).
T15.3: README шаг 4 импорта; T15.4: test_components_have_no_duplicated_models.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2