Andrey Epifancev f9b8a21410 docs: README/CLAUDE.md Среза 2c, tasks.md
README: разделы «Первичные документы: загрузка», «Статусы файла и документа», «Цикл маппинга», «API файлов и документов»,
«Удаление и замена», «Ограничения Среза 2c»; настройки UPLOAD_MAX_BYTES/MAPPING_POLL_SECONDS/MAPPING_MAX_ATTEMPTS; каталог
кодов ошибок файлов и документов. CLAUDE.md: модули Среза 2c, три фоновых цикла, порядок блокировок, маркер e2e.
OpenSpec: tasks.md (все [x], кроме 5.3; формулировки 1.2/2.1/3.1 по коду), дельта documents-api (job.mapping_error,
DOCUMENT_NOT_CONFIRMABLE), design.md — решения №40–50.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
2026-10-06 00:11:52 +04:00

40 KiB
Raw Blame History

Design: Срез 2c — загрузка ОСВ, цикл маппинга, документы

Context

Обследование кода 05.10.2026: загрузки нет; outbox (publish_pending) берёт задания queued через inner join stored_file/s3_object; inbox (handle_message) в одной короткой транзакции под FOR UPDATE фиксирует статус задания, регистрирует s3_object результата и вызывает ResultMapper (NoopMapper); файл результата из S3 не читается (S3Storage.get_object отдаёт сырой response); CommittedJobs.cleanup() не знает document/osv_entry; FakeStorage умеет только put_object. Контракт v1.1 в api/contracts/ (PR #13). DBML v5.14: osv_data_type без NU, document уникален по (stored_file_id, document_type, osv_account_code), stored_file.content_hash без UNIQUE. Макет: загрузка из drawer организации, тип и период не выбираются (40), удаление — иконка в строке (39). Решения владельца 05.10.2026: OQ-01 (все строки, без «Итого»), OQ-02 (квартальные, сверять по данным), OQ-05 (RECOGNIZED нужен), OQ-45 (NU хранить), автозамена при совпадении счёта и периода.

Goals / Non-Goals

Goals: сквозной путь файл → osv_entry без участия человека; inbox остаётся быстрым; повторный запуск маппинга безопасен; все статусы файла и документа объяснимы по карточке файла. Non-Goals: см. proposal.

Decisions

D-1. Маппинг — отдельный цикл, признак «ещё не смапплено» — статус файла

run_mapping_loop(session_factory, storage, settings) в lifespan рядом с публикатором и вахтёром. Кандидат: recognition_job.status IN (done, needs_check) ⋈ stored_file (status = PROCESSING, current_recognition_job_id = job.id), ORDER BY finished_at, id LIMIT 1 FOR UPDATE OF job SKIP LOCKED. Сначала короткая транзакция «взять кандидата и прочитать ключ результата», затем чтение S3 без транзакции, затем одна транзакция маппинга. Новых колонок нет (DBML не меняется ради этого). Альтернатива — маппинг в inbox: блокирует ack на время вставки тысячи строк (OQ-69). Цена ошибки: вернуть вызов маппера в inbox — одна строка.

D-2. Повторы и ошибки маппинга

Счётчик recognition_job.metrics.mapping_attempts (jsonb, как republish_count). Исключение маппинга → rollback, mapping_attempts + 1, лог; ≥ MAPPING_MAX_ATTEMPTS (3) → stored_file.status = ERROR, metrics.mapping_error. Ошибки, которые повторять бессмысленно (ключ результата вне results/{file_id}/{job_id}/, объект не найден, JSON не проходит схему v1.1, в результате нет osvDocument), → сразу ERROR с причиной. Файл в ERROR показывается с error_code/error_message задания или metrics.mapping_error.

D-3. Транзакционность и идемпотентность

Маппинг одного файла — одна транзакция: document×N, osv_entry×M (пакетная вставка insert().values([...])), recognition_job.group_schema, stored_file.status = PROCESSED, автозамена, аудит FILE_REPLACE. Перед вставкой — DELETE osv_entry/document WHERE stored_file_id = file (защита от половинчатого состояния после сбоя; строки ещё не CONFIRMED, потому что файл был PROCESSING). document по индексу (stored_file_id, document_type, osv_account_code) — второй лист с тем же счётом в документ не превращается (D-7).

D-4. Загрузка

Один эндпоинт на несколько файлов; каждый файл — своя транзакция (частичный успех: ответ {"accepted": [...], "rejected": [{"name", "code", "message"}]}). Шаги на файл: safe_filename; размер ≤ UPLOAD_MAX_BYTES (50 МиБ, как PARSE_MAX_BYTES парсера) иначе 413/код FILE_TOO_LARGE; тип по расширению, сверка с сигнатурой (zip/BIFF8/PDF/PNG/ JPEG), несовпадение → UNSUPPORTED_FILE; sha256; дедуп: stored_file того же ЮЛ с тем же content_hash и status != SUPERSEDED → DUPLICATE_FILE с file_id существующего; job_id = uuid4() заранее, ключ uploads/{file_id}/{job_id}/{имя}; S3Object (flush) → put_object → StoredFile(UPLOADED, version_no=1) → RecognitionJob(queued, pipeline по типу, request_subject, schema_version=CONTRACT_VERSION, max_attempts 3/1) → stored_file.current_recognition_job_id. Без storage (dev без S3) — 503 STORAGE_UNAVAILABLE. Аудит FILE_UPLOAD на каждый принятый файл.

D-5. Статусы файла (§9.2) и кто их ставит

Событие Кто stored_file.status
загрузка эндпоинт UPLOADED
ack сервиса (processing) inbox PROCESSING
результат error; вахтёр timeout; публикатор dlq inbox / вахтёр / публикатор ERROR
результат done/needs_check inbox без изменений (PROCESSING), подхватит цикл
маппинг успешен цикл PROCESSED
маппинг невозможен / исчерпаны повторы цикл ERROR
автозамена цикл (после маппинга нового) SUPERSEDED у прежних
Переходы — только через stored_file.current_recognition_job_id = job.id (чужое/старое задание файл не трогает).

D-6. Маппинг строк (§1.3)

domain/osv_mapping.py (чистые функции): normalize_account_code("60.1") = "60.01" (числовые сегменты → 2 цифры, третий сегмент как есть 76.07.1, буквенные сегменты без изменений 76.АВ; первый сегмент нечисловой — account_root = None, ASSUMPTION(OQ-103)); account_root = первый сегмент (001 для забалансовых); build_entries(doc: OsvDocument, chart: Mapping[str, uuid], resolve: Callable[[row], uuid | None]) -> list[EntryDraft]: пропуск TOTAL; account_code = account_code_raw строки (у не-счётов — унаследованный, парсер его уже проставляет); account_id = chart.get( normalize(account_code)); parent_entry_id через карту row_number → id (id генерируются заранее uuid4); data_type как есть (NU допустим после v5.15); суммы — Decimal из контракта без изменений; counterparty_* как пришли; is_modified = false.

D-7. Документ, предупреждения, статус (§9.3)

На osvDocument: osv_account_code = normalize(header.account_code_raw), period_*, is_cumulative по шапке, затем проверка по данным (D-9), locator. Повтор счёта в том же файле → документ не создаётся, предупреждение sheet=<locator>: счёт <код> уже есть в листе <locator первого>, первый документ → NEEDS_CHECK. Статус: RECOGNIZED, если job.status = done и checks без нарушений и counterparty_check != MISMATCH и нет предупреждений маппинга; иначе NEEDS_CHECK. Предупреждения маппинга пишутся в recognition_job.metrics.mapping_warnings (jsonb, список строк) — отдельной колонки в DBML нет, а карточке файла они нужны.

D-8. Резолв контрагента (§3 п. 4) и проверка шапки

repositories/counterparties.py:CounterpartyIndex — одним запросом на файл: legal_entity(inn, name) + все алиасы → словари inn → id, normalize_alias(name) → {id…} (имя ЮЛ нормализуется той же функцией, что алиас), alias → {id…}. Резолв строки (только COUNTERPARTY, data_type ∈ {BU, SUM}): counterparty_inn → inn; иначе точное имя (после strip) → по точному legal_entity.name; иначе normalize_alias(counterparty_name) → по нормализованным именам и алиасам; на любом шаге > 1 кандидата → None. Без policy (контрагент может быть ЮЛ другого филиала). counterparty_check: header.organization_inn есть → MATCH если равен inn владельца, иначе MISMATCH; ИНН нет → normalize_alias(header.organization_name) ∈ нормализованные имя/алиасы владельца → MATCH, иначе MISMATCH; пустая шапка → UNKNOWN. period_check = UNKNOWN (нет версии консолидации; Срез 4 пересчитает).

D-9. is_cumulative по данным (OQ-02, ASSUMPTION)

Если шапка дала Q2|Q3|Q4 с is_cumulative = false: ищется документ того же ЮЛ, того же osv_account_code, period_year, period_quarter = Q1, файл не SUPERSEDED; если у корневой строки opening_debit/credit совпадают с Q1 (±0.01) — is_cumulative = true и предупреждение период …: нарастающая по данным Q1. Нет Q1 — остаётся по тексту.

D-10. Автозамена и удаление

После PROCESSED: для каждого нового документа — stored_file того же ЮЛ, status != SUPERSEDED, id != new, с document того же document_type, osv_account_code, period_year, period_quarter → SUPERSEDED, superseded_by_file_id = new, updated_at; у нового version_no = max(version_no заменённых) + 1; аудит FILE_REPLACE (object_id старого, after_value новый, description с перечнем заменённых документов и пометкой confirmed=true, если был CONFIRMED; user_id = NULL — системное действие, инициатор загрузки в description). DELETE /files/{id} (DOCUMENT_EDIT, policy): есть CONFIRMED документ → 409 FILE_HAS_CONFIRMED_DOCUMENTS; иначе osv_entry, document, recognition_job (все попытки), stored_file удаляются, s3_object.expires_at = now (объект физически не удаляется — ретеншн OQ-22), аудит FILE_DELETE с before_value. Файл SUPERSEDED удалить можно (он ни на что не влияет). superseded_by_file_id у заменённых обнуляется при удалении нового? — нет: удаление файла, который кого-то заменил, запрещено тем же 409 с кодом FILE_SUPERSEDES_OTHERS (цепочка версий не рвётся).

D-11. API чтения

GET /legal-entities/{id}/files?include_superseded=false → Page[FileOut] (FileOut: файл, job{status, error_code, error_message, metrics.warnings, mapping_warnings}, documents[DocumentBrief]); GET /files/{id} → FileOut; GET /documents/{id} → DocumentOut (+ checks из результата? — результат в S3; документ хранит только статус и проверки, поэтому checks парсера отдаются из recognition_job.metrics — не хранятся; в DocumentOut — period_check, counterparty_check, status, group_schema по locator из job.group_schema); GET /documents/{id}/entries?limit≤2000 → Page[EntryOut] в порядке row_number (дерево восстанавливается по parent_entry_id); POST /documents/{id}/confirm → CONFIRMED, confirmed_by/at, аудит DOCUMENT_CONFIRM; из CONFIRMED повторно — 409. Видимость — через ЮЛ-владельца (legal_entities_visible), чужое → 404.

D-12. S3

S3Storage.get_bytes(key) -> bytes (чтение response.content.read() + закрытие), put_object как есть; FakeStorage получает get_bytes и objects. Чтение результата — по job.result_s3_object.key (бакет сверяется с storage.bucket).

D-13. Тесты

tests/factories.py: make_stored_file, make_job, make_document; CommittedJobs.cleanup() расширен на osv_entry/document. Unit: domain/osv_mapping.py, резолв на словарях. Integration: загрузка (типы, лимит, дубль, частичный успех, policy), статусы по ack/результату/таймауту, цикл маппинга на результате из golden-снимка rb/2024-FY/ОСВ_66 за 2024.xls (FakeStorage с JSON), повторы и ERROR, автозамена с CONFIRMED, удаление, API. fixtures: маппинг всех 82 golden-снимков (../fixtures/golden/expected_osv_parse/**) в БД — инварианты: число строк = rows без TOTAL, у каждого не-корня есть родитель, account_id заполнен у всех счетов из плана, суммы совпадают с снимком. Сквозной прогон через compose (профиль services с реальным tabular-parser) — отдельный integration-тест, помеченный e2e, запускается в make check-full при поднятом профиле (иначе skip с причиной).

Risks / Trade-offs

  • [Два цикла и inbox пишут в stored_file] → только по current_recognition_job_id; тесты гонок (вахтёр vs цикл).
  • [Удаление и повторная вставка при повторе маппинга] → файл не CONFIRMED в PROCESSING, данных пользователя нет.
  • [Резолв по имени даёт ложные совпадения у физлиц/ИП] → только точное имя или алиас, никакой нечёткости; ручное сопоставление — Срез 4.
  • [Автозамена трогает CONFIRMED] → решение владельца; предупреждение в аудите и в ответе загрузки нет (асинхронно) — видно в карточке файла.
  • [e2e-тест зависит от профиля services] → skip с причиной, не падение.

Migration Plan

0003_dbml_v5_15: ALTER TYPE osv_data_type ADD VALUE 'NU' AFTER 'BU' (downgrade — пересоздание типа, как в 0002); UPDATE app_user SET email = lower(email) (при дублях по регистру миграция падает — осознанно; downgrade данные не возвращает). HEAD в тестах миграций → 0003_dbml_v5_15.

Open Questions

Не блокируют: OQ-22 (ретеншн — expires_at при удалении), OQ-55 (аудит обмена — нет), OQ-73 (replay — нет), OQ-100/103 (допущения), OQ-109 (размер объекта до скачивания — у ядра лимит на загрузке, парсер — как есть).

Decisions made without discussion

№ Дата Решение Обоснование Цена ошибки
1 2026-10-05 D-1: цикл маппинга отделён от inbox, признак — stored_file.status = PROCESSING OQ-69; без новых колонок вернуть в inbox
2 2026-10-05 D-2: 3 попытки маппинга, счётчик в metrics; неповторяемые ошибки → ERROR сразу как republish_count числа в Settings
3 2026-10-05 D-3: перед вставкой удаляются строки/документы файла (идемпотентность) файл в PROCESSING не содержит правок нет
4 2026-10-05 D-4: частичный успех, дедуп по sha256 в пределах ЮЛ, job_id заранее для ключа S3, лимит 50 МиБ макет: несколько файлов; схема ключей 03-architecture формат ответа
5 2026-10-05 D-5: ERROR файла ставят inbox/вахтёр/публикатор при финальных ошибках задания иначе файл вечно PROCESSING нет
6 2026-10-05 D-6: нормализация кода — числовые сегменты до 2 цифр, буквенные как есть; account_root = None при нечисловом первом сегменте §1.3; OQ-103 правка функции
7 2026-10-05 D-7: предупреждения маппинга — в metrics.mapping_warnings; повтор счёта в файле → первый лист + NEEDS_CHECK колонки нет в DBML; уникальность документа DBML v5.16 с колонкой
8 2026-10-05 D-8: резолв без policy; имя ЮЛ нормализуется как алиас; period_check = UNKNOWN до Среза 4 контрагент может быть чужого филиала; версии нет пересчёт в Срезе 4
9 2026-10-05 D-9: is_cumulative по данным — только сравнение с Q1 того же года OQ-02 расширить правило
10 2026-10-05 D-10: удаление запрещено при CONFIRMED и если файл заменил другие; S3-объект получает expires_at ANL-020, OQ-22 ослабить проверку
11 2026-10-05 D-11: checks парсера в БД не хранятся, карточка документа отдаёт статус и проверки ядра DBML колонка/чтение из S3
12 2026-10-05 D-13: e2e через профиль services помечен e2e и пропускается без профиля нет CI —
13 2026-10-05 v5.15: email в нижнем регистре вместо expression-индекса lower(email) сверка ORM↔DBML не умеет expression-индексы индекс в v5.16
14 2026-10-05 R-1: golden-прогон 82 снимков — второй коммит Task 8 (tests/test_mapping_golden.py) в плане нет отдельной задачи под tasks 3.4; нужен полный map_file перенос теста
15 2026-10-05 R-2: make_entries_from_result создаётся в Task 9, не в Task 2 зависит от build_entries нет
16 2026-10-05 R-3: EntryDraft.counterparty_id, проставляется в цикле маппинга через CounterpartyIndex.resolve для COUNTERPARTY + BU/SUM; build_entries без resolve спека «Резолв контрагента» не сводилась с сигнатурой плана сигнатура на callable (D-6)
17 2026-10-05 R-4: inbox при done/needs_check переводит файл UPLOADED → PROCESSING (текущее задание) core NATS без JetStream: ack может потеряться, иначе файл с готовым результатом не попадёт в маппинг убрать ветку
18 2026-10-05 R-5: ack/результат ставят PROCESSING только из UPLOADED; ERROR/SUPERSEDED/PROCESSED не перезаписываются запоздавший ack не воскрешает файл после timeout параметр only_from
19 2026-10-05 R-6: DocumentOut.warnings — metrics.mapping_warnings задания целиком, group_schema — по locator документа в DBML нет привязки предупреждения к документу фильтрация по листу
20 2026-10-05 R-7: в Task 1 правится workspace CLAUDE.md (вне git): только версия DBML по плану Task 1 откат текста
21 2026-10-05 R-8 (Task 3): 503 STORAGE_UNAVAILABLE через DomainError ({"detail":{"code","message"}}), а не строкой HTTPException; строка остаётся только у 413 внутри read_limited (превращается в rejected[FILE_TOO_LARGE]) спека и Global Constraints плана: ошибки — DomainError; бриф противоречил формат тела 503
22 2026-10-05 R-9 (Task 3): сбой S3 на одном файле → этот файл в rejected с STORAGE_UNAVAILABLE, откатывается его SAVEPOINT, принятые остаются; если отклонены все — 422 модель частичного успеха D-4; клиент видит причину по файлу вернуть 503 при отказе всех
23 2026-10-05 R-10 (Task 3): загрузка файла — SAVEPOINT внутри транзакции запроса (не отдельная транзакция на файл, как в тексте спеки); недоменная ошибка БД валит весь запрос указано в брифе плана; атомарность ответа отдельные сессии на файл
24 2026-10-05 R-11 (Task 3): pg_advisory_xact_lock(ЮЛ, sha256) против двойного клика — дубль не проскакивает; requested_at = now у задания; size_bytes у файла; .jpeg → JPG; operation_id у маршрута (OpenAPI) гонка дедупа подтверждена тестом; FIFO outbox идёт по requested_at убрать блокировку
25 2026-10-05 R-12 (Task 5): корень листа — первая строка level == 0 без родителя (в голден-снимках ровно один; в двух пустых листах корня нет, root = None); имя шапки, пустое после normalize_alias (только ОПФ/пунктуация), без ИНН → counterparty_check = UNKNOWN; ИНН шапки сравнивается по цифрам; допуск opening_matches ±0.01 включительно по дебету и кредиту шапка без идентифицирующего имени ничего не доказывает и не должна давать ложный MISMATCH вернуть MISMATCH для пустой нормализованной шапки
26 2026-10-05 R-13: «20 строк» в плане (golden ОСВ_66 за 2024) исправлено на 9 (10 строк снимка минус TOTAL); критерий — «строк результата − 1» в снимке 10 строк нет
27 2026-10-05 R-14 (Task 6): ИНН ищется точно после normalize_inn (пробелы), мусорные значения проваливаются к имени; пустая нормализация (имя из одних ОПФ) не попадает в индекс; при единственном точном совпадении имени ЮЛ побеждает, даже если нормализованное имя неоднозначно (неоднозначность оценивается по шагам, D-8) буквальное чтение §3 п. 4 «на любом шаге > 1 → NULL» поменять приоритет шагов
28 2026-10-05 R-15 (Task 7): после повторяемой неудачи маппинга цикл спит MAPPING_POLL_SECONDS (process_once возвращает False), а не крутит попытки без паузы иначе 3 попытки за миллисекунды превращают кратковременный сбой S3/БД в ERROR всех файлов вернуть мгновенный повтор / задержка в metrics
29 2026-10-05 R-16 (Task 7): статус документа считается по предупреждениям его листа (повтор счёта, сироты, нарастающая по Q1); metrics.mapping_warnings — общий список файла предупреждение одного листа не должно ронять чистый лист другого счёта передавать весь список
30 2026-10-05 R-17 (Task 7): «нарастающая по данным Q1» даёт предупреждение маппинга, поэтому документ NEEDS_CHECK (буквально D-7/D-9: RECOGNIZED только без предупреждений) спека; неопределённая нарастающая требует взгляда аналитика исключить это предупреждение из правила статуса
31 2026-10-05 R-18 (Task 7): результат с job_id/file_id ≠ заданию — неповторяемая ошибка; ошибки S3, кроме «нет объекта», — повторяемые; нелистовые документы (BALANCE/INCOME/OTHER) игнорируются без предупреждения; сироты → предупреждение + NEEDS_CHECK; ключ с сегментами ./../пустыми отвергается защита от подмены; ОСВ-only подсрез убрать проверки
32 2026-10-05 R-19 (Task 8): автозамена берёт pg_advisory_xact_lock('supersede:{legal_entity_id}'), затем FOR UPDATE старых файлов по id, документы перечитываются после блокировки (confirmed видит зафиксированное подтверждение); документ без года/квартала в автозамене не участвует в READ COMMITTED два параллельных маппинга не видят документов друг друга и оба остались бы действующими; NULL-период не доказывает «тот же период» убрать блокировку / включить NULL-периоды
33 2026-10-05 R-20 (Task 9): в JobBrief добавлено mapping_error (из metrics.mapping_error); 409 DOCUMENT_NOT_CONFIRMABLE для статуса вне {RECOGNIZED, NEEDS_CHECK}; FILE_SUPERSEDED проверяется раньше ALREADY_CONFIRMED; документ/файл исчезли за время ожидания блокировки → 404 файл ERROR после неудачного маппинга иначе необъясним по карточке; цель среза — объяснимые статусы убрать поле/код
34 2026-10-05 R-21 (Task 9): confirm берёт блокировки «файл → документ» и перечитывает оба под FOR UPDATE; чтение файлов — методы PrimaryFilesService, документов — DocumentService; задание для DocumentOut — текущее задание файла; список файлов ORDER BY uploaded_at DESC NULLS LAST, id; у моделей нет relationship(), поэтому job/документы грузятся пачками (число запросов не зависит от числа файлов, проверено тестом) согласовано с порядком блокировок автозамены; Review Focus 5 переупорядочить блокировки
35 2026-10-05 R-22 (Task 9): test_openapi_routes отвергает лишние маршруты под /legal-entities, /files, /documents (Task 10 обязан добавить DELETE в EXPECTED_ROUTES) новый маршрут сначала должен попасть в спеку ослабить тест
36 2026-10-05 R-23 (Task 10): удаление файла блокирует все его задания (по id) → файл → документы, 409 проверяются после блокировок; файл исчез за время ожидания → 404; expires_at = now безусловно у s3_object файла и объектов результатов (объекты не разделяются между файлами); FK на document из financial_statement_meta/consolidation_document/ex_snapshot_item не чистятся (в Срезе 2c недостижимы; CONFIRMED-запрет закрывает консолидации; ББ/ОФР — очищать financial_statement_meta в Срезе 3); FILE_HAS_CONFIRMED_DOCUMENTS проверяется раньше FILE_SUPERSEDES_OTHERS порядок блокировок задание → файл общий для обмена/маппинга IntegrityError 500 в Срезе 3 без очистки
37 2026-10-05 Task 11: make test-integration = pytest -m "integration and not e2e", e2e идёт целью make e2e и отдельным шагом check-full после test-integration e2e несёт маркер integration (требование брифа); без исключения он запускался бы в check-full дважды двойной прогон e2e при поднятом профиле (секунды); без профиля — два skip
38 2026-10-05 Task 11: профиль services для парсера поднимается как -p arm-ka … up -d --build --no-deps tabular-parser (README); предусловие теста — GET localhost:8001/healthcheck = 200 и nats=ok, иначе skip «профиль services не поднят» проект arm-ka поднят из другого файла compose (worktree среза 1): без --no-deps compose мог пересоздать postgres/nats/minio при расхождении конфигурации пересоздание инфраструктуры и потеря dev-данных
39 2026-10-05 Task 11: e2e подменяет async_session_maker в src.app и src.db.db и engine в src.app (как test_exchange_lifespan_nats), MAPPING_POLL_SECONDS/OUTBOX_POLL_SECONDS = 1; ЮЛ создаётся с именем АО "Русский бисквит", ИНН 3528052718 — в шапке ОСВ ИНН нет, MATCH честный по имени (D-8) нужна временная БД вместо рабочей и быстрый прогон; шапка снимка: organization_name = АО "Русский бисквит", organization_inn = null e2e читает/пишет не ту БД (рабочая armka) либо ложный MISMATCH
40 2026-10-05 Task 1: тест гонки email (test_user_email_race_with_other_case_is_not_caught) инвертирован в ..._is_caught (409 EMAIL_EXISTS); update_user email не меняет (в UPDATE_FIELDS его нет); прежний test_downgrade_one_step переименован в test_downgrade_to_v5_13, имя отдано сценарию «Откат на один шаг» при хранении email в нижнем регистре UNIQUE ловит гонку другого регистра — прежний тест фиксировал уже снятое ограничение; сценарий спеки именует откат на один шаг тест инвертируется обратно, переименование
41 2026-10-05 Task 2: make_stored_file выводит pipeline задания из file_type; make_document(job=…) использует job только как assert job.file_id == file.id; CommittedJobs.cleanup() обнуляет current_recognition_job_id и удаляет osv_entry также по recognition_job_id; маркер e2e описан в pytest.ini у document нет FK на задание; иначе удаление задания, на которое ссылается файл, нарушает FK поправить фабрики
42 2026-10-05 Task 4: тип задания в file_status — Protocol JobRef (id, file_id); пакетная set_file_status_for_jobs для вахтёра; ERROR ставится из любого статуса, кроме PROCESSED/SUPERSEDED; updated_at меняется только при реальной смене; ack для задания не в published файл не трогает; в логах обмена поля file_processing/file_changed/files_error в dlq-пути публикатора ORM-объекта под FOR UPDATE нет, только строка выборки; все переходы файла — в одном модуле сигнатуры, одна ветка
43 2026-10-05 Task 5: opening_matches — граница ±0.01 включительно, дебет и кредит попарно, None = 0; normalize_account_code делает strip(), account_root проверяет ASCII-цифры ([0-9]+); пустая строка account_code/data_type — отсутствие (None в производных), сам account_code хранится как пришёл детали D-6/D-9, которых спека не задаёт правка функций, тесты
44 2026-10-05 Task 6: CounterpartyIndex получил 4-е поле aliases_by_entity (нужно owner_names); legal_entity.inn ключуется через normalize_inn; пустое/пробельное имя → None; owner_names исключает "" (имя ЮЛ из одних ОПФ) не делать третий запрос; пустое имя шапки не должно давать ложный MATCH поле индекса, одна строка
45 2026-10-05 Task 7: pick_candidate(session, *, max_attempts) с ORDER BY finished_at NULLS LAST, id; `process_once(..., now: Clock None); сироты (MappedSheet.orphans) → предупреждение sheet=: строки без родителя …→NEEDS_CHECK, строка сохраняется с parent_entry_id = NULL; при нескольких Q1-документах D-9 берёт последний по created_at; group_schemaзадания содержит все листы ОСВ (в т.ч. лист-повтор счёта); послеPROCESSED—session.refresh(file); Falseотset_file_status_for_jobпод блокировкой —RuntimeError` детали, не заданные спекой; Core UPDATE не синхронизирует сессию
46 2026-10-05 Task 8: description FILE_REPLACE — {document_type} {osv_account_code} {year} {quarter} ({status}), список отсортирован, uploaded_by NULL → «загрузил неизвестен»; при замене обновляются version_no и updated_at нового файла; в golden все 82 снимка мапятся для одного ЮЛ, поэтому снимки с одним счётом и периодом заменяют друг друга (инвариант проверяется по каждому файлу, статус PROCESSED или SUPERSEDED); account_id проверяется у строк, чьи коды есть в плане счетов seed формат description в спеке свободен; golden не должен зависеть от замены; 19 кодов вне seed (O-3) текст события, критерий golden
47 2026-10-05 Task 9: репозиторий файлов без joinedload/selectinload (в моделях нет relationship()) — пакетные current_jobs/documents_by_file; сортировки: файлы uploaded_at DESC NULLS LAST, id, документы document_type, osv_account_code, locator, id; DocumentOut берёт текущее задание файла; group_schema без элемента с locator документа → null, groups не список → []; нестроковые предупреждения отбрасываются; FILE_SUPERSEDED приоритетнее ALREADY_CONFIRMED; права в тестах — rka/curator_go (у CLIENT_MANAGER по seed есть оба права); make_entries_from_result импортирует приватные _entry_values/_group_schema маппера в моделях нет связей; детерминизм порядка; отсутствие дублирования вставки рефакторинг репозитория, порядок 409
48 2026-10-05 Task 10: удаление блокирует ВСЕ задания файла (ORDER BY id); expires_at ставится безусловно; удаление Core DELETE с очисткой osv_entry одним оператором; 409 FILE_HAS_CONFIRMED_DOCUMENTS проверяется раньше FILE_SUPERSEDES_OTHERS; маршрут DELETE лежит в endpoints/files/read.py; financial_statement_meta, consolidation_document, ex_snapshot_item (FK на document) не чистятся — в 2c их строк нет, к Срезам 3/4 нужно расширить удаление или запрет (OQ-110) порядок «задание → файл» для всех попыток; self-FK parent_entry_id проверяется в конце оператора теоретический deadlock; IntegrityError (500) в Срезе 3 без очистки
49 2026-10-06 Task 12: формулировки tasks.md 1.2 (make_entries_from_result — Task 9), 2.1 (upload_one), 3.1 (build_entries(doc, chart) без resolve, resolve(inn, name)) приведены к коду; в дельту documents-api добавлены job.mapping_error и 409 DOCUMENT_NOT_CONFIRMABLE (R-20, №33) с двумя сценариями спека — источник истины; код и рулинги ушли от первоначального текста tasks.md правка текста
50 2026-10-06 Task 12: статус документа ERROR описан в README как зарезервированный (ядро Среза 2c его не создаёт: файл без ОСВ → файл ERROR); «нарастающая по данным Q1 → NEEDS_CHECK» (R-17, №30) вынесена в OQ как вопрос владельцу в Срезе 2c нет сценария, где документ получает ERROR; решение R-17 меняет поведение для всех нарастающих Q2–Q4 текст README/OQ