diff --git a/openspec/changes/aurora-1848-sheet-json/.openspec.yaml b/openspec/changes/archive/2026-11-12-aurora-1848-sheet-json/.openspec.yaml similarity index 100% rename from openspec/changes/aurora-1848-sheet-json/.openspec.yaml rename to openspec/changes/archive/2026-11-12-aurora-1848-sheet-json/.openspec.yaml diff --git a/openspec/changes/aurora-1848-sheet-json/design.md b/openspec/changes/archive/2026-11-12-aurora-1848-sheet-json/design.md similarity index 100% rename from openspec/changes/aurora-1848-sheet-json/design.md rename to openspec/changes/archive/2026-11-12-aurora-1848-sheet-json/design.md diff --git a/openspec/changes/aurora-1848-sheet-json/proposal.md b/openspec/changes/archive/2026-11-12-aurora-1848-sheet-json/proposal.md similarity index 100% rename from openspec/changes/aurora-1848-sheet-json/proposal.md rename to openspec/changes/archive/2026-11-12-aurora-1848-sheet-json/proposal.md diff --git a/openspec/changes/aurora-1848-sheet-json/specs/realtime-sheet-updates/spec.md b/openspec/changes/archive/2026-11-12-aurora-1848-sheet-json/specs/realtime-sheet-updates/spec.md similarity index 100% rename from openspec/changes/aurora-1848-sheet-json/specs/realtime-sheet-updates/spec.md rename to openspec/changes/archive/2026-11-12-aurora-1848-sheet-json/specs/realtime-sheet-updates/spec.md diff --git a/openspec/changes/aurora-1848-sheet-json/specs/sheet-read-api/spec.md b/openspec/changes/archive/2026-11-12-aurora-1848-sheet-json/specs/sheet-read-api/spec.md similarity index 100% rename from openspec/changes/aurora-1848-sheet-json/specs/sheet-read-api/spec.md rename to openspec/changes/archive/2026-11-12-aurora-1848-sheet-json/specs/sheet-read-api/spec.md diff --git a/openspec/changes/aurora-1850-upd-form-cells-subtree/.openspec.yaml b/openspec/changes/archive/2026-11-12-aurora-1850-upd-form-cells-subtree/.openspec.yaml similarity index 100% rename from openspec/changes/aurora-1850-upd-form-cells-subtree/.openspec.yaml rename to openspec/changes/archive/2026-11-12-aurora-1850-upd-form-cells-subtree/.openspec.yaml diff --git a/openspec/changes/aurora-1850-upd-form-cells-subtree/before-0051.sql b/openspec/changes/archive/2026-11-12-aurora-1850-upd-form-cells-subtree/before-0051.sql similarity index 100% rename from openspec/changes/aurora-1850-upd-form-cells-subtree/before-0051.sql rename to openspec/changes/archive/2026-11-12-aurora-1850-upd-form-cells-subtree/before-0051.sql diff --git a/openspec/changes/aurora-1850-upd-form-cells-subtree/design.md b/openspec/changes/archive/2026-11-12-aurora-1850-upd-form-cells-subtree/design.md similarity index 100% rename from openspec/changes/aurora-1850-upd-form-cells-subtree/design.md rename to openspec/changes/archive/2026-11-12-aurora-1850-upd-form-cells-subtree/design.md diff --git a/openspec/changes/aurora-1850-upd-form-cells-subtree/proposal.md b/openspec/changes/archive/2026-11-12-aurora-1850-upd-form-cells-subtree/proposal.md similarity index 100% rename from openspec/changes/aurora-1850-upd-form-cells-subtree/proposal.md rename to openspec/changes/archive/2026-11-12-aurora-1850-upd-form-cells-subtree/proposal.md diff --git a/openspec/changes/aurora-1850-upd-form-cells-subtree/specs/sheet-cell-edit/spec.md b/openspec/changes/archive/2026-11-12-aurora-1850-upd-form-cells-subtree/specs/sheet-cell-edit/spec.md similarity index 100% rename from openspec/changes/aurora-1850-upd-form-cells-subtree/specs/sheet-cell-edit/spec.md rename to openspec/changes/archive/2026-11-12-aurora-1850-upd-form-cells-subtree/specs/sheet-cell-edit/spec.md diff --git a/openspec/specs/realtime-sheet-updates/spec.md b/openspec/specs/realtime-sheet-updates/spec.md new file mode 100644 index 0000000..01baa88 --- /dev/null +++ b/openspec/specs/realtime-sheet-updates/spec.md @@ -0,0 +1,43 @@ +# realtime-sheet-updates Specification + +## Purpose +Как редактируемая таблица фронта (лист формы, отчёт проекта) применяет входящие правки соавторов и эхо собственных правок к уже загруженным строкам и как она показывает значения, которых нет в строке. Цель — чтобы правки не терялись, даже если строки загружены без null-полей. + +## Requirements + +### Requirement: Входящая правка применяется к отсутствующему ключу +Когда приходит обновление строк — сообщение WebSocket `cell_updated` или ответ PATCH на собственную правку — фронт SHALL записать в найденную строку каждое поле из обновления, на любой глубине. Это делается и тогда, когда ключа этого поля или содержащего его блока в загруженной строке нет. Правка SHALL NOT теряться из-за того, что при загрузке листа поле было опущено. + +#### Scenario: Соавтор заполняет пустую ячейку +- **WHEN** лист загружен без ключа `data.q1.<поле>` у строки (ячейка пустая), а соавтор вводит в эту ячейку значение +- **THEN** у пользователя ячейка показывает новое значение без перезагрузки листа + +#### Scenario: Отсутствует весь блок +- **WHEN** в загруженной строке нет ключа блока `data.<блок>`, а приходит обновление с `data.<блок>.<поле>` +- **THEN** в строке появляется блок с этим полем, а ячейка показывает значение + +#### Scenario: Прочие поля строки сохраняются +- **WHEN** приходит обновление одного поля строки +- **THEN** остальные поля строки сохраняют прежние значения + +### Requirement: Очистка ячейки доходит до соавторов +Если в обновлении поле равно `null`, фронт SHALL записать `null`, и ячейка SHALL показываться пустой. + +#### Scenario: Соавтор очищает ячейку +- **WHEN** у пользователя ячейка показывает значение, а соавтор очищает её +- **THEN** у пользователя ячейка становится пустой без перезагрузки листа + +### Requirement: Отсутствующее значение показывается как null +Если ключа нет в `data`, редактируемая таблица, свод 1 и свод 2 SHALL показывать и обрабатывать значение так же, как явный `null`. Отображение: пустая ячейка в листе, «—» в сводах. Правила подсветки ошибок валидации не срабатывают. Поиск и фильтр по колонке не находят такую ячейку по непустому запросу. При входе в редактирование поле ввода пустое. Правка, которую фронт отправляет по WebSocket, SHALL всегда содержать ключ `value`; пустое значение передаётся как `null`, а не опускается. + +#### Scenario: Выход из пустой ячейки без ввода +- **WHEN** пользователь входит в редактирование ячейки, ключа которой нет в строке, и выходит без ввода +- **THEN** сообщение `cell_updated` содержит `"value": null`, как и для ячейки с явным `null`, а пользователь не получает ошибку + +#### Scenario: Пустая ячейка свода 2 +- **WHEN** свод 2 загружен без ключа `data.q1.<поле>` у строки +- **THEN** ячейка показывает «—», как и для `null` + +#### Scenario: Валидация не срабатывает +- **WHEN** у строки листа отсутствует ключ поля, к которому привязано правило валидации (например, остаток бронирования) +- **THEN** ячейка не подсвечивается как ошибочная diff --git a/openspec/specs/sheet-cell-edit/spec.md b/openspec/specs/sheet-cell-edit/spec.md new file mode 100644 index 0000000..083fa9a --- /dev/null +++ b/openspec/specs/sheet-cell-edit/spec.md @@ -0,0 +1,113 @@ +# sheet-cell-edit Specification + +## Purpose +Ответ сервера на правку ячеек листа бюджетной формы (HTTP `PATCH /form/{id}/sheet/{sheet}/cell(s)` и WebSocket `cell_updated`): какие строки в нём приходят, какими они должны быть относительно полного GET листа и сколько времени правка может стоить на сервере. + +## Requirements + +### Requirement: Ответ на правку содержит затронутые строки целиком +Ответ на правку ячеек листов AHR/CAP/OPER форм FORM_1, FORM_2 и FORM_4 SHALL содержать каждую изменённую строку INPUT и все её строки-агрегаты, то есть всех предков по дереву статей до ROOT включительно. Для FORM_4 предками считаются только ROOT раздела, GROUP программы и ITEM проекта изменённой строки. Агрегат SHALL входить в ответ, даже если его значения не изменились. Ответ SHALL NOT содержать строку META и строки других ветвей дерева. Поле `data` каждой строки SHALL быть полной строкой в том же виде, что и в GET листа с теми же `direction` и `sections`: пустые значения передаются явным `null`. Если задан `sections`, ключи верхнего уровня у исключённых разделов SHALL отсутствовать в `data`. + +#### Scenario: Правка числовой ячейки возвращает строку и её предков +- **WHEN** исполнитель меняет `plan.q1` у строки листа FORM_2/AHR +- **THEN** ответ содержит эту строку INPUT и по одной строке-агрегату на каждый уровень её пути в дереве статей +- **AND** `data` каждой строки ответа равно `data` строки того же `sort_order` из последующего GET листа + +#### Scenario: Правка нечисловой ячейки всё равно возвращает предков +- **WHEN** исполнитель меняет `header.justification` у строки FORM_2/AHR +- **THEN** ответ содержит строку INPUT и всех её предков, хотя их суммы не изменились + +#### Scenario: Очистка значения +- **WHEN** исполнитель записывает `null` в `plan.q1` +- **THEN** в строке INPUT ответа ключ `plan.q1` присутствует со значением `null` (или 0 — так же, как в GET) +- **AND** суммы предков в ответе уменьшены на прежнее значение ячейки + +#### Scenario: Правка с фильтром разделов +- **WHEN** правка выполняется с `sections=plan` +- **THEN** ни одна строка ответа не содержит ключей разделов, отличных от `plan`, а также служебных ключей, которых нет в GET с тем же `sections` + +### Requirement: sort_order в ответе совпадает с полным листом +Значения `row_type`, `depth` и `sort_order` каждой строки ответа на правку SHALL совпадать со значениями этой же строки в полном GET листа с теми же параметрами, выполненном сразу после правки. `sort_order` — сквозной номер строки во всём листе, а не номер внутри ответа. Это SHALL выполняться для первой и последней строки листа, после добавления и удаления строк в листе, а также при совпадении ключа сортировки у строки INPUT и узла дерева. Порядок строк в GET при совпадении ключа SHALL быть детерминированным и одинаковым в GET и в ответе на правку. + +#### Scenario: Первая и последняя строка листа +- **WHEN** правится первая по порядку строка INPUT листа, а затем последняя +- **THEN** у каждой строки обоих ответов есть строка с тем же `(row_type, depth, sort_order)` в GET листа, и её `data` совпадает + +#### Scenario: После добавления и удаления строк +- **WHEN** в лист добавлены строки (POST line), одна из строк в середине листа удалена (DELETE line), после чего правится строка, расположенная ниже удалённой +- **THEN** `sort_order` каждой строки ответа равен её `sort_order` в GET листа после этих операций + +#### Scenario: Пакет правок по разным ветвям дерева +- **WHEN** через `PATCH /cells` одним запросом правятся строки двух разных ветвей дерева статей +- **THEN** ответ содержит обе строки INPUT и объединение их предков без дублей, и каждая строка совпадает со строкой GET того же `sort_order` + +#### Scenario: Направление FORM_1 +- **WHEN** правится строка FORM_1/AHR с `direction=Support` +- **THEN** `sort_order` строк ответа равен `sort_order` в GET FORM_1/AHR с `direction=Support` + +### Requirement: Состав ответа на листах сателлитов, AHR_LIMIT и OTCH9F +На листах FORM_2 AHR_RENT, AHR_UTILITY и AHR_SECURITY ответ на правку SHALL содержать только изменённые строки. На AHR_LIMIT ответ SHALL содержать изменённые строки LEAF шаблона и их предков по шаблону (GROUP и SECTION). На OTCH9F ответ SHALL содержать все строки (все месяцы) того же сочетания статьи и оборудования, что и изменённая строка: остатки на начало месяца зависят от предыдущих месяцев. Для всех этих листов SHALL выполняться требование о совпадении `data` и `sort_order` с полным GET. + +#### Scenario: Правка строки аренды +- **WHEN** исполнитель меняет `plan.q1` у строки AHR_RENT +- **THEN** ответ содержит ровно одну строку с `data.id` этой строки, и она совпадает со строкой GET AHR_RENT того же `sort_order` + +#### Scenario: Правка количества в лимитах +- **WHEN** исполнитель меняет `qty_q1` у строки LEAF листа AHR_LIMIT +- **THEN** ответ содержит эту строку и строки её группы и раздела с пересчитанными `amount_q1`, и они совпадают с GET AHR_LIMIT + +#### Scenario: Правка месяца в OTCH9F +- **WHEN** исполнитель меняет поступление за месяц у строки OTCH9F +- **THEN** ответ содержит строки всех месяцев того же оборудования той же статьи с пересчитанными остатками, и они совпадают с GET OTCH9F + +### Requirement: Обновление АХР после правки сателлита +После правки или добавления строки на AHR_RENT, AHR_UTILITY, AHR_SECURITY или AHR_LIMIT формы FORM_2 сервер SHALL отправить подключённым к листу AHR событие (`cell_updated` или `row_added`). Событие SHALL содержать синхронизированные строки AHR и всех их предков, включая уровень SUB_SUB_ITEM. Каждая из этих строк SHALL совпадать со строкой того же `sort_order` полного GET листа AHR. Если синхронизированных строк нет, событие SHALL NOT содержать строк AHR, то есть ответ пуст, а не весь лист. Строки AHR_LIMIT SHALL сопоставляться и со статьями AHR глубины 4 по `num_group_id` (поведение миграции 0037). + +#### Scenario: Правка аренды обновляет АХР +- **WHEN** исполнитель меняет `plan.q1` у строки AHR_RENT, связанной со строкой AHR +- **THEN** строки, которые сервер готовит для события на лист AHR, совпадают со строками GET AHR того же `sort_order`, и среди них есть синхронизированная строка INPUT с новой суммой + +#### Scenario: Источник без синхронизированных строк +- **WHEN** запрашивается обновление АХР для строки сателлита, у которой нет связанной строки AHR +- **THEN** набор строк для события пуст + +#### Scenario: Лимит на статье глубины 4 +- **WHEN** правится строка AHR_LIMIT, код статьи которой совпадает с `num_group_id` статьи AHR глубины 4 +- **THEN** набор строк для события содержит синхронизированную строку AHR этой статьи + +### Requirement: Пакет правок атомарен +Если хотя бы одна правка в пакете отклонена (неизвестная колонка, чужая строка, нет прав, окно закрыто), SHALL NOT применяться ни одна правка пакета, и ответ SHALL быть ошибкой. + +#### Scenario: Ошибочная колонка в пакете +- **WHEN** `PATCH /cells` содержит одну корректную правку и одну с несуществующей колонкой +- **THEN** ответ — ошибка 4xx, и GET листа после запроса совпадает с GET до него + +### Requirement: GET листа не меняется +Содержимое GET листов FORM_1 и FORM_2 (строки, `data`, `sort_order`) после изменения SHALL совпадать с содержимым до него на тех же данных. Исключение — порядок строк с совпадающим ключом сортировки: до изменения он не был определён. Время GET FORM_2/AHR SHALL NOT вырасти больше чем на 10 %. + +#### Scenario: Снимок GET до и после миграции +- **WHEN** на стенде профилирования снят GET всех листов FORM_1 и FORM_2 из манифеста до применения миграции и после неё +- **THEN** ответы попарно совпадают + +#### Scenario: Время GET после смешанной нагрузки +- **WHEN** в одном соединении с БД чередуются правки ячеек и GET листа FORM_2/AHR +- **THEN** среднее время GET не больше базового + 10 % + +### Requirement: Время правки ячейки на FORM_1 и FORM_2 +Правка одной ячейки листа FORM_1 или FORM_2 SHALL NOT собирать лист целиком: на правку SHALL приходиться 0 вложенных полных сборок листа. Среднее серверное время `v3.upd_form_cell` на FORM_2/AHR SHALL быть не больше 50 мс как на листе в 1154 строки, так и на листе в 2092 строки. Требование не распространяется на FORM_4 до слияния AURORA-1846. + +#### Scenario: Соавтор на листе масштаба 1 +- **WHEN** на стенде профилирования (масштаб 1, без grow, FORM_2/AHR 1154 строки) выполняется шаг `coauthor` +- **THEN** в `sql/coauthor/sql_top.json` `mean_exec_time` вызова `v3.upd_form_cell` ≤ 50 мс +- **AND** вызовов `v3.v_form_view` не верхнего уровня (`toplevel = false`) нет, а число вызовов `v3.v_form2_view(p_form_id, p_sheet, p_sections)` равно числу GET листа и не зависит от числа правок + +#### Scenario: Соавтор на выросшем листе +- **WHEN** после `perfkit.gen.grow` (FORM_2/AHR 2092 строки) шаг `coauthor` выполняется повторно +- **THEN** `mean_exec_time` вызова `v3.upd_form_cell` ≤ 50 мс + +### Requirement: Ручной ввод без очереди +Сервер SHALL успевать подтверждать правки одного соединения с частотой ручного ввода (5 правок/с), не накапливая очередь. + +#### Scenario: Соавтор с частотой 5 правок в секунду +- **WHEN** бот соавтора на FORM_2/AHR отправляет 5 правок/с в течение шага `coauthor` +- **THEN** число полученных ответов (эхо и ошибки) составляет не меньше 95 % от числа отправленных правок diff --git a/openspec/specs/sheet-read-api/spec.md b/openspec/specs/sheet-read-api/spec.md new file mode 100644 index 0000000..48de8b3 --- /dev/null +++ b/openspec/specs/sheet-read-api/spec.md @@ -0,0 +1,132 @@ +# sheet-read-api Specification + +## Purpose +Контракт ответа тяжёлых GET с табличными данными: лист формы, своды 1 и 2, отчёты и сводные проектов РФ. Здесь описано, что получает фронт: обёртка, строки, отсутствие null-полей, служебный заголовок времени БД. Описаны и неизменные пути записи и выгрузки, и пороги производительности. + +## Requirements + +### Requirement: Обёртка и строки ответа тяжёлых GET +Следующие эндпоинты (далее — «тяжёлые GET») SHALL отвечать при успехе статусом 200, `Content-Type: application/json` и телом-объектом `{"success": true, "message": null, "result": [...], "count": N}`: +- `GET /api/v1/form/{form_id}/sheet/{sheet}`, +- `GET /api/v1/svod/1/{sheet}/{year}`, +- `GET /api/v1/svod/2/{sheet}/{year}`, +- `GET /api/v1/projects/{project_id}/report/{year}/{report_type}`, +- `GET /api/v1/rf-rollup/{branch_id}/{year}`, +- `GET /api/v1/projects/{project_id}/summary`, +- `GET /api/v1/projects/plan-summary`, `GET /api/v1/projects/fact-summary`, `GET /api/v1/projects/detail-summary`. + +Каждый элемент `result` SHALL быть объектом с ключами `row_type` (строка), `depth` (целое), `sort_order` (целое или null) и `data` (объект). `count` SHALL равняться числу элементов `result`. Строки SHALL идти в том порядке, в каком их возвращает SQL-функция эндпоинта. Ответ SHALL сжиматься gzip так же, как остальные ответы API: при `Accept-Encoding: gzip` и размере от 1000 байт. + +#### Scenario: Лист формы +- **WHEN** пользователь с доступом к форме запрашивает `GET /api/v1/form/{id}/sheet/AHR` для формы FORM_2 +- **THEN** ответ 200, тело — объект с `success: true`, `message: null`, `result` — массив строк с ключами `row_type`, `depth`, `sort_order`, `data`, первая строка — `META` с `depth = -1`, `count` равен длине `result` + +#### Scenario: Пустой свод +- **WHEN** пользователь без доступа ни к одной из запрошенных `org_ids` запрашивает `GET /api/v1/svod/2/GO/{year}?org_ids=...` +- **THEN** ответ 200 с телом `{"success": true, "message": null, "result": [], "count": 0}` + +#### Scenario: Сжатие +- **WHEN** клиент запрашивает свод 2 с заголовком `Accept-Encoding: gzip` +- **THEN** ответ приходит с `Content-Encoding: gzip`, а после распаковки — тот же JSON + +### Requirement: Значения ответа совпадают с прежними +После разбора JSON тяжёлые GET SHALL отдавать те же строки и те же значения, что отдавались до этого изменения при тех же данных, параметрах и пользователе. Единственное допустимое отличие — правило про null-поля (см. требование «Ответ листа и сводов без null-полей»). Текстовая запись чисел MAY отличаться (`1.50` вместо `1.5`), если после разбора значения равны. Порядок ключей в объектах и пробелы MAY отличаться. + +Проверки доступа, ответы на ошибки (статус и тело) и обработка параметров (`direction`, `sections`, `org_ids`, `offset`, `limit`, `sort_by`, `sort_direction`, `branch_id`, `project_type`) SHALL остаться прежними. + +#### Scenario: Сравнение до и после на стенде +- **WHEN** на одних и тех же данных стенда `perf/` сняты ответы всех листов, сводов 1 и 2 и отчётов проектов по манифесту — сначала на версии до изменения, затем после PR A +- **THEN** для каждого запроса результат `json.loads` ответа «после» равен результату `json.loads` ответа «до» + +#### Scenario: Ошибки не изменились +- **WHEN** запрашивается лист несуществующей формы (`GET /api/v1/form/999/sheet/AHR`) или лист свода вне допустимого списка +- **THEN** статус и тело ответа такие же, как до изменения (404 с прежним `message`) + +#### Scenario: Ограничение организаций в своде +- **WHEN** пользователь с ролью EXECUTOR_DFIP запрашивает свод с `org_ids`, часть которых ему не назначена +- **THEN** в ответе только строки назначенных ему организаций, как и до изменения + +### Requirement: Ответ листа и сводов без null-полей +После PR B в ответах `GET /api/v1/form/{form_id}/sheet/{sheet}`, `GET /api/v1/svod/1/{sheet}/{year}` и `GET /api/v1/svod/2/{sheet}/{year}` объект `data` каждой строки, включая `META`, SHALL NOT содержать полей со значением `null` на любой глубине вложенности объектов. Объект, у которого все поля были `null`, SHALL остаться пустым объектом `{}`, а не исчезнуть. Элементы массивов, равные `null`, SHALL остаться. Ключи верхнего уровня строки (`row_type`, `depth`, `sort_order`, `data`) SHALL присутствовать всегда. Значения, отличные от `null`, SHALL совпадать с прежними. + +Отчёты и сводные проектов (эндпоинты `/api/v1/projects/...` и `/api/v1/rf-rollup/...`) SHALL по-прежнему отдавать `null` явно. + +#### Scenario: Пустая ячейка листа +- **WHEN** у строки INPUT листа FORM_2/AHR колонка `q1.<поле>` не заполнена и фронт запрашивает лист +- **THEN** в `data.q1` строки нет ключа этого поля, остальные поля `data.q1` присутствуют со своими значениями + +#### Scenario: Сравнение до и после на стенде +- **WHEN** на одних и тех же данных сняты ответы всех листов и сводов 1 и 2 до изменения и после PR B +- **THEN** ответ «после» после `json.loads` равен ответу «до», из `data` которого рекурсивно удалены поля со значением `null` + +#### Scenario: Отчёт проекта не меняется +- **WHEN** после PR B запрашивается `GET /api/v1/projects/{id}/report/{year}/LIMIT` +- **THEN** поля `data` со значением `null` присутствуют, как и до изменения + +### Requirement: Ответы на правку отдают null явно +Ответы путей записи SHALL содержать поля со значением `null` явно, как до изменения: `PATCH /api/v1/form/{id}/sheet/{sheet}/cell`, `PATCH .../cells`, `POST .../line`, `DELETE .../line/{row_id}`, правки FORM_3 в `/api/v1/projects/...` и сообщения WebSocket (`cell_updated` и остальные с `result`). Очищенная ячейка SHALL приходить в ответе как ключ со значением `null`. + +#### Scenario: Очистка ячейки +- **WHEN** пользователь отправляет `PATCH /api/v1/form/{id}/sheet/{sheet}/cell` со значением `null` для заполненной ячейки +- **THEN** в строке ответа есть ключ этой колонки со значением `null` + +#### Scenario: Эхо соавтору +- **WHEN** пользователь очищает ячейку через WebSocket +- **THEN** соавторы получают `cell_updated`, где у этой колонки значение `null`, а не отсутствующий ключ + +### Requirement: Выгрузка Excel не меняется +Выгрузки Excel листов, сводов 1 и 2 и отчётов проектов SHALL формировать то же содержимое, что и до изменения, при тех же данных и параметрах. + +#### Scenario: Выгрузка свода 2 +- **WHEN** после PR A и PR B выгружается свод 2 GO за год +- **THEN** содержимое файла (ячейки и значения) совпадает с выгрузкой до изменения + +### Requirement: Заголовок X-DB-Time-Ms +Каждый успешный ответ тяжёлого GET SHALL содержать заголовок `X-DB-Time-Ms` — неотрицательное число миллисекунд с двумя знаками после точки. Это время от начала обработчика до получения строк из БД. Обработчик SHALL по-прежнему писать в лог запись `db_time` с тем же набором полей. + +#### Scenario: Заголовок на ответе листа +- **WHEN** запрашивается `GET /api/v1/form/{id}/sheet/{sheet}` +- **THEN** в ответе есть заголовок `X-DB-Time-Ms`, его значение разбирается как число ≥ 0 + +#### Scenario: Методика api_split +- **WHEN** после изменения запускается `perf/analysis/api_split.py --scale 1` +- **THEN** для каждого запроса скрипт получает числовое `db_ms` (не `nan`) + +### Requirement: Схема OpenAPI не меняется +В схеме OpenAPI (`/openapi.json`) у тяжёлых GET схема успешного ответа SHALL остаться `BaseListResponse[SheetResponse]`, как до изменения. + +#### Scenario: Схема ответа листа +- **WHEN** запрашивается `/openapi.json` +- **THEN** у `GET /api/v1/form/{form_id}/sheet/{sheet}` схема ответа 200 ссылается на ту же компоненту, что и до изменения + +### Requirement: Время ответа тяжёлых GET +На стенде `perf/` (масштаб 1, те же данные, напрямую на API, медиана 10 повторов после прогрева) после PR B полное время ответа SHALL снизиться относительно версии до изменения: +- `GET /api/v1/svod/2/GO/{year}` (все организации, роль dfip) — не менее чем на 30 %; +- `GET /api/v1/form/{id}/sheet/AHR` для FORM_2 (роль rf) — не менее чем на 10 %. Дальнейшее ускорение этого листа — предмет AURORA-1847 (SQL листа). + +После PR A полное время ни одного запроса из набора `api_split.py` SHALL NOT вырасти больше чем на 5 % от версии до изменения. Доля Python (`(полное − X-DB-Time-Ms) / полное`) — справочная метрика, порогом она не является. + +Пороги этого требования и требования «Размер ответа листа FORM_2/AHR уменьшается» — критерии приёмки задачи, а не условие мержа PR. PR вливается по зелёным тестам и доказанной корректности (требования «Значения ответа совпадают с прежними» и «Ответ листа и сводов без null-полей»: сравнение ответов на стенде в режимах exact и strip-nulls). Корректность — жёсткое условие. Цифры замера дописываются в описание PR, недобор порога оформляется follow-up задачей. + +#### Scenario: Замер свода 2 GO +- **WHEN** `perf/analysis/api_split.py --scale 1 --repeats 10` запускается на версии до изменения и после PR B на одних и тех же данных +- **THEN** медиана `total_ms` свода 2 GO «после» не больше 0,7 × медианы «до» + +#### Scenario: Замер листа FORM_2/AHR +- **WHEN** тот же замер для листа FORM_2/AHR +- **THEN** медиана `total_ms` «после» не больше 0,9 × медианы «до» + +#### Scenario: PR A не замедляет +- **WHEN** тот же замер на версии после PR A +- **THEN** медиана `total_ms` каждого запроса набора не больше 1,05 × медианы «до» + +#### Scenario: Недобор порога +- **WHEN** тесты зелёные, сравнение ответов на стенде прошло, а замер показал недобор порога +- **THEN** PR вливается, цифры фиксируются в описании PR, недобор оформляется follow-up задачей + +### Requirement: Размер ответа листа FORM_2/AHR уменьшается +После PR B распакованный размер ответа `GET /api/v1/form/{id}/sheet/AHR` для FORM_2 на стенде (масштаб 1) SHALL быть меньше, чем до изменения (критерий Jira AURORA-1848; вместе с −10 % времени — критерий по этому листу, согласовано 28.10). + +#### Scenario: Сравнение размеров +- **WHEN** `api_split.py --scale 1` запускается до изменения и после PR B +- **THEN** значение `bytes` листа FORM_2/AHR «после» меньше, чем «до»