docs(slice-4): README и CLAUDE.md по консолидации

README: раздел «Консолидация (Срез 4)» (эндпоинты, статусы, блокировка, привязка, чек-лист, свод, сопоставление, экспорт,
порядок блокировок), коды ошибок Среза 4 в «Ошибки API», раскладка. CLAUDE.md: модули консолидации, правила
(`transition`, порядок блокировок «файл → документ → advisory ГК → версии», FOR NO KEY UPDATE, Q1-эквивалентность).
design.md: решения №82–85; tasks.md: 5.2 выполнена.

Co-Authored-By: Claude Sonnet <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
This commit is contained in:
Andrey Epifancev 2026-10-06 08:02:50 +04:00
parent a5cde2adb7
commit 2c6908a6e2
5 changed files with 215 additions and 14 deletions

View File

@ -23,7 +23,10 @@
`domain/` (чистые функции по §§ правил, без БД и сессий: `policy.py` — видимость справочников §10.1, `domain/` (чистые функции по §§ правил, без БД и сессий: `policy.py` — видимость справочников §10.1,
`directory_import.py` — чтение xlsx/csv и план импорта §13, `aliases.py` — нормализация алиаса §3, `inn.py` — формат `directory_import.py` — чтение xlsx/csv и план импорта §13, `aliases.py` — нормализация алиаса §3, `inn.py` — формат
ИНН, `file_types.py` — тип файла по расширению и сигнатуре, `osv_mapping.py` — результат парсера → документ/строки ИНН, `file_types.py` — тип файла по расширению и сигнатуре, `osv_mapping.py` — результат парсера → документ/строки
ОСВ (§1.3, §1.5, §9.3); для вычислений консолидации — Срез 3+), `contract/` (модели контракта обмена), `api/scripts/` (`check_dbml_vs_orm.py`, ОСВ (§1.3, §1.5, §9.3); Срез 4 — `consolidation_periods.py` (выбор источников и приведение периодов, §2, Q1-эквивалентность ОСВ,
R-14), `consolidated_osv.py` (чистая сборка свода: склейка кварталов, пробелы, дорезолв, фильтры, §2–§3), `checklist.py`
(чек-лист комплекта, ячейки `CellStatus`); `policy.py` дополнен `IN_WORK_STATUSES`/`ensure_version_editable`, §10; для шахматки
и КФО — Срезы 5–6), `contract/` (модели контракта обмена), `api/scripts/` (`check_dbml_vs_orm.py`,
`seed.py` + `seed_lib/`, `check_infra.py`), данные seed — `api/seed/`. `seed.py` + `seed_lib/`, `check_infra.py`), данные seed — `api/seed/`.
- **DI** — ручные фабрики `Depends` в `src/api/dependencies/`; `storage`/`nats` — из `request.app.state`. **Сессия** — - **DI** — ручные фабрики `Depends` в `src/api/dependencies/`; `storage`/`nats` — из `request.app.state`. **Сессия** —
только `session: SessionDep` (`src/api/dependencies/db.py` = `Depends(get_async_session, scope="function")`): одна только `session: SessionDep` (`src/api/dependencies/db.py` = `Depends(get_async_session, scope="function")`): одна
@ -76,9 +79,32 @@
(`CounterpartyIndex` — резолв контрагента без policy). **Фоновых циклов три** (запускаются в lifespan при (`CounterpartyIndex` — резолв контрагента без policy). **Фоновых циклов три** (запускаются в lifespan при
`NATS_ENABLED`; цикл маппинга — ещё и при подключённом S3 (storage)): публикатор outbox, вахтёр сроков и цикл маппинга `NATS_ENABLED`; цикл маппинга — ещё и при подключённом S3 (storage)): публикатор outbox, вахтёр сроков и цикл маппинга
(`MAPPING_POLL_SECONDS`, `MAPPING_MAX_ATTEMPTS`); inbox результат не читает и документов не создаёт. **Порядок блокировок: задание → файл → документы** (inbox, вахтёр, публикатор, маппер, (`MAPPING_POLL_SECONDS`, `MAPPING_MAX_ATTEMPTS`); inbox результат не читает и документов не создаёт. **Порядок блокировок: задание → файл → документы** (inbox, вахтёр, публикатор, маппер,
`confirm`, `delete`) — иначе взаимоблокировки; автозамена дополнительно берёт advisory-lock по ЮЛ. Аудит: `FILE_UPLOAD`, `confirm`, `delete`; с Среза 4 далее — advisory-lock ГК → версии консолидации, см. «Консолидации») — иначе
взаимоблокировки; автозамена дополнительно берёт advisory-lock по ЮЛ. Аудит: `FILE_UPLOAD`,
`FILE_REPLACE` (системное, `user_id` пуст), `FILE_DELETE`, `DOCUMENT_CONFIRM`; смена статусов обмена и маппинга не аудируется `FILE_REPLACE` (системное, `user_id` пуст), `FILE_DELETE`, `DOCUMENT_CONFIRM`; смена статусов обмена и маппинга не аудируется
(OQ-55). `osv_entry` пишет только маппер, один раз на файл (очистка перед вставкой); `TOTAL` не хранится. (OQ-55). `osv_entry` пишет только маппер, один раз на файл (очистка перед вставкой); `TOTAL` не хранится.
- **Консолидации (Срез 4)** — `api/endpoints/consolidations/` (periods, versions, locks, status, members, checklist, osv, export,
mapping), сервисы: `consolidations` (создание, список, карточка, периметр `PUT …/members`), `consolidation_guard`
(`lock_visible_version`/`lock_editable_version`: версия `FOR UPDATE` → видимость 404 → статус «в работе» → своя блокировка),
`consolidation_locks` (взять/перехватить/снять; без истечения, OQ-119), `consolidation_status` (`transition` + ручной
`POST …/status`), `consolidation_documents` (`fits`, `bind_confirmed`, `unbind_entity`, хуки `on_document_confirmed` /
`on_documents_superseded` / `on_documents_mapped`), `consolidated_osv` (единая сборка `_build`: страница, сводка, кандидаты,
экспорт), `counterparty_mapping` (ручное сопоставление, алиас, автоприменение), `osv_export` (openpyxl, лист «Пробелы»).
**Правила:** (1) статус версии меняет ТОЛЬКО `consolidation_status.transition`; версия к вызову уже под `FOR UPDATE` у
вызывающего (иначе параллельный переход или перехват читает устаревший статус); (2) **порядок блокировок: файл → документ →
advisory-lock ГК → версии (по возрастанию `id`)**; правка версии начинается с `consolidation_version FOR UPDATE`;
`lock_versions_for_entity` блокирует надмножество версий «в работе» (ЮЛ — член ИЛИ та же ГК), создание версии берёт
per-ГК `pg_advisory_xact_lock` (R-2); (3) `lock_document`/`lock_file_documents` — `FOR NO KEY UPDATE` (R-3: вставка
`consolidation_document` берёт `FOR KEY SHARE` на документ и взаимоблокировалась бы с `FOR UPDATE`); (4) сводная ОСВ,
чек-лист и признак контрагента — чистые функции, в БД не пишутся; единственное хранимое — ручное сопоставление
(`counterparty_id` + `counterparty_match_kind` + алиас); (5) ключ замены привязки — «тип, счёт, квартал, нарастающий»
(R-1); ОСВ за Q1 равнозначна нарастающей и квартальной (R-14, Q1-эквивалентность — только для ОСВ, ББ/ОФР за Q1 версиям Q2–Q4
не подходят, R-14a); (6) «строка версии» для ручного сопоставления/автоприменения — только строки свода (R-11); (7) 404 —
код `NOT_FOUND`, различие по тексту; правки — `ensure_version_editable` (`VERSION_NOT_EDITABLE`/`LOCK_REQUIRED`/
`LOCKED_BY_OTHER` без `locked_by_name`). Деньги в ответах — строки с двумя знаками. Аудит: `CONSOLIDATION_CREATE`,
`CONSOLIDATION_STATUS_CHANGE`, `CONSOLIDATION_LOCK`, `CONSOLIDATION_LOCK_TAKEOVER`, `MEMBERSHIP_CHANGE`,
`OSV_COUNTERPARTY_MAP`, `LEGAL_ENTITY_CHANGE`, `EXPORT`; `FILE_REPLACE.unbound_version_ids`. Контракт для фронта —
`../docs/dev/api-contract-slice-4.md`; решения — `openspec/changes/slice-4-consolidation/design.md`.
- **Аудит** — `services/audit.py`, единственная точка записи `audit_event` (append-only: только `record`, в текущей транзакции; - **Аудит** — `services/audit.py`, единственная точка записи `audit_event` (append-only: только `record`, в текущей транзакции;
тест `test_audit_service.py` сканирует `src/` на UPDATE/DELETE по `AuditEvent`). В маршрутах — `RequestAudit` тест `test_audit_service.py` сканирует `src/` на UPDATE/DELETE по `AuditEvent`). В маршрутах — `RequestAudit`
(`get_request_audit`: пользователь, IP, user-agent). (`get_request_audit`: пользователь, IP, user-agent).

185
README.md
View File

@ -28,6 +28,10 @@ PostgreSQL 16, core NATS без JetStream), `web/` — фронтенд (React +
«Первичные документы: загрузка», «Статусы файла и документа», «Цикл маппинга», «API файлов и документов», «Удаление и «Первичные документы: загрузка», «Статусы файла и документа», «Цикл маппинга», «API файлов и документов», «Удаление и
замена», «Ограничения Среза 2c», «Сквозной прогон (e2e)». замена», «Ограничения Среза 2c», «Сквозной прогон (e2e)».
Срез 4 («консолидация»): схема по DBML v5.16, версии консолидации ГК за период, периметр, блокировка «взять в работу»,
статусы, привязка подтверждённых документов к версии, чек-лист, сводная ОСВ, ручное сопоставление контрагента, экспорт xlsx.
Спецификация — `openspec/changes/slice-4-consolidation/`; раздел ниже: «Консолидация (Срез 4)».
## Предварительные требования ## Предварительные требования
- **Python 3.11** (версия платформы) и **[`uv`](https://docs.astral.sh/uv/)** — `make venv` создаёт окружение и ставит - **Python 3.11** (версия платформы) и **[`uv`](https://docs.astral.sh/uv/)** — `make venv` создаёт окружение и ставит
@ -455,7 +459,7 @@ curl -s "localhost:8000/api/v1/files/<file_id>" -H "Authorization: Bearer $TOKEN
- `ERROR` — зарезервирован: в Срезе 2c ядро документ в `ERROR` не создаёт (файл без разбираемой ОСВ → файл `ERROR`). - `ERROR` — зарезервирован: в Срезе 2c ядро документ в `ERROR` не создаёт (файл без разбираемой ОСВ → файл `ERROR`).
`counterparty_check` (`MATCH` / `MISMATCH` / `UNKNOWN`) сверяет шапку (ИНН, иначе имя или алиас) с ЮЛ — владельцем файла; `counterparty_check` (`MATCH` / `MISMATCH` / `UNKNOWN`) сверяет шапку (ИНН, иначе имя или алиас) с ЮЛ — владельцем файла;
`period_check` — `UNKNOWN` до версии консолидации (Срез 4). Допущения помечены `ASSUMPTION(OQ-nn)` в коде. `period_check` — `UNKNOWN`: подходящесть документа версии консолидации — функция (документ, версия), а не свойство документа (Срез 4, OQ-120). Допущения помечены `ASSUMPTION(OQ-nn)` в коде.
## Цикл маппинга ## Цикл маппинга
@ -539,11 +543,158 @@ S3 (storage); без S3 — предупреждение в логе, цикл
- ИНН контрагента в тексте строки парсер не извлекает (OQ-100): резолв идёт по ИНН из колонки, иначе по имени и алиасу. - ИНН контрагента в тексте строки парсер не извлекает (OQ-100): резолв идёт по ИНН из колонки, иначе по имени и алиасу.
- ОСВ «нарастающая по данным Q1» (Q2–Q4 с `opening` корня, равным `opening` Q1) получает предупреждение маппинга, - ОСВ «нарастающая по данным Q1» (Q2–Q4 с `opening` корня, равным `opening` Q1) получает предупреждение маппинга,
поэтому документ остаётся `NEEDS_CHECK`: аналитик подтверждает его вручную (решение R-17, ждёт подтверждения владельца). поэтому документ остаётся `NEEDS_CHECK`: аналитик подтверждает его вручную (решение R-17, ждёт подтверждения владельца).
- `period_check` — `UNKNOWN` до Среза 4; удаление файла после консолидаций (FK на `document` из ББ/ОФР и версий) - `period_check` — `UNKNOWN` (OQ-120); к версии консолидации привязываются только `CONFIRMED` документы, а их файл удалить
в этом срезе не рассматривается; `EntryOut` не отдаёт `is_modified` (панель правки — подсрез 2d). нельзя — удаление файла с документом в версии невозможно (OQ-110, Срез 4); FK `financial_statement_meta` и `ex_snapshot_item`
— Срезы 3 и 7; `EntryOut` не отдаёт `is_modified` (панель правки — подсрез 2d).
- Два экземпляра ядра могут одновременно мапить один файл (второй проиграет блокировку и пропустит кандидата); - Два экземпляра ядра могут одновременно мапить один файл (второй проиграет блокировку и пропустит кандидата);
при уменьшении `MAPPING_MAX_ATTEMPTS` файлы с исчерпанным счётчиком остаются `PROCESSING` до ручного вмешательства. при уменьшении `MAPPING_MAX_ATTEMPTS` файлы с исчерпанным счётчиком остаются `PROCESSING` до ручного вмешательства.
## Консолидация (Срез 4)
Срез 4: версии консолидации ГК за период, периметр, блокировка «взять в работу», статусы (автоматические по событиям
документов и ручные), привязка подтверждённых документов к версии, чек-лист комплекта, сводная ОСВ (чистая функция от
версии, в БД не хранится), ручное сопоставление контрагента и экспорт в xlsx. Спецификация —
`openspec/changes/slice-4-consolidation/` (`design.md`: D-1…D-16 и таблица «Decisions made without discussion»);
контракт для фронта — `../docs/dev/api-contract-slice-4.md`; правила — `../docs/context/05-domain-rules.md` §2, §3, §9, §10.
### Эндпоинты
Все маршруты — под `/api/v1`; права — `CONSOLIDATION_VIEW` (чтение) и `CONSOLIDATION_EDIT` (правки). Порядок проверок:
право → видимость ГК (чужое и несуществующее — один 404) → статус «в работе» и своя блокировка → операция.
| Метод и путь | Право | Что делает |
|---|---|---|
| `POST /consolidations` | EDIT | создать консолидацию ГК за период и версию №1 (`INITIATED`); члены — текущие члены ГСК (≥ 2) |
| `GET /company-groups/{id}/consolidations` | VIEW | периоды ГК и их версии (`in_work_only`) |
| `GET /consolidations/versions/{id}` | VIEW | карточка версии: периметр, чек-лист по ЮЛ, история статусов, блокировка |
| `POST /consolidations/versions/{id}/lock` | EDIT | «взять в работу»; `{takeover_reason}` — перехват чужой блокировки с причиной |
| `DELETE /consolidations/versions/{id}/lock` | EDIT | снять свою блокировку (204; нет активной — тоже 204) |
| `POST /consolidations/versions/{id}/status` | EDIT | ручной переход: `CANCELLED` (с комментарием) или возврат в `INITIATED` |
| `PUT /consolidations/versions/{id}/members` | EDIT | заменить периметр (ЮЛ должны принадлежать ГК, ≥ 2) |
| `GET /consolidations/versions/{id}/checklist` | VIEW | комплект документов: ЮЛ × правила, статус ячейки |
| `GET /consolidations/versions/{id}/osv` | VIEW | страница сводной ОСВ и фильтры (`limit` ≤ 2000, по умолчанию 500) |
| `GET /consolidations/versions/{id}/osv/summary` | VIEW | `rows_total`, `unmapped_total`, `auto_matched_total`, `documents_total`, `gaps[]` |
| `GET /consolidations/versions/{id}/osv/export` | VIEW | xlsx: лист «Сводная ОСВ» и лист «Пробелы» |
| `GET /consolidations/versions/{id}/counterparty-candidates` | VIEW | кандидаты для сопоставления: `scope=group\|all`, `q`, `entry_id` |
| `POST /consolidations/versions/{id}/osv/entries/{entry_id}/counterparty` | EDIT | ручное сопоставление контрагента строки свода |
| `POST /legal-entities/{id}/files?consolidation_version_id=` | `DOCUMENT_EDIT` | загрузка в контексте версии (необязательный параметр, Срез 2c) |
Деньги в ответах — строки JSON с двумя знаками (`"100.00"`); версия любого статуса (включая `CANCELLED`) читается.
### Статусы версии
Автоматические переходы (`services/consolidation_status.transition` — единственное место смены `consolidation_version.status`;
пишет `consolidation_status_history` и `audit_event(CONSOLIDATION_STATUS_CHANGE)`):
| Событие | Переход | `changed_by` |
|---|---|---|
| загрузка с `consolidation_version_id`, принят ≥ 1 файл | `INITIATED` → `DOCUMENTS_PROCESSING` | загрузивший |
| маппинг создал документ `NEEDS_CHECK`, подходящий версии | `DOCUMENTS_PROCESSING` → `DATA_CORRECTION` | `uploaded_by` файла (нет — переход пропускается, лог) |
| подтверждение документа, привязанного к версии | `INITIATED`/`DOCUMENTS_PROCESSING` → `DATA_CORRECTION` | подтвердивший |
| `POST …/status {CANCELLED, comment}` | любой «в работе» → `CANCELLED` (снимает блокировку) | пользователь |
| `POST …/status {INITIATED}` | `DOCUMENTS_PROCESSING`/`DATA_CORRECTION` → `INITIATED` (привязки сохраняются) | пользователь |
«В работе» — `INITIATED`, `DOCUMENTS_PROCESSING`, `DATA_CORRECTION`. Остальные переходы (маршрут согласования) — Срез 7;
вручную они дают `409 TRANSITION_NOT_ALLOWED`. Загрузка и подтверждение документа блокировкой не ограничиваются.
### Блокировка
`consolidation_lock`: одна активная блокировка на версию (частичный UNIQUE `released_at IS NULL`). `POST …/lock` без тела:
свободна — `201`, своя — `200` (идемпотентно), чужая — `409 LOCKED_BY_OTHER`. С `takeover_reason` (непустая) чужая
блокировка снимается, создаётся новая (`taken_over_from`), `audit_event(CONSOLIDATION_LOCK_TAKEOVER)`. Любая правка версии
(`PUT …/members`, `POST …/status`, ручное сопоставление) требует статус «в работе» и **свою** активную блокировку.
Блокировка не истекает по времени (OQ-119); `CANCELLED` её снимает.
### Привязка документов
Версия владеет явным комплектом документов (`consolidation_document`). Документ **подходит** версии, если ЮЛ — член
периметра, `period_year` = год версии и: нарастающий (ББ/ОФР всегда) — `period_quarter` = квартал версии, квартальный —
`period_quarter` ≤ квартал версии. **Исключение для ОСВ (R-14):** документ ОСВ за `Q1` равнозначен нарастающему и
квартальному и подходит версии любого квартала года (парсер помечает ОСВ за Q1 как нарастающую); ББ/ОФР за Q1 версиям
Q2–Q4 не подходят (R-14a). Привязываются только `CONFIRMED` документы незаменённых файлов.
- **Привязка:** подтверждение документа (ко всем версиям «в работе» ГК ЮЛ), создание версии и добавление ЮЛ в периметр
(подхват: на ключ «тип, счёт, квартал, признак нарастающего» — документ с самым поздним `confirmed_at`).
- **Отвязка:** автозамена файла (привязки всех документов заменённого файла; версии — в `FILE_REPLACE.after_value.unbound_version_ids`)
и удаление ЮЛ из периметра. Версии `CANCELLED` не трогаются.
- **Файл с документом в версии удалить нельзя** (привязываются только `CONFIRMED`, а их файл удалять запрещено — OQ-110).
- `period_check` документа остаётся `UNKNOWN` — подходящесть есть функция (документ, версия), а не свойство документа (OQ-120).
### Чек-лист
`GET …/checklist` и `members[].checklist` карточки: строки — члены периметра, колонки — `required_document_rule` подхода
версии (`BASIC`: ББ, ОФР, ОСВ 01, 58, 60, 62, 66, 67, 76, 90). Статус ячейки: `CONFIRMED` (пара покрыта привязанными
документами), `NEEDS_CHECK`/`RECOGNIZED` (есть неподтверждённый подходящий), `PROCESSING` (у ЮЛ есть файл в обработке),
`ERROR` (последний файл ЮЛ в `ERROR`), `MISSING`; `missing_quarters` — пропущенные кварталы при частичном покрытии ОСВ.
`PROCESSING` ставится по всем недостающим столбцам ЮЛ, пока файл не смаплен (до маппинга счёт файла неизвестен).
### Сводная ОСВ
Чистая функция от версии (`domain/consolidated_osv.build` + `domain/consolidation_periods.select_sources`), в БД не пишется
(DECISION-5); одна и та же для страницы, сводки и экспорта (`services/consolidated_osv.py:_build`, весь свод версии в памяти —
порог для кэша OQ-121). Источники — строки привязанных ОСВ-документов (`group_type = COUNTERPARTY`, `data_type BU/SUM`).
- На пару (ЮЛ, счёт): нарастающий документ за квартал версии — как есть; иначе квартальные документы `Q1..Qn`
склеиваются по ключу (счёт, имя контрагента, ИНН): сальдо начала — из Q1, обороты — сумма, сальдо конца — из Qn,
отсутствующая в квартале строка — нули; нарастающий и квартальные вместе — нарастающий. Нет квартала — **пробел**
(`gaps[]` по паре, строк нет). Q1-эквивалентность ОСВ — см. «Привязка документов».
- Контрагент строки: хранимый `osv_entry.counterparty_id` либо резолв на лету (ИНН → имя → алиас; в БД не пишется,
`match_stored = false`); `match_kind` — `INN`/`NAME`/`ALIAS` (шаг резолва), `MANUAL`, `PROPAGATED`.
Тип контрагента: `GSK` (член периметра), `GK` (та же ГК), иначе `EXTERNAL`.
- Фильтры: `legal_entity_id`, `account_root`, `counterparty_type`, `unmapped`, `match_kind` (повторяемый), `q`
(имя — подстрока без регистра, ИНН — префикс). Порядок: имя ЮЛ, счёт (натуральный), номер строки.
- `warnings` строк — диагностика (расхождение `counterparty_id` между кварталами), не контракт.
### Сопоставление контрагента
`GET …/counterparty-candidates` — до 100 ЮЛ: `scope=group` (периметр и ГК, `q` необязателен) или `scope=all` (весь справочник,
`q` ≥ 3 символов, иначе `422 QUERY_TOO_SHORT`); `recommended` — нормализованное имя/алиас совпало с именем строки `entry_id`.
`POST …/osv/entries/{entry_id}/counterparty {legal_entity_id, move_alias}` — одна транзакция:
1. строка должна быть строкой **свода** версии (`COUNTERPARTY`, `BU`/`SUM`, документ привязан; `NU`/`ACCOUNT` — 404);
2. строка получает `counterparty_id` и `match_kind = MANUAL`; имя/ИНН из файла не меняются;
3. создаётся алиас имени строки у выбранного ЮЛ (`LEGAL_ENTITY_CHANGE`); алиас, закреплённый за другим ЮЛ, —
`409 ALIAS_TAKEN` (`owners[]`), с `move_alias = true` переносится (право `DIRECTORY_DELETE` не нужно);
4. автоприменение: строки всех ОСВ версии с тем же именем, пустым `counterparty_id` и ИНН пустым или равным — `PROPAGATED`;
5. `audit_event(OSV_COUNTERPARTY_MAP)` с `before/after` и `propagated_entry_ids`.
Ответ — `{row, propagated, alias_created, alias_moved_from}`; `row` бывает `null` (строка привязанного документа, не попавшая
в свод). Строки `osv_entry` общие у версий одного документа: сопоставление в одной версии меняет строку для всех (OQ-123).
### Экспорт
`GET …/osv/export` — xlsx: лист «Сводная ОСВ» (фильтры как у `GET …/osv`, без пагинации; суммы — числовые ячейки `#,##0.00`) и
лист «Пробелы» (по своду целиком, от фильтров не зависит). Имя — заголовок `Content-Disposition`
(`filename="svod-osv.xlsx"; filename*=UTF-8''svod-osv_<ГК>_<год>-<Qn>_v<n>.xlsx`). Текстовые ячейки пишутся как текст
(без формул из внешних строк). Пишет `audit_event(EXPORT)`.
### События аудита
`CONSOLIDATION_CREATE`, `CONSOLIDATION_STATUS_CHANGE`, `CONSOLIDATION_LOCK`, `CONSOLIDATION_LOCK_TAKEOVER`, `MEMBERSHIP_CHANGE`
(замена периметра; состав не менялся — события нет), `OSV_COUNTERPARTY_MAP`, `LEGAL_ENTITY_CHANGE` (алиасы сопоставления),
`EXPORT`; `FILE_REPLACE` дополнен `unbound_version_ids`.
### Порядок блокировок и инварианты
- Блокировки строк: **файл → документ → advisory-lock ГК → версии (по возрастанию `id`)**; правка версии начинается с
`consolidation_version FOR UPDATE`. `lock_versions_for_entity` блокирует надмножество версий «в работе» (член ИЛИ та же
ГК), создание версии берёт `pg_advisory_xact_lock` по ГК (R-2) — иначе привязка тихо теряется.
- Документы под `FOR NO KEY UPDATE` (R-3): вставка `consolidation_document` берёт `FOR KEY SHARE` на документ и конфликтует
с `FOR UPDATE`. Сопоставление: версия → advisory по алиасу → алиасы → строки `osv_entry` с тем же именем.
- Статус версии меняет только `consolidation_status.transition`, версия под `FOR UPDATE` у вызывающего.
### Ограничения Среза 4
- Маршрут согласования (`CONSOLIDATION_RF` и далее), «Добавить версию», `CONFIRM`, `EXTENDED`, гудвилл — Срезы 6–7;
шахматка и ВГО — Срез 5; ББ/ОФР в своде не участвуют; UI — отдельный исполнитель.
- Открытые вопросы — `../docs/context/07-open-questions.md`: OQ-118 (запрет загрузки/подтверждения после `DATA_CORRECTION`),
OQ-119 (истечение блокировки), OQ-120 (`period_check`), OQ-121 (порог размера свода), OQ-122 (ячейка `PROCESSING`),
OQ-123 (общие строки у версий), OQ-124 (Q1-эквивалентность ОСВ), OQ-125 (склейка Q1 версии Q1), OQ-126 (область ручного
сопоставления), OQ-127 (рекомендация кандидата), OQ-128 (неоднозначность дорезолва), OQ-129 (формулировка «1 + propagated»).
- Экспорт: числа xlsx — IEEE-754, суммы свыше ~10^13 могут потерять младшую копейку.
- Тест на реальных данных — `api/tests/test_consolidation_fixtures.py` (маркер `fixtures`): ОСВ РБ и Счастливочной за Q1–Q3 2024
→ версия Q3 → свод (1385 строк), 22 документа привязано, пробелы по остальным счетам.
## Ошибки API ## Ошибки API
Доменные ошибки (`src/common/exceptions.py`, обработчик — `src/core/exception_handlers.py`) отдаются объектом: Доменные ошибки (`src/common/exceptions.py`, обработчик — `src/core/exception_handlers.py`) отдаются объектом:
@ -571,7 +722,7 @@ email без учёта регистра проверяет только сер
| Код | HTTP | Где | | Код | HTTP | Где |
|---|---|---| |---|---|---|
| `NOT_FOUND` | 404 | объекта нет или он вне видимости | | `NOT_FOUND` | 404 | объекта нет или он вне видимости; в консолидациях различие — по тексту: «версия консолидации не найдена», «ГК не найдена», «ГСК/ГСЗ не найдена», «строка ОСВ не найдена в версии», «ЮЛ не найдено» |
| `OFFICE_NOT_FOUND` | 422 | филиал из тела запроса не существует (ГК, ЮЛ, пользователь) | | `OFFICE_NOT_FOUND` | 422 | филиал из тела запроса не существует (ГК, ЮЛ, пользователь) |
| `OFFICE_NOT_ALLOWED` | 422 | ГК можно закрепить только за своим филиалом | | `OFFICE_NOT_ALLOWED` | 422 | ГК можно закрепить только за своим филиалом |
| `INVALID_INN` | 422 | ИНН не из 10 (ЮЛ) / 12 (ИП) цифр | | `INVALID_INN` | 422 | ИНН не из 10 (ЮЛ) / 12 (ИП) цифр |
@ -602,6 +753,18 @@ email без учёта регистра проверяет только сер
| `DOCUMENT_NOT_CONFIRMABLE` | 409 | подтверждение: статус документа не `RECOGNIZED`/`NEEDS_CHECK` (например, `ERROR`) | | `DOCUMENT_NOT_CONFIRMABLE` | 409 | подтверждение: статус документа не `RECOGNIZED`/`NEEDS_CHECK` (например, `ERROR`) |
| `FILE_HAS_CONFIRMED_DOCUMENTS` | 409 | удаление файла с `CONFIRMED` документом | | `FILE_HAS_CONFIRMED_DOCUMENTS` | 409 | удаление файла с `CONFIRMED` документом |
| `FILE_SUPERSEDES_OTHERS` | 409 | удаление файла, на который ссылаются `superseded_by_file_id` других файлов | | `FILE_SUPERSEDES_OTHERS` | 409 | удаление файла, на который ссылаются `superseded_by_file_id` других файлов |
| `CONSOLIDATION_EXISTS` | 409 | консолидация этой ГК за год и квартал уже создана (в т. ч. гонка двух `POST /consolidations`) |
| `PERIMETER_TOO_SMALL` | 422 | в ГСК (при создании) или в новом периметре (`PUT …/members`) меньше двух ЮЛ; ничего не создаётся и не меняется |
| `RELATED_GROUP_MISMATCH` | 422 | ГСК принадлежит другой ГК или имеет вид `gsz` (невидимая/неизвестная ГСК — 404 `NOT_FOUND`) |
| `ENTITY_NOT_IN_GROUP` | 422 | `PUT …/members`: ЮЛ не из ГК консолидации, без ГК или несуществующее (`legal_entity_ids` — перечень); проверка раньше `PERIMETER_TOO_SMALL` |
| `ENTITY_NOT_IN_PERIMETER` | 422 | загрузка с `consolidation_version_id`: ЮЛ файла не входит в периметр версии |
| `QUERY_TOO_SHORT` | 422 | кандидаты `scope=all`: `q` короче 3 символов после обрезки |
| `COMMENT_REQUIRED` | 422 | `POST …/status` в `CANCELLED` без комментария (из одних пробелов — тоже) |
| `VERSION_NOT_EDITABLE` | 409 | версия не «в работе» (правки, загрузка с версией) либо `CANCELLED` при «взять в работу» |
| `LOCK_REQUIRED` | 409 | правка версии без своей активной блокировки |
| `LOCKED_BY_OTHER` | 409 | версию держит другой пользователь: из `POST …/lock` — `locked_by`, `locked_by_name`, `locked_at`; из правок (`ensure_version_editable`) — только `locked_by`, `locked_at`; гонка двух взятий (частичный UNIQUE) — то же 409 |
| `NOT_LOCK_OWNER` | 409 | `DELETE …/lock`: блокировка чужая (`locked_by`, `locked_at`) |
| `TRANSITION_NOT_ALLOWED` | 409 | ручной переход вне таблицы Среза 4, в т. ч. повтор того же статуса (`from_status`, `to_status`) |
Файл импорта больше `IMPORT_MAX_BYTES` — 413 со строкой в `detail`; при загрузке первичных файлов лимит `UPLOAD_MAX_BYTES` отдаётся не 413, а строкой `rejected[]` с кодом `FILE_TOO_LARGE`. Недостаточно прав — 403 строкой Файл импорта больше `IMPORT_MAX_BYTES` — 413 со строкой в `detail`; при загрузке первичных файлов лимит `UPLOAD_MAX_BYTES` отдаётся не 413, а строкой `rejected[]` с кодом `FILE_TOO_LARGE`. Недостаточно прав — 403 строкой
(`недостаточно прав: требуется <ПРАВО>`), нет токена или он неверен — 401. Под `/api/v1` SPA не отвечает: неверный (`недостаточно прав: требуется <ПРАВО>`), нет токена или он неверен — 401. Под `/api/v1` SPA не отвечает: неверный
@ -694,7 +857,8 @@ email без учёта регистра проверяет только сер
MinIO `9000`), поэтому после `make up` тесты работают «из коробки». То же относится к `make migration-cycle` и MinIO `9000`), поэтому после `make up` тесты работают «из коробки». То же относится к `make migration-cycle` и
`make check-full`: они тоже читают `.env.test` через `pytest.ini`. `make check-full`: они тоже читают `.env.test` через `pytest.ini`.
- **Реальные данные** — репозиторий `../fixtures/` (путь можно задать переменной `ARM_KA_FIXTURES`). Тесты с маркером - **Реальные данные** — репозиторий `../fixtures/` (путь можно задать переменной `ARM_KA_FIXTURES`). Тесты с маркером
`fixtures` без каталога пропускаются с понятной причиной. `fixtures` без каталога пропускаются с понятной причиной. Срез 4: `tests/test_consolidation_fixtures.py` — сквозной сценарий
на ОСВ РБ и Счастливочной (golden-снимки, без tabular-parser).
### Сквозной прогон (e2e) ### Сквозной прогон (e2e)
@ -755,7 +919,7 @@ api/
scripts/ check_dbml_vs_orm.py (ORM <-> DBML), seed.py и seed_lib/ (загрузчик справочников) scripts/ check_dbml_vs_orm.py (ORM <-> DBML), seed.py и seed_lib/ (загрузчик справочников)
src/ src/
app.py FastAPI-приложение: lifespan (S3, NATS, SMB), /healthcheck, раздача SPA app.py FastAPI-приложение: lifespan (S3, NATS, SMB), /healthcheck, раздача SPA
api/endpoints/ эндпоинты (me.py, dev_auth.py, directories/, admin/); router.py собирает /api/v1 api/endpoints/ эндпоинты (me.py, dev_auth.py, directories/, admin/, files/, consolidations/); router.py собирает /api/v1
api/dependencies/ зависимости FastAPI (current_user, проверка прав) api/dependencies/ зависимости FastAPI (current_user, проверка прав)
core/ config, auth (JWT), clients (S3), nats (подключение, publish, подписка), core/ config, auth (JWT), clients (S3), nats (подключение, publish, подписка),
logger, exception_handlers logger, exception_handlers
@ -764,10 +928,15 @@ api/
repositories/ доступ к данным repositories/ доступ к данным
schemas/ pydantic-схемы запросов и ответов schemas/ pydantic-схемы запросов и ответов
services/ бизнес-логика; primary_files (загрузка/удаление), result_mapping (цикл маппинга и автозамена), services/ бизнес-логика; primary_files (загрузка/удаление), result_mapping (цикл маппинга и автозамена),
file_status (статус файла по заданию), documents (карточки, подтверждение), recognition_exchange (обмен) file_status (статус файла по заданию), documents (карточки, подтверждение), recognition_exchange (обмен);
Срез 4 — consolidations (версии, периметр), consolidation_guard (общий пролог правок), consolidation_locks,
consolidation_status (`transition`), consolidation_documents (привязка), consolidated_osv (свод, кандидаты),
counterparty_mapping (ручное сопоставление), osv_export (xlsx)
common/ доменные ошибки (`{detail: {code, message}}`), часы common/ доменные ошибки (`{detail: {code, message}}`), часы
domain/ чистые функции по §§ правил, без БД: policy (видимость), directory_import (§13), aliases (§3), inn, domain/ чистые функции по §§ правил, без БД: policy (видимость), directory_import (§13), aliases (§3), inn,
file_types (тип файла по расширению и сигнатуре), osv_mapping (результат парсера → документы/строки, §1.3, §1.5, §9.3) file_types (тип файла по расширению и сигнатуре), osv_mapping (результат парсера → документы/строки, §1.3, §1.5, §9.3);
Срез 4 — consolidation_periods (выбор источников, §2), consolidated_osv (свод), checklist (чек-лист), policy
(`ensure_version_editable`, §10)
tests/ pytest (юнит и integration/fixtures по маркерам) tests/ pytest (юнит и integration/fixtures по маркерам)
web/ фронтенд (React 18 + TypeScript, rsbuild, antd): api/ (клиент), auth/ (вход, профиль), layouts/Admin (/admin); сборка web/dist -> api/web/ web/ фронтенд (React 18 + TypeScript, rsbuild, antd): api/ (клиент), auth/ (вход, профиль), layouts/Admin (/admin); сборка web/dist -> api/web/
setup/ бутстрап шаблона через Nexus банка (pip.conf, setup.sh) setup/ бутстрап шаблона через Nexus банка (pip.conf, setup.sh)

View File

@ -264,8 +264,8 @@
- Modify: `README.md` (раздел «Срез 4»: эндпоинты, статусы, блокировка, привязка, свод, сопоставление, экспорт, коды ошибок), `CLAUDE.md` (новые модули, порядок блокировок «файл → документ → версия», правило «переходы только через `transition`»), `openspec/changes/slice-4-consolidation/tasks.md` (все `[x]`) - Modify: `README.md` (раздел «Срез 4»: эндпоинты, статусы, блокировка, привязка, свод, сопоставление, экспорт, коды ошибок), `CLAUDE.md` (новые модули, порядок блокировок «файл → документ → версия», правило «переходы только через `transition`»), `openspec/changes/slice-4-consolidation/tasks.md` (все `[x]`)
- Modify (worktree docs): `context/05-domain-rules.md` (§2, §3, §9.1, §9.3, §9.5, §10 по proposal docs-change), `context/03-architecture.md`, `context/07-open-questions.md` (закрыть OQ-03, OQ-19, OQ-40, OQ-110 (Срез 4); добавить OQ-118…OQ-123 с текстами из design D-6/D-7/D-12/D-1/D-13/Risks), `context/09-delivery-plan.md` (Срез 4 по факту), `context/10-user-journey.md` (эндпоинты у шагов 2.1–2.6, 3.1), `dev/qa-guide.md` §7, новый `dev/api-contract-slice-4.md` (маршрут → схема → коды ошибок → экран макета; выжимка из OpenAPI `GET /openapi.json`), `openspec/changes/slice-4-consolidation/tasks.md` 2–4 → `[x]` - Modify (worktree docs): `context/05-domain-rules.md` (§2, §3, §9.1, §9.3, §9.5, §10 по proposal docs-change), `context/03-architecture.md`, `context/07-open-questions.md` (закрыть OQ-03, OQ-19, OQ-40, OQ-110 (Срез 4); добавить OQ-118…OQ-123 с текстами из design D-6/D-7/D-12/D-1/D-13/Risks), `context/09-delivery-plan.md` (Срез 4 по факту), `context/10-user-journey.md` (эндпоинты у шагов 2.1–2.6, 3.1), `dev/qa-guide.md` §7, новый `dev/api-contract-slice-4.md` (маршрут → схема → коды ошибок → экран макета; выжимка из OpenAPI `GET /openapi.json`), `openspec/changes/slice-4-consolidation/tasks.md` 2–4 → `[x]`
- [ ] **Step 1:** Проверить `GET /openapi.json` на тестовом приложении (`test_openapi_routes`) — все маршруты консолидации с `operation_id` и `responses`. - [x] **Step 1:** Проверить `GET /openapi.json` на тестовом приложении (`test_openapi_routes`) — все маршруты консолидации с `operation_id` и `responses`.
- [ ] **Step 2:** Написать документы; `make check` (линтер markdown, если есть); коммит в arm-core `docs(slice-4): README и CLAUDE.md по консолидации`; коммит в docs `docs(slice-4): правила §2/§3/§9/§10 по факту, OQ, контракт API для фронта, QA-гайд`. - [x] **Step 2:** Написать документы; `make check` (линтер markdown, если есть); коммит в arm-core `docs(slice-4): README и CLAUDE.md по консолидации`; коммит в docs `docs(slice-4): правила §2/§3/§9/§10 по факту, OQ, контракт API для фронта, QA-гайд`.
--- ---

View File

@ -266,3 +266,7 @@ legal_entity_name, account_code, missing_quarters[]}}`. Фильтры `GET …/
| 79 | 2026-10-06 | Сквозной тест на фикстурах (Task 13): ГК «Русский бисквит» сидится в Ростовском филиале, а не в «СПб РФ», как в плане | dev-пользователь `ka_rf` (seed) — сотрудник Ростовского филиала; видимость ГК и документов у РФ ограничена своим филиалом (policy), у `ka_rf` нет СПб; смысл сценария (КА РФ ведёт свою ГК) не меняется | тест не проверяет конкретное название филиала; при появлении dev-пользователя СПб РФ — заменить константу `ROSTOV` | | 79 | 2026-10-06 | Сквозной тест на фикстурах (Task 13): ГК «Русский бисквит» сидится в Ростовском филиале, а не в «СПб РФ», как в плане | dev-пользователь `ka_rf` (seed) — сотрудник Ростовского филиала; видимость ГК и документов у РФ ограничена своим филиалом (policy), у `ka_rf` нет СПб; смысл сценария (КА РФ ведёт свою ГК) не меняется | тест не проверяет конкретное название филиала; при появлении dev-пользователя СПб РФ — заменить константу `ROSTOV` |
| 80 | 2026-10-06 | Сверка сопоставления на реальных данных: `unmapped_total` падает на число СКЛЕЕННЫХ строк свода, получивших контрагента, а `1 + propagated` равно числу записей `osv_entry` с этим именем в ОСВ РБ, привязанных к версии (включая пары-пробелы 66/76, строк в своде не дающие); равенство «`unmapped_total` падает на `1 + propagated`» из прежней формулировки spec consolidated-osv «Сопоставление из плана среза» нарушается дважды: квартальные записи склеиваются в одну строку свода, а записи в парах-пробелах (счета 66/76 РБ) дают `propagated`, но строк свода не дают; верно оно только когда записи не склеены и не лежат в пробелах (нарастающие документы без пробелов) | на фикстурах квартальные записи одного контрагента склеиваются (D-2): в РБ «Юг-Финанс / ПЕКАРНЯ СЧАСТЛИВОЧНАЯ» — 18 записей, 4 строки свода по счетам 60/62; поведение продукта (propagated считает записи хранилища, summary — строки свода) согласовано с D-2/D-3, неточна формулировка spec | формулировка сценария в spec исправлена (fix round 1, M1); код не менялся | | 80 | 2026-10-06 | Сверка сопоставления на реальных данных: `unmapped_total` падает на число СКЛЕЕННЫХ строк свода, получивших контрагента, а `1 + propagated` равно числу записей `osv_entry` с этим именем в ОСВ РБ, привязанных к версии (включая пары-пробелы 66/76, строк в своде не дающие); равенство «`unmapped_total` падает на `1 + propagated`» из прежней формулировки spec consolidated-osv «Сопоставление из плана среза» нарушается дважды: квартальные записи склеиваются в одну строку свода, а записи в парах-пробелах (счета 66/76 РБ) дают `propagated`, но строк свода не дают; верно оно только когда записи не склеены и не лежат в пробелах (нарастающие документы без пробелов) | на фикстурах квартальные записи одного контрагента склеиваются (D-2): в РБ «Юг-Финанс / ПЕКАРНЯ СЧАСТЛИВОЧНАЯ» — 18 записей, 4 строки свода по счетам 60/62; поведение продукта (propagated считает записи хранилища, summary — строки свода) согласовано с D-2/D-3, неточна формулировка spec | формулировка сценария в spec исправлена (fix round 1, M1); код не менялся |
| 81 | 2026-10-06 | R-14 + R-14a: ОСВ за `Q1` (`period_quarter = Q1`) равнозначна и нарастающей, и квартальной. `consolidation_periods.effective_cumulative(is_cumulative, period_quarter)` (ложна для `Q1`, иначе `counts_as_cumulative`) используется в `select_sources` (для версии `Q2+` ОСВ за `Q1` — квартальный источник `Q1`; нарастающая за квартал версии по-прежнему побеждает; для версии `Q1` — прежнее поведение: нарастающая «как есть», квартальная — склейка, №50), в адаптере чек-листа и — только для `document_type = OSV` — в `consolidation_documents._cumulative`, то есть в `fits` (ОСВ за `Q1` подходит любой версии года при любом флаге) и в ключе замены `_key` (нарастающая и квартальная ОСВ за `Q1` одной пары — один слот, побеждает поздний `confirmed_at`, при равенстве — больший `id`). R-14a: ББ/ОФР за `Q1` — отчёт на конец квартала, не источник версий `Q2`–`Q4`: для них прежнее правило D-5 (NULL → нарастающий → только квартал версии), ключ замены не меняется | парсер помечает ОСВ за первый квартал нарастающей (период с начала года = Q1); без этого сквозной тест на фикстурах РБ давал 17 привязок из 22, пары 60/62(/76) уходили в пробел `[Q1]`, свод пуст | нарастающая и квартальная ОСВ за `Q1` одной пары вытесняют друг друга при привязке; если и ББ/ОФР за `Q1` должны подхватываться версиями `Q2`–`Q4` — снять условие `document_type = OSV` в `_cumulative` | | 81 | 2026-10-06 | R-14 + R-14a: ОСВ за `Q1` (`period_quarter = Q1`) равнозначна и нарастающей, и квартальной. `consolidation_periods.effective_cumulative(is_cumulative, period_quarter)` (ложна для `Q1`, иначе `counts_as_cumulative`) используется в `select_sources` (для версии `Q2+` ОСВ за `Q1` — квартальный источник `Q1`; нарастающая за квартал версии по-прежнему побеждает; для версии `Q1` — прежнее поведение: нарастающая «как есть», квартальная — склейка, №50), в адаптере чек-листа и — только для `document_type = OSV` — в `consolidation_documents._cumulative`, то есть в `fits` (ОСВ за `Q1` подходит любой версии года при любом флаге) и в ключе замены `_key` (нарастающая и квартальная ОСВ за `Q1` одной пары — один слот, побеждает поздний `confirmed_at`, при равенстве — больший `id`). R-14a: ББ/ОФР за `Q1` — отчёт на конец квартала, не источник версий `Q2`–`Q4`: для них прежнее правило D-5 (NULL → нарастающий → только квартал версии), ключ замены не меняется | парсер помечает ОСВ за первый квартал нарастающей (период с начала года = Q1); без этого сквозной тест на фикстурах РБ давал 17 привязок из 22, пары 60/62(/76) уходили в пробел `[Q1]`, свод пуст | нарастающая и квартальная ОСВ за `Q1` одной пары вытесняют друг друга при привязке; если и ББ/ОФР за `Q1` должны подхватываться версиями `Q2`–`Q4` — снять условие `document_type = OSV` в `_cumulative` |
| 82 | 2026-10-06 | Task 14 (документы): открытые вопросы владельцу заведены в `07-open-questions.md` как OQ-124…OQ-129 (после OQ-118…123 из docs-proposal): Q1-эквивалентность ОСВ (R-14/R-14a), склейка Q1 версии Q1 (№50), область ручного сопоставления (R-11), нечёткая рекомендация кандидата (O-3), неоднозначность дорезолва при тёзке (T11.9), формулировка сценария «1 + propagated» (R-15) | список вопросов ждёт подтверждения владельца; допущения уже реализованы в коде, поэтому вопросы — «допущение принято», а не «открыт» | если владелец решит иначе, меняется код и docs в соответствующем месте (указано в каждом OQ) |
| 83 | 2026-10-06 | OQ-03 и OQ-19 в реестре помечены закрытыми решением владельца 06.10.2026, OQ-40 — «допущение принято и реализовано» (не «закрыт»: правило банка не подтверждено аналитикой), OQ-110 — закрыт только для Среза 4 (остаток FK — Срезы 3 и 7) | docs-proposal велит закрыть OQ-03/19/110/40; формулировка по OQ-40 следует правилу CLAUDE.md «агент не закрывает вопросы сам» (допущение, а не решение) | реестр вопросов формально неточен, если владелец считает OQ-40 решённым |
| 84 | 2026-10-06 | `dev/qa-guide.md`: новый раздел 7 «Консолидация через API (Срез 4)», прежние разделы 7–10 сдвинуты на 8–11, внутренние ссылки «раздел 8» → «раздел 9» | docs-proposal называет раздел §7; внешних ссылок на номера разделов гайда в репозиториях нет (ссылаются по названию) | сторонние закладки на «раздел 8» (Сброс стенда) указывают на «Известные ограничения» |
| 85 | 2026-10-06 | Контракт API (`../docs/dev/api-contract-slice-4.md`) описывает доменные 422-коды (`COMMENT_REQUIRED`, `PERIMETER_TOO_SMALL` и др.) вручную: в OpenAPI они не объявлены (там общий 422 `HTTPValidationError`; объявлены 404/409/ErrorDetail) | менять объявления маршрутов в Task 14 нельзя (документная задача); OpenAPI проверен `test_openapi_routes` — у всех маршрутов консолидации есть `operationId` и `responses` | фронт не получит доменные 422-коды из сгенерированного клиента, пока responses не расширены (follow-up) |

View File

@ -62,5 +62,7 @@
экспорт открывается. Найденный тестом дефект (нарастающий Q1 не привязывался к версии Q3 и не считался экспорт открывается. Найденный тестом дефект (нарастающий Q1 не привязывался к версии Q3 и не считался
квартальным Q1) исправлен рулингом R-14 (design #81): документ за Q1 равнозначен нарастающему и квартальному; квартальным Q1) исправлен рулингом R-14 (design #81): документ за Q1 равнозначен нарастающему и квартальному;
xfail снят. xfail снят.
- [ ] 5.2 **Документы.** README (консолидации, статусы Среза 4, свод, сопоставление, экспорт, коды ошибок), CLAUDE.md - [x] 5.2 **Документы.** README (раздел «Консолидация (Срез 4)»: эндпоинты, статусы, блокировка, привязка, чек-лист, свод,
(новые модули, порядок блокировок), `openspec` спеки; синхронизация с docs-change (OQ, §2/§3/§9). сопоставление, экспорт, порядок блокировок; коды ошибок в «Ошибки API»), CLAUDE.md (новые модули, правила: `transition`,
порядок блокировок «файл → документ → advisory ГК → версии», `FOR NO KEY UPDATE`, Q1-эквивалентность), синхронизация с
docs-change (§2/§3/§9/§10, OQ-03/19/40/110 закрыты, OQ-118…129, контракт API, QA-гайд). `openspec/specs/` и архивация — человеку.