Срез 2c: загрузка файлов ОСВ, цикл маппинга результата в document/osv_entry, API документов, DBML v5.15 #15

Merged
aep merged 35 commits from feat/14-slice-2c-osv-ingest into main 2026-10-06 00:56:15 +03:00
Owner

Closes #14. Спека и реализация подсреза 2c (решения владельца 05.10.2026: автозамена при совпадении счёта/периода; маппинг отдельным циклом). 35 коммитов: 12 задач плана, финальное ревью (opus) с fix-wave, индексы по I-1.

Что в PR

  • DBML v5.15 (миграция 0003_dbml_v5_15): osv_data_type + NU, app_user.email в нижнем регистре (данные приводятся миграцией; нормализация в seed и admin API), индексы osv_entry(parent_entry_id), osv_entry(recognition_job_id), stored_file(current_recognition_job_id) (удаление документа 1000 строк: 8,6 с → 16 мс).
  • Загрузка POST /legal-entities/{id}/files (multipart, несколько файлов, частичный успех, тип по расширению и сигнатуре, лимит UPLOAD_MAX_BYTES 50 МиБ, дедуп по sha256 в пределах ЮЛ под advisory-lock, s3_object + stored_file(UPLOADED) + recognition_job(queued) в outbox, аудит FILE_UPLOAD).
  • Статусы файла в обмене (services/file_status.py, единственная точка): ack → PROCESSING; error/timeout/dlq/INVALID_REQUEST → ERROR; ResultMapper/NoopMapper удалены, inbox не читает S3.
  • Цикл маппинга (services/result_mapping.py, третий фоновый цикл lifespan): кандидат по stored_file.status = PROCESSING + current_recognition_job_id, проверка ключа результата (OQ-70), чтение S3 вне транзакции, одна транзакция на файл (очистка → document×N → пакетная вставка osv_entry → group_schema → PROCESSED → автозамена FILE_REPLACE), повторы MAPPING_MAX_ATTEMPTS с паузой, категории ошибок в metrics.mapping_error.
  • Маппинг (domain/osv_mapping.py, repositories/counterparties.py): все строки кроме TOTAL, нормализация кода счёта, account_root/account_id, резолв контрагента ИНН → имя → алиас (неоднозначность → NULL), counterparty_check, period_check = UNKNOWN (до Среза 4), is_cumulative по Q1, статус RECOGNIZED/NEEDS_CHECK, повтор счёта в файле → предупреждение + NEEDS_CHECK.
  • API: список файлов ЮЛ и карточка, документ, строки (до 2000 на страницу), POST /documents/{id}/confirm, DELETE /files/{id} (запреты FILE_HAS_CONFIRMED_DOCUMENTS, FILE_SUPERSEDES_OTHERS; s3_object.expires_at).
  • Тесты: unit 901, integration 445 (включая golden-прогон 82 снимков парсера: 85 документов, 13 510 строк, ~4 с; маппинг ОСВ_60 за 2024 1033 строки — 0,32 с), e2e через compose с реальным tabular-parser (make e2e, прошёл: ОСВ_66 → PROCESSED, RECOGNIZED; без профиля services — skip). Ledger — docs/superpowers/plans/2026-10-05-slice-2c-osv-ingest-ledger.md.

Проверки (HEAD 6ccb991)

make check-full EXIT 0: ruff/format/mypy чисто; unit 901 passed; dbml-check пусто; check-infra ok; integration 445 passed (e2e skipped без профиля); docker-build ok; web-check чисто. Финальное ревью (opus): Critical нет; I-1 закрыт индексами в v5.15; M-1…M-4 исправлены.

Rulings I made (design.md №14–53, ключевые)

  • R-4: при результате done/needs_check inbox переводит файл UPLOADED → PROCESSING (ack мог потеряться без JetStream); R-5: PROCESSING только из UPLOADED.
  • R-10/R-11: загрузка файла — SAVEPOINT в транзакции запроса; дедуп под pg_advisory_xact_lock(ЮЛ, sha256).
  • R-14: ИНН точно после нормализации; единственное точное совпадение имени побеждает неоднозначность нормализованного.
  • R-15: после повторяемой ошибки маппинга — пауза MAPPING_POLL_SECONDS (иначе 3 попытки за миллисекунды).
  • R-17 (вопрос владельцу, OQ-116): документ Q2–Q4, признанный нарастающим по данным Q1, получает предупреждение и потому NEEDS_CHECK.
  • R-18: job_id/file_id результата ≠ заданию — неповторяемая ошибка; листы ББ/ОФР игнорируются; строки-сироты → NEEDS_CHECK.
  • R-19/R-21/R-23: порядок блокировок задание → файл → документы везде; автозамена под advisory-lock по ЮЛ; confirm перечитывает файл и документ под FOR UPDATE.
  • R-24/№53: индексы добавлены в v5.15 (v5.16 не понадобился).

Вопросы владельцу (в OQ docs, не блокируют)

  • OQ-116 — NEEDS_CHECK для «нарастающей по данным» (R-17).
  • OQ-115 — новый файл с частью документов прежнего заменяет прежний целиком.
  • OQ-113 — в seed плана счетов нет 19 субсчетов из реальных ОСВ (01.01, 66.01, 76.01, 60.АВ…) → account_id = NULL.

Deferred (follow-up issue при закрытии)

T2.3, T3.1–T3.6, T4.3–T4.5, T5.2, T5.3, T6.2, T6.3, T6.5, T7.3–T7.5, T7.8–T7.13, T8.2, T8.3, T8.5, T8.7, T9.1, T9.2, T9.4–T9.7, T10.1–T10.5, T11.3, T11.5, T11.6; OQ-110…112, 114. Формулировки — в ledger.

Зависит от: RSHB/arm-ka-docs#17 (DBML v5.15). Порядок merge: docs → arm-core.

🤖 Generated with Claude Code

https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2

Closes #14. Спека и реализация подсреза 2c (решения владельца 05.10.2026: автозамена при совпадении счёта/периода; маппинг отдельным циклом). 35 коммитов: 12 задач плана, финальное ревью (opus) с fix-wave, индексы по I-1. ## Что в PR - **DBML v5.15** (миграция `0003_dbml_v5_15`): `osv_data_type + NU`, `app_user.email` в нижнем регистре (данные приводятся миграцией; нормализация в seed и admin API), индексы `osv_entry(parent_entry_id)`, `osv_entry(recognition_job_id)`, `stored_file(current_recognition_job_id)` (удаление документа 1000 строк: 8,6 с → 16 мс). - **Загрузка** `POST /legal-entities/{id}/files` (multipart, несколько файлов, частичный успех, тип по расширению и сигнатуре, лимит `UPLOAD_MAX_BYTES` 50 МиБ, дедуп по sha256 в пределах ЮЛ под advisory-lock, `s3_object` + `stored_file(UPLOADED)` + `recognition_job(queued)` в outbox, аудит `FILE_UPLOAD`). - **Статусы файла в обмене** (`services/file_status.py`, единственная точка): ack → `PROCESSING`; `error`/`timeout`/`dlq`/`INVALID_REQUEST` → `ERROR`; `ResultMapper`/`NoopMapper` удалены, inbox не читает S3. - **Цикл маппинга** (`services/result_mapping.py`, третий фоновый цикл lifespan): кандидат по `stored_file.status = PROCESSING` + `current_recognition_job_id`, проверка ключа результата (OQ-70), чтение S3 вне транзакции, одна транзакция на файл (очистка → `document`×N → пакетная вставка `osv_entry` → `group_schema` → `PROCESSED` → автозамена `FILE_REPLACE`), повторы `MAPPING_MAX_ATTEMPTS` с паузой, категории ошибок в `metrics.mapping_error`. - **Маппинг** (`domain/osv_mapping.py`, `repositories/counterparties.py`): все строки кроме `TOTAL`, нормализация кода счёта, `account_root`/`account_id`, резолв контрагента ИНН → имя → алиас (неоднозначность → NULL), `counterparty_check`, `period_check = UNKNOWN` (до Среза 4), `is_cumulative` по Q1, статус `RECOGNIZED`/`NEEDS_CHECK`, повтор счёта в файле → предупреждение + `NEEDS_CHECK`. - **API**: список файлов ЮЛ и карточка, документ, строки (до 2000 на страницу), `POST /documents/{id}/confirm`, `DELETE /files/{id}` (запреты `FILE_HAS_CONFIRMED_DOCUMENTS`, `FILE_SUPERSEDES_OTHERS`; `s3_object.expires_at`). - **Тесты**: unit 901, integration 445 (включая golden-прогон 82 снимков парсера: 85 документов, 13 510 строк, ~4 с; маппинг `ОСВ_60 за 2024` 1033 строки — 0,32 с), e2e через compose с реальным `tabular-parser` (`make e2e`, прошёл: `ОСВ_66` → `PROCESSED`, `RECOGNIZED`; без профиля `services` — skip). Ledger — `docs/superpowers/plans/2026-10-05-slice-2c-osv-ingest-ledger.md`. ## Проверки (HEAD 6ccb991) `make check-full` EXIT 0: ruff/format/mypy чисто; unit **901 passed**; dbml-check пусто; check-infra ok; integration **445 passed** (e2e skipped без профиля); docker-build ok; web-check чисто. Финальное ревью (opus): Critical нет; I-1 закрыт индексами в v5.15; M-1…M-4 исправлены. ## Rulings I made (design.md №14–53, ключевые) - R-4: при результате `done`/`needs_check` inbox переводит файл `UPLOADED → PROCESSING` (ack мог потеряться без JetStream); R-5: `PROCESSING` только из `UPLOADED`. - R-10/R-11: загрузка файла — SAVEPOINT в транзакции запроса; дедуп под `pg_advisory_xact_lock(ЮЛ, sha256)`. - R-14: ИНН точно после нормализации; единственное точное совпадение имени побеждает неоднозначность нормализованного. - R-15: после повторяемой ошибки маппинга — пауза `MAPPING_POLL_SECONDS` (иначе 3 попытки за миллисекунды). - **R-17 (вопрос владельцу, OQ-116):** документ Q2–Q4, признанный нарастающим по данным Q1, получает предупреждение и потому `NEEDS_CHECK`. - R-18: `job_id`/`file_id` результата ≠ заданию — неповторяемая ошибка; листы ББ/ОФР игнорируются; строки-сироты → `NEEDS_CHECK`. - R-19/R-21/R-23: порядок блокировок задание → файл → документы везде; автозамена под advisory-lock по ЮЛ; `confirm` перечитывает файл и документ под `FOR UPDATE`. - R-24/№53: индексы добавлены в v5.15 (v5.16 не понадобился). ## Вопросы владельцу (в OQ docs, не блокируют) - OQ-116 — `NEEDS_CHECK` для «нарастающей по данным» (R-17). - OQ-115 — новый файл с частью документов прежнего заменяет прежний целиком. - OQ-113 — в seed плана счетов нет 19 субсчетов из реальных ОСВ (`01.01`, `66.01`, `76.01`, `60.АВ`…) → `account_id = NULL`. ## Deferred (follow-up issue при закрытии) T2.3, T3.1–T3.6, T4.3–T4.5, T5.2, T5.3, T6.2, T6.3, T6.5, T7.3–T7.5, T7.8–T7.13, T8.2, T8.3, T8.5, T8.7, T9.1, T9.2, T9.4–T9.7, T10.1–T10.5, T11.3, T11.5, T11.6; OQ-110…112, 114. Формулировки — в ledger. Зависит от: RSHB/arm-ka-docs#17 (DBML v5.15). Порядок merge: docs → arm-core. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
aep added 1 commit 2026-10-05 20:09:32 +03:00
proposal, design (D-1…D-13), спеки primary-files, osv-result-mapping, documents-api (новые), recognition-job-exchange,
data-model (MODIFIED), audit-trail (ADDED), tasks (13 задач).

Issue: RSHB/arm-core#14

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
aep added 1 commit 2026-10-05 20:14:35 +03:00
aep changed title from Срез 2c: загрузка файлов ОСВ, цикл маппинга результата, документы и строки — спека OpenSpec to Срез 2c: загрузка файлов ОСВ, цикл маппинга результата в document/osv_entry, API документов, DBML v5.15 2026-10-06 00:00:51 +03:00
aep added 33 commits 2026-10-06 00:00:53 +03:00
Co-Authored-By: Claude Sonnet 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
Co-Authored-By: Claude Sonnet 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
Co-Authored-By: Claude Sonnet 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
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
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
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
Co-Authored-By: Claude Sonnet 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
Co-Authored-By: Claude Sonnet 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
Co-Authored-By: Claude Sonnet 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
`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
`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
Co-Authored-By: Claude Sonnet 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
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
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
DELETE /files/{file_id} (DOCUMENT_EDIT, policy): блокировки «задание → файл → документы», проверки после
блокировок (409 FILE_HAS_CONFIRMED_DOCUMENTS, FILE_SUPERSEDES_OTHERS), удаление osv_entry/document/recognition_job/
stored_file (разрыв FK-цикла), expires_at у объектов S3 файла и результатов, аудит FILE_DELETE с before_value.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
Тест tests/e2e (маркеры integration, e2e, fixtures): полное приложение src.app.app с lifespan на временной БД,
реальные NATS и MinIO, tabular-parser из профиля services; ОСВ 66 за 2024 -> PROCESSED, документ 66/2024/Q4,
9 строк, counterparty_check = MATCH, status = RECOGNIZED; без профиля — skip. Makefile: цель e2e, check-full
включает e2e, test-integration исключает e2e. README: раздел «Сквозной прогон (e2e)».

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
e2e подписывается на реальный recognition.result в queue group arm-core: другой экземпляр ядра (контейнер arm-core
профиля services, локальный сервер с NATS_ENABLED=true) заберёт часть ack/результатов. README и docstring теста
требуют остановить такие экземпляры; комментарий: MATCH достигается по имени (ИНН в шапке нет).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
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
Негативное требование спеки audit-trail: ack, результат, маппинг и таймаут вахтёра не создают строк audit_event.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
M-1 (= T7.6): metrics.mapping_error виден пользователю (JobBrief.mapping_error) —
ошибка схемы даёт «результат не соответствует схеме recognition-result: ошибок N,
первая: <loc> <type>» без input_value и ссылок pydantic; исчерпавшая попытки
ошибка БД/S3 — «маппинг не удался: ошибка БД/S3 (<ExcType>), попыток N» без
[SQL: …] и параметров. Полный текст исключения — только в логе.

T7.2: черновики osv_entry сортируются устойчиво по level перед вставкой пачками —
родитель раньше потомка на любой границе пачки, независимо от порядка строк
результата.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
M-3: s3_object.expires_at = now ставится только объектам с ролью uploads и ключом
uploads/{file_id}/… или с ролью results и ключом results/{file_id}/…; ссылка
задания на объект другого файла (inbox переиспользует s3_object при конфликте
(bucket, key)) не трогается и ошибкой не считается.

Фабрика make_stored_file кладёт загрузку в папку файла uploads/{file_id}/…, как
upload_key в проде.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
- .env.example: UPLOAD_MAX_BYTES — больший файл уходит в rejected[] с кодом
  FILE_TOO_LARGE, ответ 201/422, а не 413 (M-2).
- README «Ограничения»: размер multipart-запроса и число файлов приложением не
  ограничены — лимит ingress платформы (M-4); индексы osv_entry — OQ-117 (I-1);
  S3 — OQ-22 и OQ-111 (T12.3); автозамена частью документов — OQ-115 (T12.2);
  удаление ставит expires_at только объектам папок файла (M-3).
- README/CLAUDE.md: цикл маппинга запускается при NATS_ENABLED и подключённом S3;
  строка NATS_ENABLED в таблице настроек перечисляет цикл маппинга (T12.4).
- tasks.md 3.1: сигнатура counterparty_check(header, owner_inn,
  owner_names_normalized) (T12.5).
- design.md: №51 (R-24, I-1 — решение человека, OQ-117), №52 (R-25, fix wave
  M-1/M-3/T7.2).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
- test_api_documents: тавтология по counterparty_name строки 18 ОСВ_66 заменена
  значениями снимка (T9.3).
- test_mapping_golden: план счетов не пуст и есть строки с account_id (T8.6).
- e2e: код ответа DELETE проверяется после try/finally — не маскирует причину
  падения (T11.2).
- test_counterparty_resolve: единственное точное имя побеждает неоднозначность
  нормализованного (T6.1).
- test_domain_osv_mapping: шапка «ООО» без ИНН → UNKNOWN (T5.1).
- seed_lib/apply: email пользователя нормализуется один раз для upsert и ролей,
  поведение не меняется (T1.1).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
- ORM: ix_osv_entry_parent_entry_id, ix_osv_entry_recognition_job_id (models/osv.py),
  ix_stored_file_current_recognition_job_id (models/files.py) — по DBML v5.15 п. 4.
- Миграция 0003_dbml_v5_15: create_index в upgrade, drop_index в downgrade.
- tests/test_migrations.py: индексы есть после upgrade head (btree, не unique), исчезают
  после downgrade на 0002 и возвращаются при повторном upgrade.
- openspec change: design.md решение №53 (I-1 закрыт в v5.15), Migration Plan; дельта
  data-model — 0003 создаёт индексы, откат на шаг их удаляет.
- README «Ограничения Среза 2c»: убран пункт об отсутствии индексов.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
aep merged commit 54630ee3cc into main 2026-10-06 00:56:15 +03:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: RSHB/arm-core#15
No description provided.