docs(arm-core): правки README и CLAUDE.md по ревью Task 13 (I1–I3, минорные)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
This commit is contained in:
Andrey Epifancev 2026-10-07 13:44:01 +04:00
parent d807380068
commit 7578b5fb12
3 changed files with 24 additions and 14 deletions

View File

@ -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,

View File

@ -461,14 +461,17 @@ curl -s "localhost:8000/api/v1/files/<file_id>" -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`) |

View File

@ -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