feat(slice-4): консолидация — версии, периметр, блокировка, привязка документов, сводная ОСВ, экспорт xlsx #19

Merged
aep merged 27 commits from feat/18-slice-4-consolidation into main 2026-10-06 09:38:38 +03:00
Owner

Срез 4 — консолидация: версии, периметр, блокировка, привязка документов, сводная ОСВ, экспорт xlsx. Closes #18.

Порядок мержа: сначала docs PR RSHB/arm-ka-docs#20 (DBML v5.16), затем этот: Makefile ссылается на data-model-v5.16.dbml, make dbml-check в основном checkout упадёт, пока docs не смержен.

Что внутри

  • DBML v5.16: osv_entry.counterparty_match_kind (INN/NAME/ALIAS/MANUAL/PROPAGATED), миграция 0004 с backfill; маппинг пишет шаг резолва.
  • POST /consolidations, список по ГК с деревом версий, карточка версии, PUT …/members, POST/DELETE …/lock с перехватом по причине, POST …/status (CANCELLED, возврат в INITIATED), GET …/checklist.
  • Привязка consolidation_document при подтверждении, подхват при создании версии и расширении периметра, отвязка при автозамене; переходы INITIATED → DOCUMENTS_PROCESSING (загрузка с consolidation_version_id) → DATA_CORRECTION (NEEDS_CHECK при маппинге, подтверждение).
  • Сводная ОСВ как чистая функция (domain/consolidated_osv.py): выбор источников, склейка кварталов, пробелы, признак ГСК/ГК/Внешний, дорезолв на чтение; GET …/osv, GET …/osv/summary, GET …/counterparty-candidates, POST …/osv/entries/{id}/counterparty (алиас, перенос, автоприменение), GET …/osv/export (xlsx, два листа).
  • Сквозной тест на фикстурах РБ за Q3 2024: 22 документа, 1385 строк свода, пробелы по 01/58.03/66/67/76 РБ и 01/02 Счастливочной.
  • Спека openspec/changes/slice-4-consolidation/ (design #1–#85), ledger docs/superpowers/plans/2026-10-06-slice-4-consolidation-ledger.md.

Прогоны

make check-full на 98922cc — EXIT 0: unit 1106, integration 670, fixtures 14 passed + 1 skipped (e2e без профиля services), dbml-check v5.16, docker build, web-check. На HEAD 42c2531: unit 1106, integration 677. Финальное ревью (opus): Critical нет, Important I-1 исправлена (42c2531).

Rulings I made

Рулинг Решение Обоснование Цена ошибки
R-1 (#11) Ключ замены привязки включает признак нарастающего спека противоречила сценарию «нарастающий рядом с квартальными» меняется только ключ
R-2 (#12) lock_versions_for_entity блокирует надмножество версий; создание версии берёт advisory-lock по ГК; порядок файл → документ → advisory ГК → версии иначе подтверждение ∥ периметр и автозамена ∥ подхват теряют привязку редкая тихая потеря привязки
R-3 (#13) lock_document/lock_file_documents → FOR NO KEY UPDATE вставка привязки берёт KEY SHARE, FOR UPDATE давал взаимоблокировку нулевая
R-4 (#14) Resolution в domain/osv_mapping.py домен не импортирует репозитории нулевая
R-5 (#15) EntityFilesState — Task 2, unconfirmed_documents — Task 8 план дублировал владение нулевая
R-7 (#16) counterparty_match_kind последняя колонка в ORM и DBML ADD COLUMN кладёт в конец порядок колонок
R-8 (#17) 404 всегда код NOT_FOUND, идентификатор — в тексте конвенция Срезов 1–2c отдельные коды при необходимости
R-9 (#18) in_work_only убирает пустые периоды; невидимая ГК/ГСК → 404; чужая ГСК или gsz → 422 RELATED_GROUP_MISMATCH продолжение policy 404 ожидание фронта по пустому списку
R-10 (#43) сбой перехода после загрузки логируется, файлы остаются приняты (201) приём файлов важнее побочного статуса версия может остаться INITIATED
R-11 (#62/#63) ручное сопоставление и автоприменение только по строкам свода (COUNTERPARTY, BU/SUM); NU/ACCOUNT → 404 §3 п. 2 NU-строки без counterparty_id; на подтверждение владельцу (OQ-126)
R-12 (#77) текст в xlsx: вырезаются управляющие символы, принудительно строковый тип защита от формульной инъекции текст с ведущим «=» не вычисляется
R-13 (#78) entity_names убран из build_workbook не используется вернуть при необходимости
R-14 (#81) ОСВ за Q1 равнозначна нарастающей и квартальной, подходит версиям Q2+ как квартальный Q1 парсер помечает ОСВ за Q1 нарастающей; без этого свод на реальных данных пуст на подтверждение (OQ-124)
R-14a (#81) эквивалентность Q1 только для ОСВ; ББ/ОФР за Q1 версиям Q2–Q4 не подходят ББ/ОФР за Q1 — баланс на конец Q1 снять условие, если должны подхватываться
R-15 (#80) сценарий «1 + propagated» уточнён: unmapped_total падает на число строк свода propagated считает osv_entry, свод склеивает кварталы формулировка (OQ-129)

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

T1.1, T2.1–T2.6, T5.2–T5.3, T6.1 (вынос transition в листовой модуль), T7 (N+1 в хуке маппинга), T8.3, T9.b (версия Q1, OQ-125), T9.c–T9.d, T10.1 (IN(ids) против лимита параметров), T10.2–T10.3, T11.3 (aliases.remove без блокировки), T11.4–T11.7, M-4 (тест advisory-блокировки алиаса), M-6 (границы входов, FOR UPDATE до проверки видимости), M-7 (общие osv_entry для версий не «в работе», до Среза 7, OQ-123), design #85 (OpenAPI без доменных 422-кодов).

🤖 Generated with Claude Code

https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2

Срез 4 — консолидация: версии, периметр, блокировка, привязка документов, сводная ОСВ, экспорт xlsx. Closes #18. **Порядок мержа:** сначала docs PR RSHB/arm-ka-docs#20 (DBML v5.16), затем этот: `Makefile` ссылается на `data-model-v5.16.dbml`, `make dbml-check` в основном checkout упадёт, пока docs не смержен. ## Что внутри - DBML v5.16: `osv_entry.counterparty_match_kind` (INN/NAME/ALIAS/MANUAL/PROPAGATED), миграция 0004 с backfill; маппинг пишет шаг резолва. - `POST /consolidations`, список по ГК с деревом версий, карточка версии, `PUT …/members`, `POST/DELETE …/lock` с перехватом по причине, `POST …/status` (CANCELLED, возврат в INITIATED), `GET …/checklist`. - Привязка `consolidation_document` при подтверждении, подхват при создании версии и расширении периметра, отвязка при автозамене; переходы INITIATED → DOCUMENTS_PROCESSING (загрузка с `consolidation_version_id`) → DATA_CORRECTION (NEEDS_CHECK при маппинге, подтверждение). - Сводная ОСВ как чистая функция (`domain/consolidated_osv.py`): выбор источников, склейка кварталов, пробелы, признак ГСК/ГК/Внешний, дорезолв на чтение; `GET …/osv`, `GET …/osv/summary`, `GET …/counterparty-candidates`, `POST …/osv/entries/{id}/counterparty` (алиас, перенос, автоприменение), `GET …/osv/export` (xlsx, два листа). - Сквозной тест на фикстурах РБ за Q3 2024: 22 документа, 1385 строк свода, пробелы по 01/58.03/66/67/76 РБ и 01/02 Счастливочной. - Спека `openspec/changes/slice-4-consolidation/` (design #1–#85), ledger `docs/superpowers/plans/2026-10-06-slice-4-consolidation-ledger.md`. ## Прогоны `make check-full` на 98922cc — EXIT 0: unit 1106, integration 670, fixtures 14 passed + 1 skipped (e2e без профиля services), dbml-check v5.16, docker build, web-check. На HEAD 42c2531: unit 1106, integration 677. Финальное ревью (opus): Critical нет, Important I-1 исправлена (42c2531). ## Rulings I made | Рулинг | Решение | Обоснование | Цена ошибки | |---|---|---|---| | R-1 (#11) | Ключ замены привязки включает признак нарастающего | спека противоречила сценарию «нарастающий рядом с квартальными» | меняется только ключ | | R-2 (#12) | `lock_versions_for_entity` блокирует надмножество версий; создание версии берёт advisory-lock по ГК; порядок файл → документ → advisory ГК → версии | иначе подтверждение ∥ периметр и автозамена ∥ подхват теряют привязку | редкая тихая потеря привязки | | R-3 (#13) | `lock_document`/`lock_file_documents` → `FOR NO KEY UPDATE` | вставка привязки берёт KEY SHARE, `FOR UPDATE` давал взаимоблокировку | нулевая | | R-4 (#14) | `Resolution` в `domain/osv_mapping.py` | домен не импортирует репозитории | нулевая | | R-5 (#15) | `EntityFilesState` — Task 2, `unconfirmed_documents` — Task 8 | план дублировал владение | нулевая | | R-7 (#16) | `counterparty_match_kind` последняя колонка в ORM и DBML | `ADD COLUMN` кладёт в конец | порядок колонок | | R-8 (#17) | 404 всегда код `NOT_FOUND`, идентификатор — в тексте | конвенция Срезов 1–2c | отдельные коды при необходимости | | R-9 (#18) | `in_work_only` убирает пустые периоды; невидимая ГК/ГСК → 404; чужая ГСК или gsz → 422 `RELATED_GROUP_MISMATCH` | продолжение policy 404 | ожидание фронта по пустому списку | | R-10 (#43) | сбой перехода после загрузки логируется, файлы остаются приняты (201) | приём файлов важнее побочного статуса | версия может остаться INITIATED | | R-11 (#62/#63) | ручное сопоставление и автоприменение только по строкам свода (COUNTERPARTY, BU/SUM); NU/ACCOUNT → 404 | §3 п. 2 | NU-строки без `counterparty_id`; **на подтверждение владельцу (OQ-126)** | | R-12 (#77) | текст в xlsx: вырезаются управляющие символы, принудительно строковый тип | защита от формульной инъекции | текст с ведущим «=» не вычисляется | | R-13 (#78) | `entity_names` убран из `build_workbook` | не используется | вернуть при необходимости | | R-14 (#81) | ОСВ за Q1 равнозначна нарастающей и квартальной, подходит версиям Q2+ как квартальный Q1 | парсер помечает ОСВ за Q1 нарастающей; без этого свод на реальных данных пуст | **на подтверждение (OQ-124)** | | R-14a (#81) | эквивалентность Q1 только для ОСВ; ББ/ОФР за Q1 версиям Q2–Q4 не подходят | ББ/ОФР за Q1 — баланс на конец Q1 | снять условие, если должны подхватываться | | R-15 (#80) | сценарий «1 + propagated» уточнён: `unmapped_total` падает на число строк свода | `propagated` считает `osv_entry`, свод склеивает кварталы | формулировка (OQ-129) | ## Deferred (в follow-up issue при закрытии) T1.1, T2.1–T2.6, T5.2–T5.3, T6.1 (вынос `transition` в листовой модуль), T7 (N+1 в хуке маппинга), T8.3, T9.b (версия Q1, OQ-125), T9.c–T9.d, T10.1 (`IN(ids)` против лимита параметров), T10.2–T10.3, T11.3 (`aliases.remove` без блокировки), T11.4–T11.7, M-4 (тест advisory-блокировки алиаса), M-6 (границы входов, `FOR UPDATE` до проверки видимости), M-7 (общие `osv_entry` для версий не «в работе», до Среза 7, OQ-123), design #85 (OpenAPI без доменных 422-кодов). 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
aep added 1 commit 2026-10-06 01:52:47 +03:00
Proposal, design (D-1…D-16, решения без обсуждения), tasks и дельты спек: новые capability consolidations-api и
consolidated-osv; изменения data-model (DBML v5.16, counterparty_match_kind), documents-api (привязка при
подтверждении), primary-files (consolidation_version_id, отвязка при автозамене), osv-result-mapping (match_kind,
NEEDS_CHECK → DATA_CORRECTION), directory-policy (редактируемость версии), audit-trail (события консолидации).
Решения владельца 06.10.2026: OQ-03 пробел, OQ-19 блокировка, статусы по событиям, явная привязка, экспорт одним листом.

Refs RSHB/arm-core#18

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
aep added 1 commit 2026-10-06 02:04:23 +03:00
aep added 25 commits 2026-10-06 07:36:48 +03:00
Рулинги контроллера R-1…R-5 из pre-flight-проверки плана: ключ привязки с признаком нарастающего,
блокировка надмножества версий, FOR NO KEY UPDATE на документах/файлах, Resolution в домене,
разнесение EntityFilesState между задачами.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
Enum counterparty_match_kind (INN, NAME, ALIAS, MANUAL, PROPAGATED) и nullable-колонка osv_entry.counterparty_match_kind
(NULL <=> counterparty_id NULL). Миграция 0004_dbml_v5_16: enum, колонка, backfill тремя UPDATE по уже смапленным
строкам (ИНН без пробелов -> INN, trim(имя) -> NAME, остальные -> ALIAS), откат удаляет колонку и тип.
CounterpartyIndex.resolve возвращает Resolution(entity_id, kind) (определение в domain/osv_mapping, R-4);
маппинг пишет kind; EntryOut.match_kind. DBML_VERSION 5.16, Makefile/dbml_parity/CLAUDE/README/config.yaml.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
Репозиторий консолидаций (get_version/list_periods/lock_versions_for_entity с блокировкой
надмножества версий по R-2, load_osv_rows, entity_files_state), схемы API (D-16),
domain/checklist.EntityFilesState (R-5), фабрики make_consolidation/make_lock/make_confirmed_osv
и очистка таблиц консолидации в CommittedJobs.cleanup().

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
POST /consolidations (версия №1 INITIATED, периметр из текущих членов ГСК, история,
CONSOLIDATION_CREATE с consolidation_version_id), GET /company-groups/{id}/consolidations
и GET /consolidations/versions/{id}. reporting_date по OQ-40; подхват документов — заглушка
bind_confirmed до Task 6, чек-лист в карточке пуст до Task 8. UNIQUE периода -> 409
CONSOLIDATION_EXISTS; RequestAudit.record принимает consolidation_version_id.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
- domain/policy: IN_WORK_STATUSES, ensure_version_editable (VERSION_NOT_EDITABLE / LOCK_REQUIRED / LOCKED_BY_OTHER)
- services/consolidation_locks: take/release, перехват с причиной, release_active_lock; FOR UPDATE версии первой (D-9),
  вставка под SAVEPOINT, нарушение uq_consolidation_lock_version_active -> 409 LOCKED_BY_OTHER с владельцем
- services/consolidation_status: MANUAL_TRANSITIONS, transition (единственное место смены статуса: история + аудит,
  CANCELLED снимает блокировку), ConsolidationStatusService.change
- маршруты POST/DELETE …/lock, POST …/status; реестр constraint_conflicts
- тесты: таблица ensure_version_editable, сценарии спеки, гонка двух POST …/lock, перехват во время правки и
  аннулирования (две сессии, Review Focus 4)
- минорные T3.1 (FINISHED_STATUSES из policy в фабриках), T3.2 (удалена CONSOLIDATION_NOT_FOUND)
- план: шаги Task 4; tasks.md 2.2, 2.3

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
- I-1 (T4.1): lock_out — публичный в services/consolidations.py, копия в consolidation_locks удалена
- M-b (T4.3): удалён осиротевший комментарий в tests/factories.py
- M-c (T4.4): точная проверка ip_address/user_agent в аудите смены статуса
- M-d (T4.5): docstring теста Review Focus 4 ссылается на различающий тест порядка D-9
- design.md: решения без обсуждения #17 (R-8), #18 (R-9), #19–#24 (решения Task 4)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
PUT /consolidations/versions/{id}/members: замена состава ЮЛ версии, проверка принадлежности ГК
(ENTITY_NOT_IN_GROUP), минимум два ЮЛ, diff с отвязкой документов ушедших (unbind_entity) и подхватом
добавленных (bind_confirmed, заглушка до Task 6), аудит MEMBERSHIP_CHANGE. Общий пролог
«lock → видимость → редактируемость» вынесен в services/consolidation_guard.py (T4.2). Решения #25–#29.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
- consolidation_documents: чистая fits (D-5), bind_confirmed (подхват, самый поздний confirmed_at на ключ R-1),
  unbind_entity, on_document_confirmed (привязка ко всем подходящим версиям «в работе», переход в DATA_CORRECTION
  через transition), on_documents_superseded (отвязка документов заменённых файлов).
- DocumentService.confirm вызывает хук привязки; supersede_previous снимает привязки и пишет
  FILE_REPLACE.after_value.unbound_version_ids; create и PUT …/members подхватывают по-настоящему.
- R-2: advisory-блокировка привязок ГК (lock_group_bindings) в create и lock_versions_for_entity закрывает
  гонку «создание версии ∥ подтверждение/автозамена».
- R-3: lock_document и lock_file_documents — FOR NO KEY UPDATE (совместимо с FOR KEY SHARE вставки привязки).
- Тесты: таблица fits, сценарии documents-api и primary-files, две версии одного документа, гонки на двух
  сессиях (confirm ∥ members в обе стороны, create ∥ confirm в обе стороны, цикл блокировок документа и версии).
- design.md: решения №30–№36; plan и tasks.md (3.1) отмечены.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
- T6.0: тесты на двух сессиях «автозамена ∥ подхват при правке периметра» и «автозамена ∥ создание версии»:
  автозамена ждёт, снимает незафиксированную ранее привязку, unbound_version_ids — эта версия.
- T6.2: тесты выбора самого позднего confirmed_at строятся так, чтобы «наибольший id» проигрывал;
  тест равенства confirmed_at (больший id, решение №35).
- T6.3: реальный подхват через PUT …/members — версия в INITIATED, статус не меняется.
- T6.4: lock_versions_for_entity не читает ГК ЮЛ второй раз (подзапрос заменён значением).
- T6.5: design №30 — цена: подтверждения документов ЮЛ одной ГК сериализуются на advisory даже без версий в работе.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
Загрузка с consolidation_version_id: проверки до приёма (404/409/422), после приёма
INITIATED -> DOCUMENTS_PROCESSING в отдельной транзакции без блокировки версии.
Маппинг: on_documents_mapped переводит версии DOCUMENTS_PROCESSING в DATA_CORRECTION
при подходящем документе NEEDS_CHECK (changed_by = uploaded_by), привязки не создаёт.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
T7.0: двухсессионный тест маппинг ∥ перевод версии в DOCUMENTS_PROCESSING (ловит мутант
statuses={DOCUMENTS_PROCESSING}). T7.1 (R-10, design №43): ошибка перехода после загрузки логируется,
принятые файлы и 201 сохраняются. T7.3: лог пропуска без uploaded_by только при наличии NEEDS_CHECK.
T7.4: TransactionFactory импортируется из session_audit. T7.2/T7.5: дополнена строка design №39.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
Домен `checklist` (Rule, CellStatus, Cell, order_rules, build) — чистые функции без ORM; ячейка (ЮЛ, правило):
CONFIRMED по привязанным документам (ОСВ: нарастающий за квартал версии либо квартальные Q1..Qn, пробел →
missing_quarters; ББ/ОФР: документ за квартал версии), NEEDS_CHECK/RECOGNIZED по неподтверждённому подходящему,
PROCESSING, ERROR, иначе MISSING; правило ОСВ закрывает только точный код счёта. Репозиторий: rules_for,
unconfirmed_documents. Сервис: checklist(), ячейки в карточке версии. Маршрут GET /consolidations/versions/{id}/checklist.
Схема использует CellStatus домена (T2.8). Решения №44-49 в design.md.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
- consolidation_periods: QUARTER_ORDER (единственное определение), quarters_through,
  counts_as_cumulative (R-1), select_sources -> Sources | Gap (D-2)
- consolidated_osv: build (склейка квартальных по ключу, нарастающий как есть, пробелы,
  counterparty_id последнего квартала + warnings, дорезолв без записи, признак GSK/GK/EXTERNAL,
  порядок D-16), apply_filter, summary, classify
- checklist: _osv_coverage — адаптер над select_sources (T8.1), явная таблица статусов
  кандидатов (T8.4), QUARTER_ORDER/account_sort_key из домена (T8.5)
- consolidation_documents: QUARTER_ORDER и counts_as_cumulative из домена
- schemas: CounterpartyType — доменный StrEnum (T2.8)
- design.md: решения №50–56; план и tasks.md 4.1 отмечены

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
GET /consolidations/versions/{id}/osv и /osv/summary (CONSOLIDATION_VIEW): ConsolidatedOsvService собирает свод
чистой функцией от версии (периметр, привязанные ОСВ, load_osv_rows, один CounterpartyIndex, один SELECT legal_entity)
и режет страницу в памяти; чтение любой версии, включая CANCELLED, без блокировок и аудита.
OsvFilters отделён от OsvPageParams (T2.7), суммы всегда с двумя знаками; account_sort_key устойчив к «6²»/«①» (T9.a).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
GET …/counterparty-candidates (scope group|all, QUERY_TOO_SHORT, recommended по
нормализованному имени строки) и POST …/osv/entries/{entry_id}/counterparty:
проверка строки свода версии, ЮЛ без policy, ALIAS_TAKEN с owners / перенос алиаса,
MANUAL строке и PROPAGATED строкам с тем же strip()-именем без противоречия ИНН,
один OSV_COUNTERPARTY_MAP и LEGAL_ENTITY_CHANGE на каждый алиас. Строки общие для
версий блокируются FOR NO KEY UPDATE ORDER BY id; advisory-блокировка алиаса общая
со справочником. Решения #62–#70 в design.md; 4.3 отмечена.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
GET /consolidations/versions/{id}/osv/export: лист «Сводная ОСВ» (15 колонок D-14, суммы — числовые
ячейки #,##0.00 из Decimal) и лист «Пробелы» по своду целиком; фильтры как у GET …/osv без пагинации;
Content-Disposition с filename* (чистый ASCII); audit_event(EXPORT) с filters и rows.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
Строки пишутся по явному счётчику (sheet.max_row делал сборку квадратичной: 6 000 строк — 8.4 с → 0.5 с),
сборка вынесена в run_in_threadpool. Тексты проходят _put_text: запрещённые XML-символы вырезаются, тип
ячейки строковый — ведущие =, +, -, @ не становятся формулами (R-12, design #77). Параметр entity_names
удалён (R-13, #78), порог точности double в #73 исправлен, тесты пустых сумм и 6 000 строк.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
Хелпер map_golden_snapshot, сценарий маппинг golden -> подтверждение -> консолидация 2024 Q3 -> свод,
сопоставление Счастливочной, экспорт, независимая сверка склейки периодов. Тест помечен xfail(strict):
на реальных данных нарастающий Q1 (is_cumulative=true) не привязывается к версии Q3 и не считается
квартальным (fits / select_sources) — свод пуст; подробности в отчёте задачи.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
Сквозной тест на фикстурах РБ показал: парсер помечает ОСВ за Q1 нарастающей, а
привязка (`fits`) брала нарастающий только за квартал версии, `select_sources` не
считал его квартальным Q1 — для версии Q3 привязано 17 из 22, пары 60/62(/76) в
пробеле [Q1], свод пуст.

- consolidation_periods.effective_cumulative: для Q1 — квартальный при любом флаге;
  одна функция для `fits`, ключа замены `_key`, `select_sources` (версии Q2+) и
  адаптера чек-листа; для версии Q1 поведение прежнее (№50)
- тесты: таблица effective_cumulative, select_sources/build/чек-лист с нарастающим
  Q1, fits любой версии года, подтверждение и подхват (нарастающий и квартальный Q1 —
  один слот, побеждает поздний)
- test_consolidation_fixtures: xfail снят, сценарий зелёный (22 привязки, 1385 строк)
- спеки documents-api и consolidated-osv: фраза о равнозначности Q1; design #81;
  план Task 13 Step 2–4 и tasks.md 5.1 отмечены

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
ББ/ОФР за Q1 — отчёт на конец квартала, не источник версий Q2–Q4. В
`consolidation_documents._cumulative` равнозначность Q1 (`effective_cumulative`)
применяется только к `document_type = OSV`; для ББ/ОФР — прежнее правило D-5 (NULL —
нарастающий, только квартал версии), ключ замены не меняется.

- тесты: ББ/ОФР за Q1 подходят только версии Q1; ключ замены ББ/ОФР всегда с
  признаком нарастающего, ОСВ за Q1 — без различия флага; подтверждение ББ/ОФР за Q1
  при версии Q3 — без привязки и перехода; ячейка ББ чек-листа Q3 не закрывается ББ Q1
- спека documents-api: ключ замены R-1 с признаком нарастающего (вместо устаревшей
  «тройки»), равнозначность Q1 — только для ОСВ; consolidated-osv — «ОСВ за Q1»
- design #81: R-14 + R-14a

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
Точные числа свода и набор пробелов, кандидаты, MANUAL/alias_created, EXTERNAL до сопоставления; формулировка
сценария «Сопоставление из плана среза» и design #80 уточнены; docstring effective_cumulative.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
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
Co-Authored-By: Claude Sonnet <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
I-1 (T8.2): тесты, что неподтверждённый документ даёт NEEDS_CHECK/RECOGNIZED только если подходит версии (fits):
нарастающий Q2 и квартальный Q4 при версии Q3, другой год и ЮЛ вне периметра не влияют; положительный контроль.
M-2: 404 `NOT_FOUND` с сообщением `…_NOT_FOUND`, ключ привязки с признаком нарастающего (R-1, R-14), порядок и
режимы блокировок подтверждения (R-3) в spec. M-3: в design #39 и #43 — окно коммита перехода после загрузки.
M-5: docstring lock_versions_for_entity про ЮЛ без ГК в периметре.

Co-Authored-By: Claude Sonnet <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
aep changed title from spec(slice-4): OpenSpec change консолидации (ревью спеки) to feat(slice-4): консолидация — версии, периметр, блокировка, привязка документов, сводная ОСВ, экспорт xlsx 2026-10-06 07:37:54 +03:00
aep merged commit 4663d44ce8 into main 2026-10-06 09:38:38 +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#19
No description provided.