docs(plan): aurora-1850 — план реализации
План в 10 задач по design.md: тестовая БД dfip_1850 на 0050 и тела функций до 0051 (D8), красный тест эквивалентности, построители с фокусом и _sort_rank (FORM_2, затем FORM_1 и v_form1_view), _form_affected_rows, дельта АХР, jit = off, защита 0037, документация. Решения плана дописаны в design.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
661a3571b5
commit
93ea5b5c42
@ -0,0 +1,799 @@
|
||||
# AURORA-1850: правка ячейки пересчитывает только затронутое поддерево. План реализации
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Ветка:** `feat/235-aurora-1850-upd-form-cells-subtree` (Gitea #235, Jira AURORA-1850, эпик AURORA-1834).
|
||||
**База стека:** `feat/215-aurora-1844-upd-form-cells-full-rows` (PR #231, миграция 0050). PR этой ветки идёт в базу стека, не в `test`.
|
||||
**Worktree:** `/Users/andrey.epifancev/projects/fomichev_claude/DFiP_Budget_planing/.claude/worktrees/aurora-1850`
|
||||
|
||||
**Goal:** `v3._form_affected_rows` для FORM_1 и FORM_2 перестаёт собирать лист целиком. Ответ на правку строят те же построители, что и GET, но с фокусом на изменённые строки и их предков. `sort_order` при этом остаётся глобальным.
|
||||
|
||||
**Architecture:** Построители `v_form{1,2}_sheet_sections` → `v_form{1,2}_sheet_jsonb` → `v_form2_view` / новая `v_form1_view` получают последним параметром `p_focus_lines integer[] DEFAULT NULL`. NULL — полный лист, `'{}'` — пустой результат. `sort_order` считается в построителе по «скелету» листа (`_sort_rank`) одинаково в обоих режимах, с tie-break D6. `_form_affected_rows` и дельта АХР вызывают построители с фокусом. Всё делается одной миграцией `0051_upd_form_cells_subtree` (DROP старых сигнатур без CASCADE, затем CREATE).
|
||||
|
||||
**Tech Stack:** PostgreSQL (PL/pgSQL, LANGUAGE sql), совместимость с 15.12; Alembic (`run_sql_file_migration`); pytest + FastAPI `TestClient` (интеграционные тесты на PG16, контейнер `perf-pg`).
|
||||
|
||||
**Spec:** `openspec/changes/aurora-1850-upd-form-cells-subtree/` (`proposal.md`, `design.md`, `specs/sheet-cell-edit/spec.md`). Решения D1–D9, Risks, «Тестирование» и «Замер до/после» в `design.md` обязательны. Ссылки вида «D4», «п. 7» ниже ведут туда.
|
||||
|
||||
## Global Constraints
|
||||
|
||||
**Окружение (правила волны 2, обязательны для всех исполнителей и ревьюеров):**
|
||||
- Переменные, которыми пользуется план:
|
||||
- `W=/Users/andrey.epifancev/projects/fomichev_claude/DFiP_Budget_planing/.claude/worktrees/aurora-1850`
|
||||
- `A=$W/.superpowers/aurora-1850` — локальные артефакты, каталог в `.gitignore`;
|
||||
- `PY=/Users/andrey.epifancev/projects/fomichev_claude/.venvs/dfip-api/bin` — свой venv не создавать;
|
||||
- `DBURL=postgresql+asyncpg://perf:perf@localhost:55432/dfip_1850`.
|
||||
- **Замок ночного прогона.** Перед КАЖДЫМ тяжёлым шагом (pytest, alembic, docker, psql в контейнере) выполнить `$A/wait_lock.sh`: он ждёт, пока существует `/private/tmp/claude-501/-Users-andrey-epifancev-projects-fomichev-claude/6fa9c75e-eb11-4e5d-98c1-05eb590ab14f/scratchpad/BENCH_LOCK`. Ожидание запускать с большим timeout или в фоне с уведомлением; если инструмент не даёт ждать долго — повторять проверку. Чтение кода, правки файлов и git-коммиты при замке разрешены. Контроллер передаёт это правило в бриф каждого сабагента.
|
||||
- БД: только `dfip_1850`, `dfip_1850_base` и `dfip_1850_snap` в контейнере `perf-pg` (порт 55432). **БД `perf` не трогать никогда.** Стенд `perf/` и порты 8000/8080/8081/8082 не запускать. Замеры делает главный контроллер.
|
||||
- Полный pytest: `cd $W/api && DATABASE_URL=$DBURL $PY/pytest -q -p no:cacheprovider -W ignore`. Исходно падают ровно 3 чужих теста: `test_backend_calls_form3_add_project_and_add_line_functions` и два в `test_export_service_query_params.py`. Новых падений быть не должно.
|
||||
- PG 15.12 проверяет главный контроллер (контейнер `dfip-pg15`, порт 55434), исполнитель туда не ходит. SQL пишется без возможностей PG16: без `any_value`, у каждого подзапроса в FROM есть алиас.
|
||||
- Карта кода — `mcp__codegraph__codegraph_explore` с `projectPath=$W`. Если ответ пуст, один раз отметить это в ledger и дальше пользоваться Read/Grep.
|
||||
- `.bounded` не создавать: правки в `api/alembic` разрешает этот план.
|
||||
|
||||
**Код (из design.md):**
|
||||
- Миграция: `api/alembic/versions/0051_upd_form_cells_subtree.py` (`revision = "0051"`, `down_revision = "0050"`, `downgrade()` пустой) и `api/alembic/versions/sql/0051_upd_form_cells_subtree.sql`.
|
||||
- `api/src`, `web/`, `perf/` не меняются. Сигнатуры `v_form_view`, `_form_affected_rows`, `upd_form_cell(s)` и `get_form2_ahr_satellite_delta` прежние.
|
||||
- `p_focus_lines integer[] DEFAULT NULL` — последний параметр у `v_form2_sheet_sections`, `v_form2_sheet_jsonb`, `v_form2_view`, `v_form1_sheet_sections`, `v_form1_sheet_jsonb` и `v_form1_view`. `NULL` — полный лист, `'{}'` — пустой результат. Вызывающие передают `COALESCE(..., '{}')`.
|
||||
- Узлы фокуса: `_expense_item_path_set(eid строк фокуса)` ∩ `tw`. eid берутся из `budget_line` без фильтра `direction`. INPUT фокуса — строки фокуса, прошедшие обычный фильтр `page`.
|
||||
- `_sort_rank bigint` — новая последняя выходная колонка `*_sheet_sections` и `*_sheet_jsonb`. Это `ROW_NUMBER() OVER (ORDER BY _sort, line_id NULLS FIRST)` по скелету: все узлы `tw` плюс все строки `page ⋈ tw`, без jsonb и сумм. Скелет одинаков в обоих режимах. View берут `sort_order` из `_sort_rank`. Скелет (D2) без контроллера не откатывать.
|
||||
- OTCH9F: ранг `ROW_NUMBER() OVER (ORDER BY expense_item_id, month, id)`.
|
||||
- Порядок в 0051 (D9): сначала все `DROP FUNCTION` без CASCADE и без `IF EXISTS` — `v_form2_view`, `v_form2_sheet_jsonb`, `v_form2_sheet_sections`, `v_form1_sheet_jsonb`, `v_form1_sheet_sections`. Затем `CREATE` по порядку: `*_sheet_sections`, `*_sheet_jsonb`, `v_form2_view`, `v_form1_view`, `v_form_view` (`CREATE OR REPLACE`), `_form_affected_rows` (`CREATE OR REPLACE`), `get_form2_ahr_satellite_delta` (`CREATE OR REPLACE`).
|
||||
- Формат SQL-файла (`api/alembic/sql_migration.py`): строки, начинающиеся с `--`, выбрасываются. `AS $function$` и закрывающий `$function$` стоят на разных строках, закрывающий — в начале строки. После каждого оператора идёт `;`.
|
||||
- Тела функций — буквальные, взятые из `pg_get_functiondef` с `dfip_1850` на ревизии 0050 (файл `before-0051.sql`, Task 1) и правленные вручную. `regexp_replace` поверх `pg_get_functiondef` не используется (D8).
|
||||
- `SET jit = off` ставится только на `v_form2_sheet_sections`, `v_form1_sheet_sections`, `_form_affected_rows` и `get_form2_ahr_satellite_delta`. Конфигурацию сервера и `work_mem` не трогать.
|
||||
- `_form_affected_rows` объявляется `STABLE`. Ветка FORM_4 и листы, которые не редактируются (SMETA), остаются на полной сборке.
|
||||
|
||||
**Стоп и итерации (design.md, Risks; решение 9):**
|
||||
- **Нарушение корректности — стоп и эскалация контроллеру, без обходов.** Сюда относятся: расхождение эквивалентности (ответ на правку ≠ полный GET), расхождение снимка GET до и после миграции (кроме строк с совпадающим ключом, D6), падение защиты 0037, падение миграции на PG15.
|
||||
- **Регресс производительности** (GET больше +10 %, правка больше 50 мс, очередь на rate 5) мерж не блокирует. Итерировать можно **только на пути правки** и строго по шагам:
|
||||
1. `SET plan_cache_mode = force_custom_plan` на `_form_affected_rows` и дельте;
|
||||
2. снять `SET jit = off` с построителей, оставив его на `_form_affected_rows` и дельте;
|
||||
3. если не помогло — стоп и вопрос контроллеру с `EXPLAIN (ANALYZE, BUFFERS)` запроса правки и GET.
|
||||
|
||||
Недобор записывается в PR и уходит в follow-up.
|
||||
|
||||
**Коммиты, PR, маркировка:**
|
||||
- Стиль `perf(sql): …`, `test(sheet): …`, `docs(...): …`, по-русски, `git add` только конкретных файлов. Последняя строка — `Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>`. Не мержить, не пушить в `test`/`main`, `git stash` без тега не использовать.
|
||||
- Решение, принятое без обсуждения, дописывается строкой в `openspec/changes/aurora-1850-upd-form-cells-subtree/design.md` → `## Decisions made without discussion`.
|
||||
- Маркеры: `T<n>.<k>` — minor-находки ревью задачи n; `C-n`/`I-n`/`M-n` — финальное ревью; `R-n` — рулинги контроллера (рядом номер строки в design.md); `O-n` — наблюдения; `H-n` — замечания человека в PR. Статус каждой находки — `fixed <commit>`, `deferred` или `rejected <причина>`. Таблица находок ведётся в ledger, раздел `## Findings`.
|
||||
|
||||
## Review Focus
|
||||
|
||||
1. **Пустой фокус превращается в полный лист.** `array_agg` по пустому набору даёт NULL, и вызов с `p_focus_lines = NULL` вернёт весь лист. Ожидание: `'{}'` даёт 0 строк у каждого построителя, у `_form_affected_rows` и у дельты. Тест `test_empty_focus_returns_nothing` (Task 2).
|
||||
2. **Строка FORM_1 чужого направления в пакете.** Правка строки Development при `direction=Support` возвращает предков (eid без фильтра направления), но не саму строку. Тест: случай FORM_1/AHR/Support в `test_affected_rows_match_reference` включает в пакет строку Development (Task 2).
|
||||
3. **OTCH9F с `equipment_name IS NULL`.** Разделы окна по NULL-оборудованию должны совпасть с полным листом: в фильтре `base` нужен `IS NOT DISTINCT FROM`, а не `=`. Тестовые данные OTCH9F содержат оборудование NULL (Task 2), реализация в Task 4.
|
||||
4. **Скелет сателлитов теряет или добавляет строки относительно GET.** В GET есть `JOIN v3.budget_line` и `JOIN v3.vsp`. Если скелет нумерует по `rent_detail` без того же соединения, `sort_order` сдвинется. Ожидание: в скелете тот же набор строк, что в GET. Тест: эквивалентность сателлитов по каждой строке (Task 2), реализация в Task 4.
|
||||
5. **Фокус при `sections`.** С `p_sections` построитель в режиме фокуса должен давать те же байты, что полный лист с тем же `sections`, а `_form_affected_rows` должен вырезать ключи исключённых разделов. Тест: случай `sections=['plan']` в `test_affected_rows_match_reference` (Task 2) и существующий `test_sections_filter_drops_other_sections_keys`.
|
||||
|
||||
---
|
||||
|
||||
## Карта файлов
|
||||
|
||||
| Файл | Что | Задачи |
|
||||
|---|---|---|
|
||||
| `$A/wait_lock.sh`, `$A/reset_db.sh`, `$A/snap.sh`, `$A/snap_seed.sql`, `$A/snap_query.sql` | локальные скрипты: замок, сброс тестовой БД, снимок GET; не коммитятся | 1 |
|
||||
| `openspec/changes/aurora-1850-upd-form-cells-subtree/before-0051.sql` | тела 8 функций с ревизии 0050 (`pg_get_functiondef`): исходник для D8, база для ревью и отката | 1 |
|
||||
| `api/tests/integration/sheet_contract.py` | SQL-хелперы эквивалентности: эталон 0050 поверх `v_form_view`, фактический ответ, сравнение | 2 |
|
||||
| `api/tests/integration/test_upd_form_cells_subtree.py` | тесты эквивалентности, отсутствия полной сборки, пустого фокуса, `sort_order`, дельты АХР, JIT | 2, 8 |
|
||||
| `api/tests/integration/test_upd_form_cells_subtree_0037_guard.py` | защита правки 0037 | 9 |
|
||||
| `api/alembic/versions/0051_upd_form_cells_subtree.py` | обёртка миграции | 3 |
|
||||
| `api/alembic/versions/sql/0051_upd_form_cells_subtree.sql` | все функции изменения | 3–8 |
|
||||
| `ARCHITECTURE.md` | абзац о `_form_affected_rows` | 10 |
|
||||
| `openspec/changes/aurora-1850-upd-form-cells-subtree/design.md` | решения без обсуждения (дописываются по ходу) | все |
|
||||
|
||||
## Модели исполнителей
|
||||
|
||||
| Задача | Модель | Почему |
|
||||
|---|---|---|
|
||||
| 1. Подготовка БД, тела функций, снимок GET | sonnet | окружение и SQL-посев по описанию |
|
||||
| 2. Красные тесты эквивалентности | sonnet | подбор данных фикстуры, интеграция с API |
|
||||
| 3. FORM_2 AHR/CAP/OPER: фокус и `_sort_rank` | sonnet | SQL-интеграция, 700-строчный построитель |
|
||||
| 4. FORM_2: сателлиты, AHR_LIMIT, OTCH9F | sonnet | SQL-интеграция |
|
||||
| 5. FORM_1: фокус, `_sort_rank`, `v_form1_view` | sonnet | SQL-интеграция |
|
||||
| 6. `_form_affected_rows` | sonnet | SQL-интеграция |
|
||||
| 7. Дельта АХР | haiku | тело задано планом и дампом |
|
||||
| 8. JIT на функциях | haiku | готовый тест и атрибуты |
|
||||
| 9. Защита 0037 | haiku | готовые тесты |
|
||||
| 10. ARCHITECTURE, прогон с нуля, снимок GET | haiku | команды и текст заданы |
|
||||
| Ревью задач | sonnet | — |
|
||||
| Финальное ревью всей ветки | **opus** | — |
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Тестовая БД на ревизии 0050, тела функций, снимок GET «до»
|
||||
|
||||
Шаг идёт **до** создания 0051 (D8, «Тестирование»).
|
||||
|
||||
**Files:**
|
||||
- Create (не коммитятся): `$A/wait_lock.sh`, `$A/reset_db.sh`, `$A/snap.sh`, `$A/snap_seed.sql`, `$A/snap_query.sql`, `$A/get-before.tsv`
|
||||
- Create: `openspec/changes/aurora-1850-upd-form-cells-subtree/before-0051.sql`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces:
|
||||
- БД `dfip_1850_base` (ревизия 0050 + `tests/sqls/fixture.sql`) — шаблон для сброса;
|
||||
- `$A/reset_db.sh` — пересоздаёт `dfip_1850` из шаблона и выполняет `alembic upgrade head`;
|
||||
- `$A/snap.sh before|after` — снимок GET на засеянной `dfip_1850_snap`;
|
||||
- `before-0051.sql` — исходные тела функций.
|
||||
|
||||
- [ ] **Step 1: Скрипты замка и сброса**
|
||||
|
||||
`$A/wait_lock.sh`:
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
L=/private/tmp/claude-501/-Users-andrey-epifancev-projects-fomichev-claude/6fa9c75e-eb11-4e5d-98c1-05eb590ab14f/scratchpad/BENCH_LOCK
|
||||
while [ -e "$L" ]; do sleep 60; done
|
||||
```
|
||||
`$A/reset_db.sh`:
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
A="$(cd "$(dirname "$0")" && pwd)"; W="$(cd "$A/../.." && pwd)"
|
||||
PY=/Users/andrey.epifancev/projects/fomichev_claude/.venvs/dfip-api/bin
|
||||
"$A/wait_lock.sh"
|
||||
docker exec perf-pg dropdb -U perf --if-exists dfip_1850
|
||||
docker exec perf-pg createdb -U perf -T dfip_1850_base dfip_1850
|
||||
cd "$W/api" && DATABASE_URL=postgresql+asyncpg://perf:perf@localhost:55432/dfip_1850 "$PY/alembic" upgrade head
|
||||
```
|
||||
`chmod +x` на оба.
|
||||
|
||||
- [ ] **Step 2: Базовый прогон на 0050 и шаблон**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
$A/wait_lock.sh && docker exec perf-pg dropdb -U perf --if-exists dfip_1850 && docker exec perf-pg createdb -U perf dfip_1850 \
|
||||
&& cd $W/api && DATABASE_URL=$DBURL $PY/pytest --init_db -q -p no:cacheprovider -W ignore
|
||||
```
|
||||
Expected: падают ровно 3 исходных теста, тесты `test_upd_form_cells_full_rows.py` проходят. Итоговую строку записать в ledger.
|
||||
|
||||
Затем создать шаблон:
|
||||
```bash
|
||||
docker exec perf-pg dropdb -U perf --if-exists dfip_1850_base && docker exec perf-pg createdb -U perf -T dfip_1850 dfip_1850_base
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Снять тела функций**
|
||||
|
||||
```bash
|
||||
$A/wait_lock.sh && docker exec perf-pg psql -U perf -d dfip_1850 -At -c "
|
||||
SELECT string_agg(pg_get_functiondef(CAST(s AS regprocedure)) || ';', E'\n\n' ORDER BY o)
|
||||
FROM unnest(ARRAY[
|
||||
'v3.v_form2_sheet_sections(integer,character varying,text[])',
|
||||
'v3.v_form2_sheet_jsonb(integer,character varying,text[])',
|
||||
'v3.v_form2_view(integer,character varying,text[])',
|
||||
'v3.v_form1_sheet_sections(integer,character varying,character varying,text[])',
|
||||
'v3.v_form1_sheet_jsonb(integer,character varying,character varying,text[])',
|
||||
'v3.v_form_view(integer,character varying,text[],character varying,integer)',
|
||||
'v3._form_affected_rows(integer,character varying,integer[],character varying,text[])',
|
||||
'v3.get_form2_ahr_satellite_delta(integer,character varying,integer)'
|
||||
]) WITH ORDINALITY AS t(s, o)" > $W/openspec/changes/aurora-1850-upd-form-cells-subtree/before-0051.sql
|
||||
```
|
||||
Первой строкой файла дописать комментарий: `-- pg_get_functiondef с dfip_1850, ревизия 0050 (AURORA-1850, D8). Не миграция: исходник для 0051, база ревью и отката.`
|
||||
|
||||
Проверить, что дамп снят после 0037:
|
||||
```bash
|
||||
grep -c "_form2_ahr_limit_amount" before-0051.sql # ≥ 4 (v_form2_view, AHR_LIMIT)
|
||||
grep -n "ei.num_group_id = lt.expense_item_code" before-0051.sql # есть в get_form2_ahr_satellite_delta
|
||||
```
|
||||
Если хотя бы одна проверка не проходит — стоп и эскалация: значит, БД не на 0050.
|
||||
|
||||
- [ ] **Step 4: Найти вызывающих построители**
|
||||
|
||||
```bash
|
||||
docker exec perf-pg psql -U perf -d dfip_1850 -At -c "
|
||||
SELECT p.oid::regprocedure FROM pg_proc p JOIN pg_namespace n ON n.oid = p.pronamespace
|
||||
WHERE n.nspname = 'v3' AND p.prosrc ~ '(v_form[12]_sheet_(sections|jsonb)|v_form2_view)\s*\('"
|
||||
```
|
||||
Записать список в ledger. Для каждой функции вне списка из 8 проверить по `prosrc`, что вызов идёт по позиции не больше чем с прежним числом аргументов и не через `SELECT *` в запись фиксированной формы. Иначе новая колонка `_sort_rank` её сломает. Ожидается, например, `v3.v_form1_smeta`: она выбирает колонки по имени, это безопасно. Если найдена опасная — стоп и вопрос контроллеру.
|
||||
|
||||
- [ ] **Step 5: Снимок GET «до» на засеянных данных**
|
||||
|
||||
`$A/snap_seed.sql` — один `DO`-блок, детерминированный (строки вставляются `ORDER BY`):
|
||||
- создать формы `v3.add_budget_form('FORM_1', 2050, 1)` и `v3.add_budget_form('FORM_2', 2050, 1)`;
|
||||
- на листах AHR/CAP/OPER каждой формы вставить `budget_line` на каждую третью листовую статью (`row_number() OVER (PARTITION BY form, sheet ORDER BY ei.id) % 3 = 1`), привязанную к типу формы через `expense_item_form_type`. `direction` = `ei.direction` для FORM_1 и NULL для FORM_2. Обязательные колонки `budget_line` добавить по схеме таблицы;
|
||||
- по каждой паре (форма, лист, направление) выполнить `PERFORM * FROM v3.upd_form_cells(form, sheet, <jsonb-массив {line_id, column:'plan.q1', value: id % 97 + 1}>, direction)`, а для половины строк ещё и `plan.q3`;
|
||||
- вставить в `v3.fixed_asset_report` формы FORM_2: две статьи, которые попадают в ветку OTCH9F `v_form2_view` (`JOIN v3.expense_item`); у первой — оборудование `'E1'` за месяцы 1–3, `'E2'` за месяц 1 и `NULL` за месяцы 1–2; у второй — `'E1'` за месяц 1. Заполнить `opening_*`, `acquired_*` и `disposed_*`;
|
||||
- `INSERT INTO v3.form_limit (budget_form_id, template_id, qty_q1)` — для трёх LEAF шаблона, включая `id = 13`.
|
||||
|
||||
`$A/snap_query.sql`:
|
||||
```sql
|
||||
WITH s(ft, sheet, dir) AS (VALUES
|
||||
('FORM_1','AHR','Support'),('FORM_1','AHR','Development'),('FORM_1','CAP','Support'),
|
||||
('FORM_1','CAP','Development'),('FORM_1','OPER',NULL),('FORM_1','SMETA',NULL),
|
||||
('FORM_2','AHR',NULL),('FORM_2','CAP',NULL),('FORM_2','OPER',NULL),('FORM_2','AHR_LIMIT',NULL),
|
||||
('FORM_2','AHR_RENT',NULL),('FORM_2','AHR_UTILITY',NULL),('FORM_2','AHR_SECURITY',NULL),
|
||||
('FORM_2','OTCH9F',NULL),('FORM_2','SMETA',NULL))
|
||||
SELECT bf.id, s.sheet, COALESCE(s.dir, '-'), v.sort_order, v.row_type, v.depth, v.data::text
|
||||
FROM v3.budget_form bf
|
||||
JOIN s ON s.ft = bf.form_type_code
|
||||
CROSS JOIN LATERAL v3.v_form_view(bf.id, s.sheet, NULL, s.dir) v
|
||||
ORDER BY 1, 2, 3, 4;
|
||||
```
|
||||
`$A/snap.sh`:
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
A="$(cd "$(dirname "$0")" && pwd)"; W="$(cd "$A/../.." && pwd)"
|
||||
PY=/Users/andrey.epifancev/projects/fomichev_claude/.venvs/dfip-api/bin
|
||||
PSQL="docker exec -i perf-pg psql -U perf -v ON_ERROR_STOP=1 -q"
|
||||
"$A/wait_lock.sh"
|
||||
docker exec perf-pg dropdb -U perf --if-exists dfip_1850_snap
|
||||
docker exec perf-pg createdb -U perf -T dfip_1850_base dfip_1850_snap
|
||||
$PSQL -d dfip_1850_snap < "$A/snap_seed.sql"
|
||||
if [ "${1:?before|after}" = after ]; then
|
||||
(cd "$W/api" && DATABASE_URL=postgresql+asyncpg://perf:perf@localhost:55432/dfip_1850_snap "$PY/alembic" upgrade head)
|
||||
fi
|
||||
$PSQL -d dfip_1850_snap -At -F $'\t' < "$A/snap_query.sql" > "$A/get-$1.tsv"
|
||||
wc -l "$A/get-$1.tsv"
|
||||
```
|
||||
Run: `$A/snap.sh before`, затем `$A/snap.sh before` ещё раз, сохранив первый результат под другим именем, и `diff` двух файлов.
|
||||
Expected: `get-before.tsv` содержит не меньше 500 строк, повторный прогон даёт идентичный файл (посев детерминирован).
|
||||
|
||||
- [ ] **Step 6: Коммит**
|
||||
|
||||
```bash
|
||||
git add openspec/changes/aurora-1850-upd-form-cells-subtree/before-0051.sql
|
||||
git commit -m "docs(openspec): aurora-1850 — тела функций до 0051 (pg_get_functiondef, ревизия 0050)
|
||||
|
||||
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Красные тесты эквивалентности
|
||||
|
||||
**Files:**
|
||||
- Modify: `api/tests/integration/sheet_contract.py` (добавить SQL-хелперы; `row_key` и `assert_patch_rows_match_sheet` не трогать)
|
||||
- Create: `api/tests/integration/test_upd_form_cells_subtree.py`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes:
|
||||
- фикстуры `headers` и `db_exec(sql: str, **params) -> list[Row]`;
|
||||
- хелперы `_new_form`, `_add_line`, `_get_sheet`, `_patch_cell`, `_qs` из `test_upd_form_cells_full_rows.py` — импортировать оттуда, не копировать.
|
||||
- Produces (в `sheet_contract.py`):
|
||||
- `Row4 = tuple[str, int, int, str]` — `(row_type, depth, sort_order, data::text)`;
|
||||
- `strip_keys(sections: list[str] | None) -> list[str]` — тот же список из 22 ключей, что `v_strip` в 0050;
|
||||
- `sheet_kind(sheet: str) -> str` — `'sat'` для AHR_RENT/AHR_UTILITY/AHR_SECURITY, `'limit'`, `'otch9f'`, иначе `'tree'`;
|
||||
- `reference_affected_rows(db_exec, form_id, sheet, ids, direction=None, sections=None, strip=True) -> list[Row4]`;
|
||||
- `affected_rows(db_exec, form_id, sheet, ids, direction=None, sections=None) -> list[Row4]`;
|
||||
- `focus_builder_rows(db_exec, form_type, form_id, sheet, ids, direction=None, sections=None) -> list[Row4]`;
|
||||
- `assert_rows_equal(actual: list[Row4], expected: list[Row4], label: str) -> None` — при расхождении сообщение называет первую отличающуюся строку и длины списков.
|
||||
|
||||
- [ ] **Step 1: Хелперы в `sheet_contract.py`**
|
||||
|
||||
Эталон — фильтр 0050 поверх полного `v_form_view`. Это оракул: запрос задаётся буквально, параметры приводятся явно (asyncpg).
|
||||
```python
|
||||
_REF_FROM = """
|
||||
SELECT v.row_type, v.depth, v.sort_order, (v.data - CAST(:strip AS text[]))::text
|
||||
FROM v3.v_form_view(:f, :s, CAST(:sec AS text[]), CAST(:d AS varchar)) v
|
||||
WHERE {where}
|
||||
ORDER BY v.sort_order"""
|
||||
_REF_WHERE = {
|
||||
"sat": "v.row_type = 'INPUT' AND (v.data->>'id')::int = ANY(CAST(:ids AS int[]))",
|
||||
"limit": "(v.data->>'id')::int = ANY(v3._limit_template_path_set(CAST(:ids AS int[])))",
|
||||
"otch9f": """EXISTS (SELECT 1 FROM v3.fixed_asset_report far
|
||||
WHERE far.id = ANY(CAST(:ids AS int[]))
|
||||
AND far.expense_item_id = (v.data->>'expense_item_id')::int
|
||||
AND far.equipment_name IS NOT DISTINCT FROM (v.data->>'equipment_name'))""",
|
||||
"tree": """(v.row_type IN ('ROOT','GROUP','ITEM','SUB_ITEM','SUB_SUB_ITEM')
|
||||
AND (v.data->'header'->>'expense_item_id')::int = ANY(v3._expense_item_path_set(
|
||||
ARRAY(SELECT DISTINCT bl.expense_item_id FROM v3.budget_line bl
|
||||
WHERE bl.id = ANY(CAST(:ids AS int[]))))))
|
||||
OR (v.row_type = 'INPUT' AND (v.data->>'line_id')::int = ANY(CAST(:ids AS int[])))""",
|
||||
}
|
||||
```
|
||||
- `affected_rows` — `SELECT r.row_type, r.depth, r.sort_order, r.data::text FROM v3._form_affected_rows(:f, :s, CAST(:ids AS int[]), CAST(:d AS varchar), CAST(:sec AS text[])) r ORDER BY r.sort_order`.
|
||||
- `focus_builder_rows`:
|
||||
- для FORM_2 — `v3.v_form2_view(:f, :s, CAST(:sec AS text[]), CAST(:ids AS int[]))`;
|
||||
- для FORM_1 — `v3.v_form1_view(:f, :s, CAST(:sec AS text[]), CAST(:d AS varchar), CAST(:ids AS int[]))`;
|
||||
- результат `ORDER BY sort_order`; сравнивается с `reference_affected_rows(..., strip=False)`.
|
||||
|
||||
- [ ] **Step 2: Тесты в `test_upd_form_cells_subtree.py`**
|
||||
|
||||
Годы новых форм — 2041–2060, у каждого теста свой. Тесты:
|
||||
|
||||
1. `test_affected_rows_match_reference[path, case]` — `path ∈ {"affected", "builder"}`. Случаи `case`:
|
||||
- FORM_1: AHR/Support, AHR/Development, CAP/Support, OPER/`None`;
|
||||
- FORM_2: AHR, CAP, OPER, AHR_RENT, AHR_UTILITY, AHR_SECURITY, AHR_LIMIT, OTCH9F;
|
||||
- отдельно FORM_2/AHR с `sections=['plan']`.
|
||||
|
||||
Данные листов с деревом:
|
||||
- новая форма;
|
||||
- строки на первой и последней листовой статье по `mv_expense_item_tree.path` среди привязанных к типу формы;
|
||||
- две строки-сестры с общим родителем;
|
||||
- строка в другом ROOT;
|
||||
- у всех строк `plan.q1` (через PATCH), у одной ещё `header.justification`;
|
||||
- для FORM_1/AHR/Support дополнительно строка с `direction=Development` (Review Focus 2).
|
||||
|
||||
Данные остальных листов:
|
||||
- сателлиты: форма 4, по 3 строки через `POST .../line` (`vsp_id`, `contract_number`; обязательные поля UTILITY и SECURITY взять из ветки `v3.add_budget_line`), PATCH `plan.q1`;
|
||||
- AHR_LIMIT: форма 4, PATCH `qty_q1` у LEAF 13 и ещё у двух LEAF;
|
||||
- OTCH9F: форма 4, `INSERT` через `db_exec` в `v3.fixed_asset_report` — та же раскладка, что в `snap_seed.sql` (Task 1), включая `equipment_name IS NULL` (Review Focus 3).
|
||||
|
||||
Проверки: для **каждой** строки листа по отдельности (id INPUT-строк из GET; для AHR_LIMIT — все LEAF, для сателлитов — все `data.id`) `assert_rows_equal(actual, reference, label=f"{case} id={i}")`. Для `path="affected"` фактический результат — `affected_rows`, для `path="builder"` — `focus_builder_rows` при `strip=False`. Дополнительно проверяется пакет `[первая, последняя, сестра, строка другого ROOT]` (для FORM_1/AHR/Support — плюс строка Development) и пакет с повтором id.
|
||||
2. `test_edit_path_does_not_call_v_form_view[case]` — случаи FORM_1/AHR/Support, FORM_2/AHR, AHR_RENT, AHR_LIMIT, OTCH9F и `delta`.
|
||||
- Если `SELECT rolsuper FROM pg_roles WHERE rolname = current_user` ложно — `pytest.skip("track_functions требует суперпользователя")`.
|
||||
- Иначе: `SET LOCAL track_functions = 'all'`, затем `n0 = COALESCE(pg_stat_get_xact_function_calls(CAST('v3.v_form_view(integer,character varying,text[],character varying,integer)' AS regprocedure)), 0)`.
|
||||
- Вызвать `_form_affected_rows` (для `delta` — `get_form2_ahr_satellite_delta(4, 'AHR_RENT', <id>)`).
|
||||
- `assert n1 - n0 == 0`.
|
||||
3. `test_empty_focus_returns_nothing` — 0 строк у каждого из вызовов:
|
||||
- `v3.v_form2_view(4, s, NULL, '{}')` для s ∈ {AHR, CAP, OPER, AHR_RENT, AHR_UTILITY, AHR_SECURITY, AHR_LIMIT, OTCH9F};
|
||||
- `v3.v_form1_view(1, 'AHR', NULL, 'Support', '{}')`;
|
||||
- `v3._form_affected_rows(4, 'AHR', '{}')`;
|
||||
- `v3.get_form2_ahr_satellite_delta(4, 'AHR_RENT', -1)`.
|
||||
4. `test_patch_cells_two_branches_matches_get` — `PATCH /cells` по строкам двух ROOT формы FORM_2/AHR. `assert_patch_rows_match_sheet`; в ответе обе INPUT; ключи `row_key` без дублей.
|
||||
5. `test_patch_after_add_and_delete_matches_get[FORM_2/AHR, FORM_1/AHR/Support]` — добавить 5 строк, удалить третью сверху (`DELETE .../line/{id}`), сделать PATCH строки ниже неё. `assert_patch_rows_match_sheet(patch, GET)`; у каждой строки ответа `sort_order` совпадает с `sort_order` строки GET с тем же `line_id` (для INPUT) или `header.expense_item_id` (для узлов).
|
||||
6. `test_patch_form1_development_matches_get` — PATCH на FORM_1/AHR с `direction=Development`, `assert_patch_rows_match_sheet` с GET того же направления.
|
||||
7. `test_patch_otch9f_returns_all_months_of_equipment` — PATCH месячного поступления (колонку взять из ветки OTCH9F `_apply_form_cell`) у строки `'E1'` месяца 1. Ответ = все строки `(статья, 'E1')`, остатки пересчитаны, `assert_patch_rows_match_sheet` с GET OTCH9F.
|
||||
8. `test_sort_key_tie_node_before_line` — новая форма FORM_2.
|
||||
- Найти статью AHR глубины 3, у которой есть дочерняя статья глубины 4, и обе привязаны к FORM_2; id дочерней не занят в `budget_line`. Если такой нет — `skip` с причиной.
|
||||
- `INSERT INTO v3.budget_line (id, budget_form_id, expense_item_id, name, …) VALUES (<id дочерней>, …)` через `db_exec`, последовательность не трогать.
|
||||
- Проверки: два GET дают одинаковый порядок; узел дочерней статьи стоит непосредственно перед этой строкой INPUT; PATCH `plan.q1` этой строки совпадает с GET (`assert_patch_rows_match_sheet`).
|
||||
9. `test_ahr_delta_matches_get_after_rent_edit` — форма 4: POST строки AHR_RENT, PATCH `plan.q1 = 100`. Для `SELECT row_type, depth, sort_order, data::text FROM v3.get_form2_ahr_satellite_delta(4, 'AHR_RENT', :id)` каждая строка равна строке с тем же `sort_order` из `v3.v_form_view(4, 'AHR', NULL, 'Support')` (тоже `data::text`). Среди строк есть INPUT с `plan.q1 = 100`.
|
||||
10. `test_ahr_delta_includes_sub_sub_item_for_depth4_limit` — форма 4.
|
||||
- Найти LEAF шаблона, у которого `expense_item_code` равен `num_group_id` статьи AHR/Support глубины 4. Ожидается LEAF 13, код `204620030`. Если такого нет — `skip`.
|
||||
- PATCH `qty_q1 = 4` этого LEAF на AHR_LIMIT (синхронизация создаёт строку AHR).
|
||||
- Дельта `(4, 'AHR_LIMIT', <lt.id>)` непуста и построчно совпадает с GET AHR, как в п. 9. В ней есть строка `SUB_SUB_ITEM` с `header.expense_item_id` статьи глубины 4.
|
||||
|
||||
- [ ] **Step 3: Убедиться, что тесты красные по правильной причине**
|
||||
|
||||
Run: `$A/wait_lock.sh && cd $W/api && DATABASE_URL=$DBURL $PY/pytest tests/integration/test_upd_form_cells_subtree.py -q -p no:cacheprovider -W ignore`
|
||||
Expected на 0050:
|
||||
- **FAIL**: все `path=builder` (`function v3.v_form2_view(integer, …, integer[]) does not exist` или `v3.v_form1_view … does not exist`); `test_edit_path_does_not_call_v_form_view` (`n1 - n0 == 1`), если он не skip; `test_empty_focus_returns_nothing` (нет сигнатуры); `test_ahr_delta_includes_sub_sub_item_for_depth4_limit` (нет SUB_SUB_ITEM).
|
||||
- **PASS** (характеризация 0050, подтверждает эталон): все `path=affected`, тесты 4–7 и 9.
|
||||
- Тест 8 до D6 может пройти или упасть случайно, это нормально.
|
||||
|
||||
Если какой-то `path=affected` падает, ошибка в эталоне или в данных: исправить тест, а не трогать SQL.
|
||||
|
||||
- [ ] **Step 4: Коммит**
|
||||
|
||||
```bash
|
||||
git add api/tests/integration/sheet_contract.py api/tests/integration/test_upd_form_cells_subtree.py
|
||||
git commit -m "test(sheet): эквивалентность ответа на правку и полной сборки — красные тесты (AURORA-1850)
|
||||
|
||||
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Миграция 0051 и FORM_2 AHR/CAP/OPER — фокус и `_sort_rank`
|
||||
|
||||
**Files:**
|
||||
- Create: `api/alembic/versions/0051_upd_form_cells_subtree.py`
|
||||
- Create: `api/alembic/versions/sql/0051_upd_form_cells_subtree.sql`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: тела `v_form2_sheet_sections`, `v_form2_sheet_jsonb` и `v_form2_view` из `before-0051.sql`.
|
||||
- Produces:
|
||||
- `v3.v_form2_sheet_sections(p_form_id integer, p_sheet character varying, p_sections text[] DEFAULT NULL, p_focus_lines integer[] DEFAULT NULL)` — `RETURNS TABLE(<прежние колонки до _sort_path включительно>, _sort_rank bigint)`, plpgsql, STABLE;
|
||||
- `v3.v_form2_sheet_jsonb(p_form_id integer, p_sheet character varying, p_sections text[] DEFAULT NULL, p_focus_lines integer[] DEFAULT NULL)` — `RETURNS TABLE(<прежние>, _sort_rank bigint)`, LANGUAGE sql, проекция с передачей `p_focus_lines`;
|
||||
- `v3.v_form2_view(p_form_id integer, p_sheet character varying, p_sections text[] DEFAULT NULL, p_focus_lines integer[] DEFAULT NULL)` — `RETURNS TABLE(row_type varchar, depth integer, sort_order bigint, data jsonb)`. Ветка AHR/CAP/OPER: `sort_order = j._sort_rank`, `ORDER BY j._sort_rank`. Остальные ветки в этой задаче — без изменений, тело из дампа.
|
||||
|
||||
- [ ] **Step 1: Обёртка миграции**
|
||||
|
||||
```python
|
||||
from sql_migration import run_sql_file_migration
|
||||
|
||||
|
||||
revision = "0051"
|
||||
down_revision = "0050"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
run_sql_file_migration("0051_upd_form_cells_subtree.sql")
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
pass
|
||||
```
|
||||
|
||||
- [ ] **Step 2: SQL — DROP и три функции FORM_2**
|
||||
|
||||
Файл начинается с комментария со ссылкой на AURORA-1850 и change. Затем:
|
||||
```sql
|
||||
DROP FUNCTION v3.v_form2_view(integer, character varying, text[]);
|
||||
DROP FUNCTION v3.v_form2_sheet_jsonb(integer, character varying, text[]);
|
||||
DROP FUNCTION v3.v_form2_sheet_sections(integer, character varying, text[]);
|
||||
```
|
||||
(DROP FORM_1 добавит Task 5 сразу после этих строк.) Затем `CREATE FUNCTION` в порядке sections → jsonb → view. Тела копируются из дампа и правятся так (D1, D2, D6):
|
||||
- в DECLARE считать `v_focus_nodes int[]` и `v_focus_eids int[]` (объединение `desc_ids` узлов фокуса из `v3.mv_expense_item_tree`), только если `p_focus_lines IS NOT NULL`. При пустом фокусе — `'{}'`, а не NULL;
|
||||
- `tw` и `page` остаются полными: из них строится скелет. Рядом появляются отфильтрованные `tw_f` (`p_focus_lines IS NULL OR id = ANY(v_focus_nodes)`) и `page_f` (`p_focus_lines IS NULL OR lid = ANY(p_focus_lines)`);
|
||||
- `jp..jq4`, `input_base` и `final_input` строятся по `page_f`;
|
||||
- `agg` и `aq_q` ограничиваются `p_focus_lines IS NULL OR eid = ANY(v_focus_eids)`;
|
||||
- `tw_agg` и `tw_aq1..4` строятся по `tw_f`;
|
||||
- скелет `sk` — узлы `tw` (`_sort = path`, `line_id = NULL`) ∪ строки `page ⋈ tw` (`_sort = tree_path || lid`, `line_id = lid`), ранг `ROW_NUMBER() OVER (ORDER BY _sort, line_id NULLS FIRST)`. Вывод соединяется со скелетом по ключу узла или строки;
|
||||
- итоговый `ORDER BY` — по `_sort_rank`.
|
||||
|
||||
Конкретная раскладка CTE остаётся за исполнителем (design.md, «Оставлено исполнителю»). Единственное ограничение: никакой ветки `IF p_focus_lines IS NULL THEN <копия запроса>` (D1в).
|
||||
|
||||
- [ ] **Step 3: Применить и прогнать тесты FORM_2 AHR/CAP/OPER**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
$A/reset_db.sh && cd $W/api && DATABASE_URL=$DBURL $PY/pytest tests/integration/test_upd_form_cells_subtree.py tests/integration/test_upd_form_cells_full_rows.py -q -p no:cacheprovider -W ignore -rf
|
||||
```
|
||||
Expected:
|
||||
- PASS: `test_affected_rows_match_reference` для FORM_2 AHR/CAP/OPER в обоих `path` и с `sections=['plan']`; все `path=affected`; весь `test_upd_form_cells_full_rows.py`.
|
||||
- Ещё красные (их закроют Task 4–7), список `-rf` должен состоять только из них: `path=builder` для сателлитов, LIMIT, OTCH9F и FORM_1; `test_empty_focus_returns_nothing`; тест «без полной сборки»; дельта SUB_SUB_ITEM; JIT ещё не написан.
|
||||
|
||||
- [ ] **Step 4: Снимок GET**
|
||||
|
||||
Run: `$A/snap.sh after && diff $A/get-before.tsv $A/get-after.tsv`
|
||||
Expected: пустой diff. Допустимы только перестановки строк с совпадающим ключом (D6). Каждую такую перестановку перечислить в отчёте задачи с обоснованием. Любое другое расхождение — **стоп и эскалация**.
|
||||
|
||||
- [ ] **Step 5: Коммит**
|
||||
|
||||
```bash
|
||||
git add api/alembic/versions/0051_upd_form_cells_subtree.py api/alembic/versions/sql/0051_upd_form_cells_subtree.sql
|
||||
git commit -m "perf(sql): фокус и _sort_rank в построителях FORM_2 AHR/CAP/OPER (AURORA-1850)
|
||||
|
||||
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: FORM_2 — сателлиты, AHR_LIMIT, OTCH9F с фокусом в `v_form2_view`
|
||||
|
||||
**Files:**
|
||||
- Modify: `api/alembic/versions/sql/0051_upd_form_cells_subtree.sql` (тело `v_form2_view`)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `v_form2_view(..., p_focus_lines)` из Task 3.
|
||||
- Produces: та же сигнатура. Для листов AHR_RENT/AHR_UTILITY/AHR_SECURITY `p_focus_lines` — id деталей, для AHR_LIMIT — id `limit_template`, для OTCH9F — id `fixed_asset_report`.
|
||||
|
||||
- [ ] **Step 1: Сателлиты (D4)**
|
||||
|
||||
Ранг — `ROW_NUMBER() OVER (ORDER BY <detail>.id)` по скелету без jsonb. **В скелете тот же набор строк, что в GET**: те же `JOIN v3.budget_line` и `JOIN v3.vsp` и тот же `WHERE` (Review Focus 4). Соединение с `vsp` по первичному ключу дешёвое. Убирать его можно только если доказано, что оно не фильтрует строки: `vsp_id NOT NULL` и есть FK. Затем фильтр `p_focus_lines IS NULL OR <detail>.id = ANY(p_focus_lines)`, и jsonb строится только по отобранным строкам.
|
||||
|
||||
- [ ] **Step 2: AHR_LIMIT**
|
||||
|
||||
Вывод фильтруется по `p_focus_lines IS NULL OR lt.id = ANY(v3._limit_template_path_set(p_focus_lines))`; `'{}'` даёт NULL и, значит, 0 строк. `sort_order = lt.sort_order`. Агрегат `tree`/`leaf_amt`/`agg` можно оставить полным. Вызовы `v3._form2_ahr_limit_amount(...)` из дампа сохраняются буквально.
|
||||
|
||||
- [ ] **Step 3: OTCH9F**
|
||||
|
||||
`base` ограничивается разделами фокуса:
|
||||
```sql
|
||||
AND (p_focus_lines IS NULL OR EXISTS (
|
||||
SELECT 1 FROM v3.fixed_asset_report ff
|
||||
WHERE ff.id = ANY(p_focus_lines)
|
||||
AND ff.expense_item_id IS NOT DISTINCT FROM far.expense_item_id
|
||||
AND ff.equipment_name IS NOT DISTINCT FROM far.equipment_name))
|
||||
```
|
||||
Ранг — отдельный скелет по всем строкам формы с тем же `JOIN v3.expense_item`: `ROW_NUMBER() OVER (ORDER BY expense_item_id, month, id)` (D6). `sort_order` берётся из скелета соединением по `id`.
|
||||
|
||||
- [ ] **Step 4: Тесты**
|
||||
|
||||
Run: `$A/reset_db.sh && cd $W/api && DATABASE_URL=$DBURL $PY/pytest tests/integration/test_upd_form_cells_subtree.py tests/integration/test_upd_form_cells_full_rows.py -q -p no:cacheprovider -W ignore`
|
||||
Expected: PASS — `test_affected_rows_match_reference` для AHR_RENT, AHR_UTILITY, AHR_SECURITY, AHR_LIMIT и OTCH9F в обоих `path`, а также `test_patch_otch9f_returns_all_months_of_equipment`. Красными остаются только случаи, которые закрывают Task 5–7.
|
||||
|
||||
- [ ] **Step 5: Снимок GET**
|
||||
|
||||
Run: `$A/snap.sh after && diff $A/get-before.tsv $A/get-after.tsv`
|
||||
Expected: diff пуст. Для OTCH9F допустимы только перестановки при равных `(expense_item_id, month)`, если они есть (D6) — перечислить. Иначе стоп.
|
||||
|
||||
- [ ] **Step 6: Коммит**
|
||||
|
||||
```bash
|
||||
git add api/alembic/versions/sql/0051_upd_form_cells_subtree.sql
|
||||
git commit -m "perf(sql): фокус сателлитов, AHR_LIMIT и OTCH9F в v_form2_view (AURORA-1850)
|
||||
|
||||
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: FORM_1 — фокус, `_sort_rank`, новая `v_form1_view`, `v_form_view`
|
||||
|
||||
**Files:**
|
||||
- Modify: `api/alembic/versions/sql/0051_upd_form_cells_subtree.sql`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: тела `v_form1_sheet_sections`, `v_form1_sheet_jsonb` и `v_form_view` из `before-0051.sql`.
|
||||
- Produces:
|
||||
- `v3.v_form1_sheet_sections(p_form_id integer, p_sheet character varying, p_direction character varying DEFAULT NULL, p_sections text[] DEFAULT NULL, p_focus_lines integer[] DEFAULT NULL)` — `RETURNS TABLE(<прежние>, _sort_rank bigint)`;
|
||||
- `v3.v_form1_sheet_jsonb(<те же параметры>)` — `RETURNS TABLE(<прежние>, _sort_rank bigint)`, LANGUAGE sql;
|
||||
- `v3.v_form1_view(p_form_id integer, p_sheet character varying, p_sections text[] DEFAULT NULL, p_direction character varying DEFAULT NULL, p_focus_lines integer[] DEFAULT NULL)` — `RETURNS TABLE(row_type varchar, depth integer, sort_order bigint, data jsonb)`, plpgsql, STABLE. Принимает только AHR/CAP/OPER, на остальных листах — `RAISE EXCEPTION 'v_form1_view: unsupported sheet %'`. Тело — `jsonb_build_object` из 16 ключей ветки FORM_1 `v_form_view` в прежнем порядке; `sort_order = j._sort_rank`;
|
||||
- `v3.v_form_view` — `CREATE OR REPLACE`, сигнатура прежняя. Ветка FORM_1 AHR/CAP/OPER — `RETURN QUERY SELECT * FROM v3.v_form1_view(p_form_id, p_sheet, p_sections, p_direction, NULL); RETURN;`. Остальное без изменений.
|
||||
|
||||
- [ ] **Step 1: DROP FORM_1**
|
||||
|
||||
Сразу после трёх DROP FORM_2:
|
||||
```sql
|
||||
DROP FUNCTION v3.v_form1_sheet_jsonb(integer, character varying, character varying, text[]);
|
||||
DROP FUNCTION v3.v_form1_sheet_sections(integer, character varying, character varying, text[]);
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Построители FORM_1**
|
||||
|
||||
Правки те же, что в Task 3, с отличием (контекст design.md): `page` и все агрегатные CTE (`agg`, `aq1..aq4`, `atot`) фильтруются по `direction` так же, как в дампе; `tw` — нет. Узлы фокуса берутся из `budget_line` **без** фильтра `direction`, INPUT фокуса — `page_f` (с фильтром). Скелет — `tw` ∪ `page ⋈ tw` с тем же фильтром направления.
|
||||
|
||||
- [ ] **Step 3: `v_form1_view` и `v_form_view`**
|
||||
|
||||
Порядок `CREATE` по D9: `v_form1_sheet_sections` и `v_form1_sheet_jsonb` вместе с остальными sections и jsonb, затем `v_form2_view`, затем `v_form1_view`, затем `v_form_view`.
|
||||
|
||||
- [ ] **Step 4: Тесты**
|
||||
|
||||
Run: `$A/reset_db.sh && cd $W/api && DATABASE_URL=$DBURL $PY/pytest tests/integration/test_upd_form_cells_subtree.py tests/integration/test_upd_form_cells_full_rows.py -q -p no:cacheprovider -W ignore`
|
||||
Expected: PASS — все случаи FORM_1 в обоих `path`, включая строку Development в пакете Support; `test_patch_form1_development_matches_get`; `test_patch_after_add_and_delete_matches_get[FORM_1…]`; `test_empty_focus_returns_nothing` (теперь есть обе сигнатуры). Красными остаются только тест «без полной сборки» и тест дельты с SUB_SUB_ITEM.
|
||||
|
||||
- [ ] **Step 5: Снимок GET**
|
||||
|
||||
Run: `$A/snap.sh after && diff $A/get-before.tsv $A/get-after.tsv` → пусто, включая FORM_1 SMETA (`v_form1_smeta` зовёт `v_form1_sheet_sections` с 4 аргументами). Иначе стоп.
|
||||
|
||||
- [ ] **Step 6: Коммит**
|
||||
|
||||
```bash
|
||||
git add api/alembic/versions/sql/0051_upd_form_cells_subtree.sql
|
||||
git commit -m "perf(sql): фокус и _sort_rank в построителях FORM_1, новая v_form1_view (AURORA-1850)
|
||||
|
||||
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 6: `_form_affected_rows` без полной сборки
|
||||
|
||||
**Files:**
|
||||
- Modify: `api/alembic/versions/sql/0051_upd_form_cells_subtree.sql` (добавить `CREATE OR REPLACE FUNCTION v3._form_affected_rows` после `v_form_view`)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `v_form2_view(..., p_focus_lines)`, `v_form1_view(..., p_focus_lines)`; тело 0050 из `before-0051.sql`.
|
||||
- Produces: `v3._form_affected_rows(p_form_id integer, p_sheet character varying, p_ids integer[], p_direction character varying DEFAULT NULL, p_sections text[] DEFAULT NULL)`, `RETURNS TABLE(row_type varchar, depth integer, sort_order bigint, data jsonb)`, plpgsql, **STABLE**.
|
||||
|
||||
- [ ] **Step 1: Переписать ветки (D5)**
|
||||
|
||||
- `v_strip` — как в 0050.
|
||||
- FORM_1, листы AHR/CAP/OPER → `SELECT v.row_type, v.depth, v.sort_order, v.data - v_strip FROM v3.v_form1_view(p_form_id, p_sheet, p_sections, p_direction, COALESCE(p_ids, '{}')) v ORDER BY v.sort_order`.
|
||||
- FORM_2, листы AHR/CAP/OPER/AHR_RENT/AHR_UTILITY/AHR_SECURITY/AHR_LIMIT/OTCH9F → то же через `v3.v_form2_view(p_form_id, p_sheet, p_sections, COALESCE(p_ids, '{}'))`.
|
||||
- Ветка FORM_4 — буквально как в 0050.
|
||||
- Остальные сочетания (SMETA и прочие) — прежний путь 0050 через `v_form_view`.
|
||||
|
||||
Фильтров по `_expense_item_path_set` и `_limit_template_path_set` в FORM_1 и FORM_2 больше нет: состав ответа задаёт построитель.
|
||||
|
||||
- [ ] **Step 2: Тесты**
|
||||
|
||||
Run: `$A/reset_db.sh && cd $W/api && DATABASE_URL=$DBURL $PY/pytest tests/integration/test_upd_form_cells_subtree.py tests/integration/test_upd_form_cells_full_rows.py -q -p no:cacheprovider -W ignore`
|
||||
Expected:
|
||||
- PASS: все `path=affected`; `test_edit_path_does_not_call_v_form_view` для всех случаев, кроме `delta` (или skip — тогда наблюдение `O-n` в ledger и вопрос контроллеру); все HTTP-тесты; `test_form4_returns_only_own_program_and_project`; `test_bad_column_in_batch_fails_and_applies_nothing`.
|
||||
- Красными остаются только тесты дельты (п. 2/delta и п. 10).
|
||||
|
||||
- [ ] **Step 3: Коммит**
|
||||
|
||||
```bash
|
||||
git add api/alembic/versions/sql/0051_upd_form_cells_subtree.sql
|
||||
git commit -m "perf(sql): _form_affected_rows собирает только затронутое поддерево (AURORA-1850)
|
||||
|
||||
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 7: Дельта АХР через `_form_affected_rows`
|
||||
|
||||
**Files:**
|
||||
- Modify: `api/alembic/versions/sql/0051_upd_form_cells_subtree.sql` (последним оператором файла)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `_form_affected_rows` из Task 6; тело `get_form2_ahr_satellite_delta` из `before-0051.sql`.
|
||||
- Produces: `v3.get_form2_ahr_satellite_delta(p_form_id integer, p_source_sheet character varying, p_source_row_id integer)` — сигнатура, `RETURNS TABLE`, `LANGUAGE sql` и `STABLE` прежние.
|
||||
|
||||
- [ ] **Step 1: Тело**
|
||||
|
||||
`CREATE OR REPLACE` с телом из дампа, в котором:
|
||||
- CTE `target_lines` скопирован **символ в символ**, включая сопоставление `(ei.depth = 3 AND ei.item_id = lt.expense_item_code) OR ei.num_group_id = lt.expense_item_code`;
|
||||
- CTE `target_path` удалён;
|
||||
- финальный SELECT заменён на
|
||||
```sql
|
||||
SELECT r.row_type, r.depth, r.sort_order, r.data
|
||||
FROM v3._form_affected_rows(
|
||||
p_form_id, 'AHR',
|
||||
COALESCE((SELECT array_agg(tl.line_id) FROM target_lines tl), '{}'),
|
||||
'Support', NULL) r
|
||||
ORDER BY r.sort_order;
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Тесты**
|
||||
|
||||
Run: `$A/reset_db.sh && cd $W/api && DATABASE_URL=$DBURL $PY/pytest tests/integration/test_upd_form_cells_subtree.py -q -p no:cacheprovider -W ignore`
|
||||
Expected: PASS (или skip с указанной причиной) для всего файла, включая `delta`, `test_ahr_delta_matches_get_after_rent_edit`, `test_ahr_delta_includes_sub_sub_item_for_depth4_limit` и пустую дельту в `test_empty_focus_returns_nothing`.
|
||||
|
||||
- [ ] **Step 3: Коммит**
|
||||
|
||||
```bash
|
||||
git add api/alembic/versions/sql/0051_upd_form_cells_subtree.sql
|
||||
git commit -m "perf(sql): дельта АХР через _form_affected_rows, с предками SUB_SUB_ITEM (AURORA-1850)
|
||||
|
||||
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 8: `SET jit = off` на построителях и пути правки (D7)
|
||||
|
||||
**Files:**
|
||||
- Modify: `api/tests/integration/test_upd_form_cells_subtree.py`
|
||||
- Modify: `api/alembic/versions/sql/0051_upd_form_cells_subtree.sql`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: функции из Task 3–7.
|
||||
- Produces: `proconfig` содержит `jit=off` у `v3.v_form2_sheet_sections`, `v3.v_form1_sheet_sections`, `v3._form_affected_rows` и `v3.get_form2_ahr_satellite_delta`.
|
||||
|
||||
- [ ] **Step 1: Падающий тест**
|
||||
|
||||
```python
|
||||
JIT_OFF_FUNCS = ("v_form2_sheet_sections", "v_form1_sheet_sections",
|
||||
"_form_affected_rows", "get_form2_ahr_satellite_delta")
|
||||
|
||||
|
||||
def test_jit_off_on_builders_and_edit_path(db_exec):
|
||||
rows = db_exec(
|
||||
"""SELECT p.proname, COALESCE(p.proconfig, '{}') FROM pg_proc p
|
||||
JOIN pg_namespace n ON n.oid = p.pronamespace
|
||||
WHERE n.nspname = 'v3' AND p.proname = ANY(CAST(:names AS text[]))""",
|
||||
names=list(JIT_OFF_FUNCS))
|
||||
assert {r[0] for r in rows} == set(JIT_OFF_FUNCS)
|
||||
missing = [r[0] for r in rows if "jit=off" not in r[1]]
|
||||
assert not missing, f"нет SET jit = off: {missing}"
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Убедиться, что тест падает**
|
||||
|
||||
Run: `$A/wait_lock.sh && cd $W/api && DATABASE_URL=$DBURL $PY/pytest tests/integration/test_upd_form_cells_subtree.py::test_jit_off_on_builders_and_edit_path -q -p no:cacheprovider -W ignore`
|
||||
Expected: FAIL, `нет SET jit = off: [...4 имени]`.
|
||||
|
||||
- [ ] **Step 3: Атрибуты**
|
||||
|
||||
В заголовок каждой из 4 функций в 0051 добавить строку ` SET jit TO 'off'` (после `STABLE`, перед `AS $function$`). На другие функции атрибут не ставить.
|
||||
|
||||
- [ ] **Step 4: Тесты**
|
||||
|
||||
Run: `$A/reset_db.sh && cd $W/api && DATABASE_URL=$DBURL $PY/pytest tests/integration/test_upd_form_cells_subtree.py tests/integration/test_upd_form_cells_full_rows.py -q -p no:cacheprovider -W ignore`
|
||||
Expected: всё PASS (или skip с причиной).
|
||||
|
||||
- [ ] **Step 5: Коммит**
|
||||
|
||||
```bash
|
||||
git add api/tests/integration/test_upd_form_cells_subtree.py api/alembic/versions/sql/0051_upd_form_cells_subtree.sql
|
||||
git commit -m "perf(sql): jit = off на построителях и пути правки (AURORA-1850)
|
||||
|
||||
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 9: Защита правки 0037 (design.md, «Тестирование», п. 7)
|
||||
|
||||
**Files:**
|
||||
- Create: `api/tests/integration/test_upd_form_cells_subtree_0037_guard.py`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: фикстуры `headers` и `db_exec`, хелперы `_get_sheet` и `_patch_cell` (импорт из `test_upd_form_cells_full_rows.py`).
|
||||
|
||||
- [ ] **Step 1: Тесты**
|
||||
|
||||
```python
|
||||
from decimal import Decimal
|
||||
|
||||
|
||||
def test_v_form2_view_keeps_0037_limit_formula(client, headers, db_exec):
|
||||
# LEAF «- принтеры», limit_template.id = 13, код 204620030 (0013_form2_limit.sql:1707)
|
||||
_patch_cell(client, headers, 4, "AHR_LIMIT", 13, "qty_q1", 4)
|
||||
row = next(r for r in _get_sheet(client, headers, 4, "AHR_LIMIT") if r["data"]["id"] == 13)
|
||||
assert Decimal(str(row["data"]["amount_q1"])) == Decimal(4) * Decimal("0.57") * 10900 / 4
|
||||
src = db_exec("SELECT pg_get_functiondef(CAST(:s AS regprocedure))",
|
||||
s="v3.v_form2_view(integer,character varying,text[],integer[])")[0][0]
|
||||
assert "_form2_ahr_limit_amount" in src
|
||||
|
||||
|
||||
OLD_DEPTH3_MATCHER = re.compile(r"ei\.depth\s*=\s*3\s+AND\s*\(") # вид до 0037
|
||||
|
||||
|
||||
def test_ahr_delta_keeps_0037_num_group_matching(client, headers, db_exec):
|
||||
found = db_exec(
|
||||
"""SELECT lt.id FROM v3.limit_template lt
|
||||
JOIN v3.expense_item ei ON ei.num_group_id = lt.expense_item_code
|
||||
AND ei.sheet = 'AHR' AND ei.direction = 'Support' AND ei.depth = 4
|
||||
WHERE lt.row_type = 'LEAF' ORDER BY lt.id LIMIT 1""")
|
||||
if not found:
|
||||
pytest.skip("нет LEAF шаблона с кодом = num_group_id статьи AHR глубины 4")
|
||||
lt_id = found[0][0]
|
||||
_patch_cell(client, headers, 4, "AHR_LIMIT", lt_id, "qty_q1", 4)
|
||||
delta = db_exec("SELECT row_type FROM v3.get_form2_ahr_satellite_delta(4, 'AHR_LIMIT', :i)", i=lt_id)
|
||||
assert any(r[0] == "INPUT" for r in delta), "дельта АХР пуста — откат сопоставления 0037"
|
||||
src = db_exec("SELECT pg_get_functiondef(CAST(:s AS regprocedure))",
|
||||
s="v3.get_form2_ahr_satellite_delta(integer,character varying,integer)")[0][0]
|
||||
assert "ei.num_group_id = lt.expense_item_code" in src
|
||||
assert not OLD_DEPTH3_MATCHER.search(src)
|
||||
```
|
||||
Импорты: `re`, `pytest`, `Decimal`. Сравнение `amount_q1` идёт через `Decimal`, чтобы масштаб numeric не мешал. Если значение в `data` — строка или число, привести через `str`.
|
||||
|
||||
- [ ] **Step 2: Прогон**
|
||||
|
||||
Run: `$A/wait_lock.sh && cd $W/api && DATABASE_URL=$DBURL $PY/pytest tests/integration/test_upd_form_cells_subtree_0037_guard.py -q -p no:cacheprovider -W ignore`
|
||||
Expected: PASS. Это защитный тест: на 0051 он должен проходить сразу. Если FAIL — откат правки 0037, **стоп и эскалация** (правка тела вручную по дампу, затем снова тест).
|
||||
|
||||
- [ ] **Step 3: Проверка, что тест кусается (без БД)**
|
||||
|
||||
Строковые проверки обоих тестов применить к текстам функций **до** 0037: `get_form2_ahr_satellite_delta` — `api/alembic/versions/sql/0031_form2_ahr_satellite_sync.sql:784-855`, `v_form2_view` — `0033_form2_old_sort.sql:1-373`. Ожидается, что `OLD_DEPTH3_MATCHER.search` находит совпадение в тексте 0031, а `"_form2_ahr_limit_amount"` отсутствует в тексте 0033. Проверить однострочником `$PY/python -c "…"` и записать вывод в ledger.
|
||||
|
||||
- [ ] **Step 4: Коммит**
|
||||
|
||||
```bash
|
||||
git add api/tests/integration/test_upd_form_cells_subtree_0037_guard.py
|
||||
git commit -m "test(sheet): защита правки 0037 после 0051 (AURORA-1850)
|
||||
|
||||
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 10: ARCHITECTURE.md, прогон с нуля, итоговый снимок GET
|
||||
|
||||
**Files:**
|
||||
- Modify: `ARCHITECTURE.md` (абзац «Поток редактирования ячейки», строка 5)
|
||||
- Modify: `openspec/changes/aurora-1850-upd-form-cells-subtree/design.md` (решения без обсуждения, если появились по ходу)
|
||||
|
||||
- [ ] **Step 1: ARCHITECTURE.md**
|
||||
|
||||
Последнее предложение абзаца заменить на:
|
||||
> Выборку делает `v3._form_affected_rows` (миграции 0050, 0051). Для FORM_1 и FORM_2 она не собирает лист целиком: построители (`v_form1_view`/`v_form2_view` → `*_sheet_jsonb` → `*_sheet_sections`) вызываются с фокусом `p_focus_lines` (NULL — весь лист, `'{}'` — ничего) и считают только изменённые строки и узлы их путей. `sort_order` в обоих режимах берётся из скелета листа (`_sort_rank`), поэтому совпадает с GET. FORM_4 до AURORA-1846 собирается полностью. Фоновая дельта АХР после правки сателлита (`get_form2_ahr_satellite_delta`) идёт тем же путём.
|
||||
|
||||
- [ ] **Step 2: Полный прогон на чистой БД (вся цепочка миграций с нуля)**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
$A/wait_lock.sh && docker exec perf-pg dropdb -U perf --if-exists dfip_1850 && docker exec perf-pg createdb -U perf dfip_1850 \
|
||||
&& cd $W/api && DATABASE_URL=$DBURL $PY/pytest --init_db -q -p no:cacheprovider -W ignore
|
||||
```
|
||||
Expected: падают те же 3 исходных теста, новых падений нет. Число passed = базовое (Task 1) плюс новые тесты. Число skipped и причины skip выписать в ledger.
|
||||
|
||||
- [ ] **Step 3: Итоговый снимок GET**
|
||||
|
||||
Run: `$A/snap.sh after && diff $A/get-before.tsv $A/get-after.tsv` → пусто или только перечисленные перестановки D6.
|
||||
|
||||
- [ ] **Step 4: Ничего лишнего в диффе ветки**
|
||||
|
||||
Run: `git diff --stat origin/feat/215-aurora-1844-upd-form-cells-full-rows...HEAD -- api/src web perf`
|
||||
Expected: пусто.
|
||||
|
||||
- [ ] **Step 5: Коммит и push**
|
||||
|
||||
```bash
|
||||
git add ARCHITECTURE.md openspec/changes/aurora-1850-upd-form-cells-subtree/design.md
|
||||
git commit -m "docs: ARCHITECTURE — фокусная сборка ответа на правку (AURORA-1850)
|
||||
|
||||
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||||
git push origin feat/235-aurora-1850-upd-form-cells-subtree
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Завершение (SDD-контроллер)
|
||||
|
||||
1. **Передать главному контроллеру проверку PG15.** Сообщить ветку и коммит и попросить прогнать `pytest --init_db` на `dfip-pg15:55434` (своя БД `dfip_1850`) — миграции с нуля до 0051 и весь набор. Исполнитель на PG15 не ходит. **Ждать ответа.** Падение миграции или новых тестов на PG15 — это нарушение корректности: стоп, правка, повторная передача.
|
||||
2. **Финальное ревью всей ветки — модель opus** (superpowers:requesting-code-review). Бриф ревьюера:
|
||||
- дифф `origin/feat/215-aurora-1844-upd-form-cells-full-rows...HEAD`;
|
||||
- `before-0051.sql` как база для сравнения тел: diff каждой функции «до» и в 0051;
|
||||
- Review Focus (5 пунктов выше), D1–D9, правило замка.
|
||||
|
||||
Находки `C-n`/`I-n` исправляются в ветке, `M-n` — по решению контроллера. После правок — повтор Task 10, Step 2.
|
||||
3. **PR без цифр** (superpowers:finishing-a-development-branch):
|
||||
```bash
|
||||
tea pulls create --base feat/215-aurora-1844-upd-form-cells-full-rows \
|
||||
--head feat/235-aurora-1850-upd-form-cells-subtree \
|
||||
--title "AURORA-1850: правка ячейки пересчитывает только затронутое поддерево" \
|
||||
--description "$(cat <файл описания>)"
|
||||
```
|
||||
Описание PR, по-русски:
|
||||
- `Closes #235`, Jira AURORA-1850;
|
||||
- «Что сделано»;
|
||||
- «Тесты» — итог полного прогона на PG16 с оговоркой про 3 исходных падения и результат PG15 от контроллера;
|
||||
- «Проверить руками» — правка на FORM_2/AHR и FORM_1/AHR у двух соавторов: сумма предка обновилась у второго, соседние строки не затронуты; правка аренды обновляет АХР;
|
||||
- «Замер до/после — добавит контроллер после прогона на стенде» и сценарий из design.md, «Замер до/после», шаги 1–7;
|
||||
- «Rulings I made» — список `R-n` из финального отчёта контроллера;
|
||||
- таблица находок `| Маркер | Суть | Статус |`, где `deferred` выделены отдельно;
|
||||
- последняя строка `🤖 Generated with [Claude Code](https://claude.com/claude-code)`.
|
||||
|
||||
Не мержить.
|
||||
4. **Точка ожидания: замеры главного контроллера** (design.md, «Замер до/после»; после основных ночных замеров). Исполнитель ничего не запускает на стенде и не трогает БД `perf`. Контроллер записывает цифры в PR.
|
||||
- Шаги 3–4 замера (снимок GET, эквивалентность на данных стенда) разошлись — **стоп и эскалация** (корректность).
|
||||
- Шаги 5–7 вышли за пороги — итерации только на пути правки, по порядку из Global Constraints («Стоп и итерации»). Каждая итерация — отдельный коммит в этой ветке, полный pytest и повторный замер контроллером. Недобор после шага 3 правила записывается в PR и уходит в follow-up.
|
||||
@ -269,3 +269,12 @@
|
||||
| 2026-10-28 | Контроллер: полный pytest на PG16 в контейнере `perf-pg` (порт 55432), отдельная БД `dfip_1850`; БД `perf` стенда не трогается никогда; тела функций для D8 снимаются через `pg_get_functiondef` с `dfip_1850` на ревизии 0050; совместимость с PG 15.12 проверяет контроллер (`dfip-pg15`, порт 55434) отдельным шагом плана | Изоляция от стенда замеров: 0051 в БД `perf` до замера «до» испортит базовую точку | При ошибке в БД — пересоздать `dfip_1850`; если 0051 попадёт в `perf`, придётся заново делать reset и gen стенда (около 15 мин) |
|
||||
| 2026-10-28 | Контроллер: замеры на стенде выполняет контроллер после основных ночных замеров; в плане — точка ожидания, PR открывается без цифр | Стенд занят ночным прогоном; пороги не условие мержа | Цифры в PR появляются позже мержа, если ревью пройдёт раньше |
|
||||
| 2026-10-28 | Контроллер: тест защиты 0037 разнесён по функциям — формула лимитов через `v_form2_view`, сопоставление `num_group_id` через дельту | Каждая из двух правок 0037 живёт в своей функции; падение теста сразу указывает, где откат | Нет: только раскладка тестов |
|
||||
| 2026-10-28 | План: шаблон `dfip_1850_base` (ревизия 0050 + фикстура) для быстрого сброса `dfip_1850` и вспомогательная `dfip_1850_snap` для снимка GET; обе в `perf-pg`, БД `perf` не затрагивается | Миграция с DROP без `IF EXISTS` не перезапускается на той же БД; сброс из шаблона быстрее `--init_db` | Две лишние БД в контейнере; при ошибке — пересоздать из `dfip_1850` |
|
||||
| 2026-10-28 | План: тела 8 функций с ревизии 0050 коммитятся в `openspec/changes/aurora-1850-upd-form-cells-subtree/before-0051.sql` (не миграция) | D8 требует буквальных тел; файл — база для ревью (diff «до» и 0051) и для будущей миграции отката; архивируется вместе с change | Лишний файл ~5 тыс. строк в change |
|
||||
| 2026-10-28 | План: локальный снимок GET всех листов FORM_1/FORM_2 до и после 0051 на детерминированно засеянной `dfip_1850_snap` после каждой задачи с построителями | Требование «GET листа не меняется» проверяется на стенде только после PR; локальная проверка ловит регресс GET раньше | Данные посева меньше стенда; снимок стенда (шаг 3 замера) остаётся обязательным |
|
||||
| 2026-10-28 | План: «0 вложенных полных сборок» в тестах проверяется через `SET LOCAL track_functions = 'all'` и `pg_stat_get_xact_function_calls(v_form_view)`; без прав суперпользователя тест делает skip | Единственный способ увидеть вложенный вызов plpgsql-функции из теста | На роли без суперпользователя критерий проверяется только замером стенда (шаг 6) |
|
||||
| 2026-10-28 | План: тест эквивалентности дополнительно сравнивает с эталоном вывод построителя в режиме фокуса (`v_form2_view`/`v_form1_view` с `p_focus_lines`), а не только `_form_affected_rows` | Даёт красный тест до реализации (путь `_form_affected_rows` на 0050 эквивалентен эталону по построению) и локализует ошибку в построителе | Тесты дольше на несколько секунд |
|
||||
| 2026-10-28 | План: скелет `sort_order` сателлитов сохраняет те же `JOIN v3.budget_line` и `JOIN v3.vsp`, что и GET; соединение с `vsp` убирается только при доказанных `NOT NULL` и FK | D4 говорит «без соединения с vsp», но в GET соединение внутреннее и может отбрасывать строки; тогда ранг сдвинется | Соединение по первичному ключу почти бесплатно; если убрать его без доказательства — тихо неверный `sort_order` |
|
||||
| 2026-10-28 | План: `v_form1_view` принимает только AHR/CAP/OPER, на других листах — `RAISE EXCEPTION` | SMETA FORM_1 собирается `v_form_view` до вызова; явная ошибка лучше пустого результата | Нет: функция внутренняя |
|
||||
| 2026-10-28 | План: раскладка тестов — `test_upd_form_cells_subtree.py` (эквивалентность, `sort_order`, дельта, JIT) и `test_upd_form_cells_subtree_0037_guard.py`; SQL-хелперы в `sheet_contract.py`; HTTP-хелперы и фикстуры импортируются из `test_upd_form_cells_full_rows.py` | Оставлено исполнителю (design.md); решено в плане, чтобы задачи 2, 8, 9 ссылались на одни имена | Нет: только раскладка |
|
||||
| 2026-10-28 | План: защита 0037 для дельты проверяет `pg_get_functiondef` регуляркой `ei\.depth\s*=\s*3\s+AND\s*\(` (вид до 0037), а не литералом | В теле до 0037 между `3` и `AND (` стоят переводы строки и отступы; литерал не поймал бы откат | Нет |
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user