From 7578b5fb1299e8548d1aee002ba7c256eeaa197d Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Wed, 7 Oct 2026 13:44:01 +0400 Subject: [PATCH] =?UTF-8?q?docs(arm-core):=20=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=BA=D0=B8=20README=20=D0=B8=20CLAUDE.md=20=D0=BF=D0=BE=20?= =?UTF-8?q?=D1=80=D0=B5=D0=B2=D1=8C=D1=8E=20Task=2013=20(I1=E2=80=93I3,=20?= =?UTF-8?q?=D0=BC=D0=B8=D0=BD=D0=BE=D1=80=D0=BD=D1=8B=D0=B5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2 --- CLAUDE.md | 7 +++-- README.md | 29 ++++++++++++------- .../changes/slice-3b-statements-api/tasks.md | 2 +- 3 files changed, 24 insertions(+), 14 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index e26fdc6..7c0f9ba 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -87,7 +87,8 @@ (OQ-55). `osv_entry` пишет только маппер, один раз на файл (очистка перед вставкой); `TOTAL` не хранится. - **Формы ББ/ОФР (Срез 3b)** — `map_file` (`services/result_mapping.py`) мапит и формы ББ/ОФР: `domain/statement_mapping.py` (чистые функции) + `repositories/statements.py` (`financial_statement_meta/column/line/value`); форма создаётся с тем, что есть в результате (колонки могут отсутствовать); - документ `BALANCE`/`INCOME` без `meta` (создан до Среза 3b) подтверждается без условий D-9, `…/statement` по нему — 409 + второй `BALANCE`/`INCOME` того же файла пропускается с предупреждением (OQ-77), `DISCLOSURE`/`OTHER` — один документ + без данных на тип (`RECOGNIZED`, D-3); документ `BALANCE`/`INCOME` без `meta` (создан до Среза 3b) подтверждается без условий D-9, `…/statement` по нему — 409 `DOCUMENT_NOT_STATEMENT` (решение №16). Проверки §7 — `domain/statement_checks.py` по словарю `api/rules/statement_checks.yaml`; **файл — копия словаря tabular-parser, при изменении правится в обоих репозиториях** (расхождение молчаливо меняет результат проверок). Проверки считаются на лету и не хранятся; хранится только акцепт (`checks_accepted_by/at/comment` в шапке). Правка формы и акцепт — @@ -100,7 +101,9 @@ не блокируют. Комментарии к документу — `services/documents.py` (`comments`/`add_comment`, `comment(object_type='document')`, без аудита; `comments_count` в карточках). `GET /files/{id}/content` — `services/primary_files.content` + `ObjectStorage.stream` (поток из S3, первая порция до заголовков → `FILE_CONTENT_NOT_FOUND`; сессию тело потока не держит; - имя — RFC 5987). `GET /users/brief` — `current_user` без права; `documents_count` у ЮЛ — `repositories/directories`; CSV чек-листа — + имя — RFC 5987, `X-Content-Type-Options: nosniff`; освобождение потока — `common/streaming.py`: `ClosingStreamingResponse` + + `ChainedStream` закрывают источник и при обрыве клиента). Скачивание первички, CSV и комментарии аудит не пишут (в отличие от + экспорта сводной ОСВ в xlsx — тот пишет `EXPORT`; OQ-153). `HeaderPatch.units` — непустая строка после обрезки (пробельная — 422). `GET /users/brief` — `current_user` без права; `documents_count` у ЮЛ — `repositories/directories`; CSV чек-листа — `?format=csv` у `GET …/checklist`. Контракт для фронта — `../docs/dev/api-contract-slice-3b.md`; решения — `openspec/changes/slice-3b-statements-api/design.md`. - **Консолидации (Срез 4)** — `api/endpoints/consolidations/` (periods, versions, locks, status, members, checklist, osv, export, diff --git a/README.md b/README.md index 2275c76..153004a 100644 --- a/README.md +++ b/README.md @@ -461,14 +461,17 @@ curl -s "localhost:8000/api/v1/files/" -H "Authorization: Bearer $TOKEN `PROCESSED` и `SUPERSEDED` события обмена не перезаписывают; запоздавший ack не воскрешает `ERROR`. -**Документ** (`document.status`) создаётся маппером на каждый лист ОСВ: +**Документ** (`document.status`) создаётся маппером на каждый лист ОСВ, на каждую форму ББ/ОФР (Срез 3b, D-2) и по одному на +тип `DISCLOSURE`/`OTHER` результата (D-3; без данных, сразу `RECOGNIZED`). Условия для листа ОСВ: - `RECOGNIZED` — задание `done`, проверки парсера без нарушений, шапка не противоречит владельцу (`counterparty_check ≠ MISMATCH`), предупреждений маппинга у листа нет; - `NEEDS_CHECK` — иначе: задание `needs_check`, нарушения `checks`, `counterparty_check = MISMATCH`, повтор счёта в файле, строки без родителя, нарастающая по данным Q1; +- для формы ББ/ОФР условия `RECOGNIZED`/`NEEDS_CHECK` — правило D-6 (раздел «Формы ББ/ОФР»): проверки §7 по суммам формы вместо + проверок парсера; - `CONFIRMED` — аналитик нажал «Подтвердить» (`POST /documents/{id}/confirm`, `confirmed_by/at`, аудит); -- `ERROR` — зарезервирован: в Срезе 2c ядро документ в `ERROR` не создаёт (файл без разбираемой ОСВ → файл `ERROR`). +- `ERROR` — зарезервирован: в Срезе 2c ядро документ в `ERROR` не создаёт (результат без документов — «в результате нет документов» — переводит в `ERROR` файл). `counterparty_check` (`MATCH` / `MISMATCH` / `UNKNOWN`) сверяет шапку (ИНН, иначе имя или алиас) с ЮЛ — владельцем файла; `period_check` — `UNKNOWN`: подходящесть документа версии консолидации — функция (документ, версия), а не свойство документа (Срез 4, OQ-120). Допущения помечены `ASSUMPTION(OQ-nn)` в коде. @@ -485,7 +488,7 @@ S3 (storage); без S3 — предупреждение в логе, цикл отвергаются); 2. читает объект из S3 **вне транзакции** и валидирует `RecognitionResult` v1.x (`job_id`/`file_id` должны совпасть с заданием); 3. в одной транзакции (порядок блокировок «задание → файл») удаляет прежние строки файла (идемпотентность), создаёт - `document` на каждый лист ОСВ и на каждую форму ББ/ОФР (шапка, колонки, строки, значения; см. «Формы ББ/ОФР»), пакетами вставляет `osv_entry` (без строки `TOTAL`; `parent_entry_id`, `account_root`, + `document` на каждый лист ОСВ и на каждую форму ББ/ОФР (второй `BALANCE`/`INCOME` файла пропускается с предупреждением; шапка, колонки, строки, значения; см. «Формы ББ/ОФР»), пакетами вставляет `osv_entry` (без строки `TOTAL`; `parent_entry_id`, `account_root`, `account_id`, `counterparty_id`), пишет `recognition_job.group_schema` и `metrics.mapping_warnings`, переводит файл в `PROCESSED` и выполняет автозамену (см. «Удаление и замена»). @@ -522,10 +525,10 @@ S3 (storage); без S3 — предупреждение в логе, цикл | `POST /documents/{document_id}/statement/checks/accept` | `DOCUMENT_EDIT` | акцепт непройденных жёстких проверок с комментарием (`{comment}`) | | `GET /documents/{document_id}/comments` | `DOCUMENT_VIEW` | комментарии по возрастанию `created_at`: `{id, text, author {id, full_name}, created_at}` | | `POST /documents/{document_id}/comments` | `DOCUMENT_EDIT` | добавить комментарий (`{text}`, 1–4000 символов после обрезки пробелов), 201; в любом статусе документа | -| `GET /files/{file_id}/content` | `DOCUMENT_VIEW` | исходный файл потоком из S3; `?disposition=attachment` (по умолчанию) или `inline`; имя — `Content-Disposition` по RFC 5987 | +| `GET /files/{file_id}/content` | `DOCUMENT_VIEW` | исходный файл потоком из S3; `?disposition=attachment` (по умолчанию) или `inline`; имя — `Content-Disposition` по RFC 5987, `X-Content-Type-Options: nosniff` | Карточка документа и документ в карточке файла несут `comments_count`. Просмотр и скачивание первички (`/files/{id}/content`) -и CSV чек-листа в аудит не пишутся (как экспорт сводной ОСВ). Чужой и несуществующий файл — 404 `NOT_FOUND`; файл есть, а объекта +и CSV чек-листа, как и комментарии, в аудит не пишутся (в отличие от экспорта сводной ОСВ в xlsx: он пишет `EXPORT`; OQ-153). Тело потока из S3 освобождается детерминированно (`common/streaming.py`: `ClosingStreamingResponse`, `ChainedStream`), в том числе при обрыве клиента. Чужой и несуществующий файл — 404 `NOT_FOUND`; файл есть, а объекта в S3 нет (истёк или пропал) — 404 `FILE_CONTENT_NOT_FOUND`; S3 не подключено — 503 `STORAGE_UNAVAILABLE`. Страницы — `{items, total, limit, offset}`. Суммы строк — десятичные значения без округления, как в результате парсера. @@ -539,7 +542,8 @@ S3 (storage); без S3 — предупреждение в логе, цикл ## Формы ББ/ОФР (Срез 3b) Цикл маппинга разбирает не только листы ОСВ, но и формы ББ/ОФР результата парсера: на каждую форму создаются `document` -(`BALANCE`/`INCOME`) и шапка `financial_statement_meta` с колонками, строками и значениями (`financial_statement_column`, +(`BALANCE`/`INCOME`; второй `BALANCE`/`INCOME` того же файла пропускается с предупреждением маппинга, OQ-77, D-3, а первый получает +`NEEDS_CHECK`) и шапка `financial_statement_meta` с колонками, строками и значениями (`financial_statement_column`, `_line`, `_value`). Первичка не мутируется: правка аналитика пишется рядом (`is_modified`, `original_amount`, `modified_by/at`; у полей шапки — `*_modified`, `*_original_value`). Период формы — по дате отчёта, а при её отсутствии по самой поздней дате колонок; ББ/ОФР всегда нарастающие. Статус документа формы — `RECOGNIZED` только если задание `done`, нет нарушений @@ -549,13 +553,14 @@ S3 (storage); без S3 — предупреждение в логе, цикл в обоих репозиториях). - **`GET /documents/{id}/statement`** — `{document, edition, header, columns[], lines[], checks}`: `header` — `inn`, - `report_date`, `units` вида `{value, original_value, verified, modified}` (у ИНН ещё `confidence`) и справочные поля формы + `report_date`, `units` вида `{value, original_value, confidence, verified, modified}` (`confidence` есть у ИНН, у даты и единиц — `null`) и справочные поля формы (`organization_name`, `okud`, `okpo`, …); `lines[].values[]` — суммы строкой, `is_modified`, `original_amount`; `checks` — `{failed, soft_failed, cross_form, accepted}` (нарушение — `{column_index, relation, expected, actual}`, у межформенной проверки `column_index = -1`). Не форма (ОСВ, расшифровка, ББ/ОФР без данных формы — созданные до Среза 3b) — 409 `DOCUMENT_NOT_STATEMENT`. Подтверждённая форма читается. - **`PATCH /documents/{id}/statement`** — `{header?: {inn?, report_date?, units?}, values?: [{line_id, column_id, amount}]}` - (до 5000 ячеек; `amount: null` очищает ячейку; не более двух знаков). Правится только неподтверждённая форма + (до 5000 ячеек; `amount: null` очищает ячейку; не более двух знаков; `units` — непустая строка после обрезки пробелов до 50 символов, + пробельная строка — 422, `null` — «не менять»). Правится только неподтверждённая форма (`RECOGNIZED`/`NEEDS_CHECK`): `CONFIRMED` — 409 `DOCUMENT_READONLY` (исправление — новым файлом), заменённый файл — 409 `FILE_SUPERSEDED`, статус `ERROR` — 409 `DOCUMENT_NOT_CONFIRMABLE`. Ячейка не из этой формы — 422 `UNKNOWN_CELL`, одна ячейка дважды — 422 `DUPLICATE_CELL`, ИНН не из 10 или 12 цифр — 422 `INVALID_INN`. Один запрос — одна транзакция и одно @@ -563,7 +568,7 @@ S3 (storage); без S3 — предупреждение в логе, цикл пересчитывает `counterparty_check`, даты отчёта — период документа; любая правка сбрасывает акцепт и пересчитывает статус. Запрос без изменений — 200 без аудита. Ответ — та же форма, что у `GET`. - **`POST /documents/{id}/statement/checks/accept`** — `{comment}` (1–2000 символов): акцепт нарушенных жёстких проверок - (`checks_accepted_by/at/comment`). Нарушений нет — 409 `NOTHING_TO_ACCEPT`. Акцепт сбрасывается любой правкой формы. + (`checks_accepted_by/at/comment`). Нарушений нет — 409 `NOTHING_TO_ACCEPT`. Акцепт сбрасывается любой правкой формы (в `before_value` события аудита сброса остаётся комментарий сброшенного акцепта). - **Подтверждение формы** (`POST /documents/{id}/confirm`) дополнительно требует определённый период (иначе 409 `PERIOD_REQUIRED`) и пройденные или акцептованные жёсткие проверки §7 (иначе 409 `CHECKS_NOT_PASSED`; в `detail.failures[]` — `{column_index, relation, expected, actual}`). Мягкие проверки (в т. ч. межформенная `1370 − 1370(начало года) = 2400`) @@ -599,7 +604,9 @@ S3 (storage); без S3 — предупреждение в логе, цикл - Общий размер multipart-запроса `POST /legal-entities/{id}/files` и число файлов в нём приложением не ограничены (Starlette спулит части на диск, `UPLOAD_MAX_BYTES` — лимит одного файла); лимит запроса задаётся ingress платформы. - Маппер Среза 2c разбирал только листы ОСВ (`osvDocument`); с Среза 3b он мапит и формы ББ/ОФР (раздел «Формы ББ/ОФР»). - Результаты `scan-gateway` (PDF/изображения) и документы `OTHER` по-прежнему игнорируются без предупреждения. + Документы `DISCLOSURE`/`OTHER` результата не игнорируются: на каждый тип создаётся один документ без данных (`RECOGNIZED`, + `locator` — перечень листов через запятую; OQ-23). Отдельной обработки по конвейеру (`tabular`/`image`) в маппере нет: разбирается + любой результат, проходящий схему `recognition-result`. - Корневой сегмент счёта (`account_root`) — только цифровой (`ASSUMPTION(OQ-103)`): забалансовые «МЦ.04», «ГТД» получают `account_root = NULL`; `account_id` ищется точным совпадением с планом счетов seed, субсчетов в seed неполный набор — часть строк остаётся с `account_id = NULL`. @@ -816,7 +823,7 @@ email без учёта регистра проверяет только сер | `UNSUPPORTED_FILE` | 422 | загрузка: тип не из xls/xlsx/xlsb/pdf/png/jpg, расширение не совпало с сигнатурой, пустое имя; строка `rejected[]` ответа загрузки (при импорте — файл целиком) | | `FILE_TOO_LARGE` | 422 | загрузка: файл больше `UPLOAD_MAX_BYTES`; строка `rejected[]` | | `DUPLICATE_FILE` | 422 | загрузка: тот же sha256 у файла этого ЮЛ (не `SUPERSEDED`); строка `rejected[]` с `file_id` прежнего файла | -| `STORAGE_UNAVAILABLE` | 503 / 422 | S3 не подключено — 503 на весь запрос; сбой записи одного файла — строка `rejected[]` (если отклонены все — 422) | +| `STORAGE_UNAVAILABLE` | 503 / 422 | S3 не подключено — 503 на весь запрос (и на `GET /files/{id}/content`); сбой записи одного файла — строка `rejected[]` (если отклонены все — 422) | | `ALREADY_CONFIRMED` | 409 | подтверждение: документ уже `CONFIRMED` | | `FILE_SUPERSEDED` | 409 | подтверждение: документ файла, заменённого новой версией | | `DOCUMENT_NOT_CONFIRMABLE` | 409 | подтверждение: статус документа не `RECOGNIZED`/`NEEDS_CHECK` (например, `ERROR`) | diff --git a/openspec/changes/slice-3b-statements-api/tasks.md b/openspec/changes/slice-3b-statements-api/tasks.md index 795d042..6b384eb 100644 --- a/openspec/changes/slice-3b-statements-api/tasks.md +++ b/openspec/changes/slice-3b-statements-api/tasks.md @@ -17,5 +17,5 @@ - [x] 11 `documents_count` у ЮЛ - [x] 12 Чек-лист в CSV (`GET …/checklist?format=csv`) - [x] 13 README, CLAUDE.md, tasks.md -- [ ] 14 Документы workspace (bounded-PR в docs): §7/§9, контракт `dev/api-contract-slice-3b.md`, OQ — PR docs +- [x] 14 Документы workspace (bounded-PR в docs): §7/§9, контракт `dev/api-contract-slice-3b.md`, OQ — PR docs arm-ka-docs#49 создан, ждёт ревью/слияния - [ ] 15 Завершение: слияние `origin/main`, `make check-full`, `openspec validate --strict`, финальное ревью ветки, PR arm-core