openspec: архив aurora-1839-cell-memo и aurora-1984-pd-standalone-projects

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Andrey Epifancev 2026-11-11 19:33:14 +04:00
parent 4dc111a0ce
commit f0470313c1
10 changed files with 294 additions and 0 deletions

View File

@ -0,0 +1,91 @@
# form4-programs-projects Specification
## Purpose
Программы и проекты на листах AHR/CAP/OPER формы 4 (FORM_4, «Проектная деятельность»): как они создаются, переименовываются и удаляются, где стоят в листе, как участвуют в итогах и выгрузке, как подписаны и как к проекту добавляется расход.
## Requirements
### Requirement: Проект создаётся в программе или прямо в разделе
Пользователь с правом добавлять строки в раздел листа SHALL иметь возможность создать проект как внутри программы этого раздела, так и прямо в разделе, без программы. Название SHALL быть непустым после обрезки пробелов. Права SHALL проверяться так же, как при создании программы в этом разделе.
#### Scenario: Проект в разделе
- **WHEN** на листе CAP выделена строка раздела (или ничего не выделено) и пользователь создаёт проект «П1»
- **THEN** «П1» появляется в разделе CAP без программы у всех подключённых к листу пользователей без перезагрузки
- **AND** после перезагрузки страницы «П1» стоит на том же месте
#### Scenario: Проект в программе — как раньше
- **WHEN** выделена программа «Пр» и пользователь создаёт проект
- **THEN** проект создаётся внутри «Пр»
#### Scenario: Выделен проект или расход
- **WHEN** выделен проект или строка расхода и пользователь создаёт проект
- **THEN** новый проект создаётся там же, где выделенный проект: в той же программе или прямо в разделе
#### Scenario: Пустое название
- **WHEN** пользователь вводит название из одних пробелов
- **THEN** проект не создаётся
### Requirement: Место самостоятельного проекта в листе
Самостоятельный проект SHALL выводиться строкой ITEM непосредственно под ROOT своего раздела, без строки GROUP над ним; его расходы — строками INPUT под ним. На уровне под ROOT программы и самостоятельные проекты SHALL идти в порядке создания (по id записи `form4_project`). Проекты внутри программы и их расходы SHALL выводиться как раньше.
#### Scenario: Вперемешку в порядке создания
- **WHEN** в разделе последовательно созданы программа «А», самостоятельный проект «Б», программа «В»
- **THEN** под ROOT раздела строки идут в порядке «А» (GROUP), «Б» (ITEM), «В» (GROUP)
#### Scenario: Пустой раздел
- **WHEN** в разделе нет ни одного расхода, но есть самостоятельный проект
- **THEN** проект виден в листе так же, как видна пустая программа
### Requirement: Суммы самостоятельного проекта
Суммы расходов самостоятельного проекта SHALL входить в строку ITEM проекта и в ROOT раздела ровно один раз. Лист SHALL NOT содержать строку GROUP без программы (`program_id` пустой). Ответ на правку ячейки расхода самостоятельного проекта SHALL содержать строку расхода, ITEM проекта и ROOT раздела.
#### Scenario: Итог раздела
- **WHEN** в разделе есть программа с расходом 100 и самостоятельный проект с расходом 50 в `plan.q1`
- **THEN** ROOT раздела показывает 150 в `plan.q1`, ITEM самостоятельного проекта — 50
#### Scenario: Правка расхода
- **WHEN** пользователь меняет `plan.q1` у расхода самостоятельного проекта
- **THEN** ответ содержит этот расход, ITEM проекта и ROOT раздела с пересчитанными суммами и не содержит GROUP
### Requirement: Расходы самостоятельного проекта
К самостоятельному проекту SHALL можно добавлять и удалять расходы так же, как к проекту в программе.
#### Scenario: Добавление расхода
- **WHEN** выделен самостоятельный проект и пользователь добавляет расход по статье
- **THEN** расход появляется под этим проектом, суммы проекта и раздела пересчитываются
### Requirement: Переименование программы и проекта
Пользователь с правом изменения SHALL иметь возможность переименовать выделенную программу или проект (в программе и самостоятельный). Окно переименования SHALL открываться с текущим названием. Пустое название и название из одних пробелов SHALL NOT сохраняться. При ошибке сервера окно SHALL оставаться открытым с введённым текстом и показывать понятное сообщение. Отмена SHALL оставлять прежнее название. Переименование SHALL NOT менять состав проектов, расходов и суммы.
#### Scenario: Успешное переименование
- **WHEN** пользователь переименовывает программу «А» в «А2»
- **THEN** у всех подключённых пользователей строка показывает «А2», после перезагрузки тоже
#### Scenario: Ошибка
- **WHEN** сервер отказывает в переименовании (например, нет прав)
- **THEN** окно остаётся открытым с введённым названием и показывает причину
### Requirement: Удаление самостоятельного проекта
Пустой самостоятельный проект SHALL удаляться с подтверждением, как проект в программе (AURORA-1978). Проект с расходами SHALL NOT удаляться, пользователь видит причину.
#### Scenario: Удаление пустого
- **WHEN** пользователь удаляет пустой самостоятельный проект и подтверждает
- **THEN** проект исчезает из листа у всех подключённых, ROOT раздела пересчитывается
### Requirement: Подпись типа в третьем столбце
В третьем столбце листов AHR/CAP/OPER формы 4 строка программы SHALL быть подписана «программа», строка проекта (в программе и самостоятельного) — «проект». Строки ROOT и расходов SHALL показывать код, как раньше. Подпись SHALL совпадать на экране и в выгрузке Excel.
#### Scenario: Подписи
- **WHEN** открыт лист CAP с программой, проектом в ней и самостоятельным проектом
- **THEN** напротив программы «программа», напротив обоих проектов «проект», у расходов — код статьи
### Requirement: Выбор расхода для проекта
Окно выбора расхода SHALL показывать варианты в виде «<номер статьи> — <название>», упорядоченные по номеру статьи с учётом числовых частей (например, 2 раньше 10). Поиск SHALL находить вариант по номеру и по названию. Выбор SHALL добавлять расход именно по выбранной статье.
#### Scenario: Сортировка
- **WHEN** среди статей есть номера «3.2.» и «3.10.»
- **THEN** «3.2.» стоит выше «3.10.»
#### Scenario: Поиск по номеру
- **WHEN** пользователь вводит «3.01»
- **THEN** в списке остаются статьи, номер которых содержит «3.01»

View File

@ -0,0 +1,203 @@
# sheet-table Specification
## Purpose
Таблица листа формы (`web/src/components/RealtimeTable`). Здесь описано, как быстро она прокручивается, как остаются корректными закреплённые колонки, подсветки и состояние ячеек при мемоизации, и какой контракт обязана соблюдать любая правка таблицы.
## Requirements
### Requirement: Шаг прокрутки листа укладывается в 25 мс главного потока
Прокрутка таблицы листа SHALL стоить не больше 25 мс главного потока на один шаг колеса 200 px на листе FORM_2/AHR.
Условия замера:
- стенд профилирования `perf/` на данных масштаба 1 (`results/manifest-s1.json`, лист до `grow`, 1154 строки);
- роль EXECUTOR_RF, prod-сборка, CPU 1×, без троттлинга;
- метрика — медиана `main_thread_ms_per_step` по трём повторам сценария `perf/e2e/scenarios/scroll.spec.ts` по оси `y`.
Горизонтальная прокрутка (ось `x`) того же листа MUST быть не медленнее базы. База — ветка AURORA-1837, на которой стоит эта задача.
Замер — на той же машине Apple M4, на которой снят отчёт профилирования, пороги абсолютные.
Пороги этого требования и остальных требований с замерами — цель, а не условие слияния. Цифры до и после SHALL быть приведены в PR. Если цель не достигнута, SHALL быть заведена follow-up issue с разбором того, что съедает остаток. Слияние блокируют только требования корректности.
#### Scenario: Недобор до цели оформляется follow-up
- **WHEN** замер после изменения показывает медиану шага больше 25 мс или регрессию относительно базы
- **THEN** PR вливается по зелёным тестам и ручному регрессу, цифры приведены в PR
- **AND** заведена follow-up issue с разбором, какие работы главного потока занимают остаток
#### Scenario: Вертикальная прокрутка FORM_2/AHR после изменения
- **WHEN** на стенде прогоняется `PERF_TARGETS='FORM_2/AHR/*' PERF_ROLES=rf PERF_AXES=y PERF_TRACE=0 PERF_REPEATS=3 npx playwright test scenarios/scroll.spec.ts` на сборке этой ветки
- **THEN** медиана `main_thread_ms_per_step` по трём повторам не больше 25 мс
#### Scenario: Горизонтальная прокрутка не хуже базы
- **WHEN** тот же сценарий прогоняется с `PERF_AXES=x` на сборке ветки AURORA-1837 и на сборке этой ветки на тех же данных
- **THEN** медиана `main_thread_ms_per_step` на этой ветке не больше, чем на ветке AURORA-1837
- **AND** обе цифры приведены в описании PR
### Requirement: Прокрутка не перерисовывает ячейки, оставшиеся на экране
Ячейка таблицы листа, которая при прокрутке осталась в видимой области и у которой не изменились данные строки и определение колонки, SHALL NOT перерисовываться. Перерисовываются только ячейки, которые появились на экране. Строки таблицы и служебные колонки (раскрытие строк) перерисовываться могут.
Числовая цель по счётчику рендеров не блокирует слияние (см. требование «Шаг прокрутки…»). Неизменность вида ячеек при прокрутке — требование корректности.
#### Scenario: Счётчик рендеров ячеек на шаг падает
- **WHEN** на профилирующей сборке (`:8081`) прогоняется `PERF_HOT_MODE=react` `perf/e2e/scenarios/hotspots.spec.ts` для FORM_2/AHR по оси `y`
- **THEN** число рендеров `ForwardRef(Styled(td))` на шаг кратно меньше 1 696, порядка числа ячеек, появившихся на экране за шаг
- **AND** цифры до и после приведены в описании PR
#### Scenario: Вид ячеек при прокрутке не меняется
- **WHEN** пользователь прокручивает лист вверх, вниз и вбок и возвращается к исходной позиции
- **THEN** каждая видимая ячейка показывает то же значение, цвет, подсветку и признаки, что и до прокрутки
### Requirement: Закреплённые колонки остаются на своих местах при изменении раскладки
После любого изменения раскладки закреплённых колонок таблица листа SHALL показывать закреплённые колонки примыкающими друг к другу, без наложения и без зазоров. Видимые ячейки закреплённых колонок SHALL оставаться на месте при горизонтальной прокрутке.
Изменения раскладки: закрепление колонки, открепление, скрытие закреплённой колонки, показ скрытой закреплённой колонки, смена ширины закреплённой колонки.
Пока пользователь тянет ширину закреплённой колонки, соседние закреплённые колонки MAY отставать от неё. После отпускания кнопки мыши раскладка MUST стать корректной без дополнительных действий пользователя.
#### Scenario: Закрепление колонки
- **WHEN** пользователь выделяет колонку в строке букв и нажимает «Закрепить область»
- **THEN** колонка встаёт сразу после ранее закреплённых и остаётся на месте при горизонтальной прокрутке
- **AND** её ячейки во всех видимых строках, включая уже отрисованные до закрепления, стоят на месте закреплённой колонки
#### Scenario: Открепление колонки
- **WHEN** пользователь открепляет закреплённую колонку
- **THEN** колонка возвращается в прокручиваемую область, а оставшиеся закреплённые колонки сдвигаются и примыкают друг к другу
#### Scenario: Скрытие и показ закреплённой колонки
- **WHEN** пользователь скрывает закреплённую колонку настройками видимости, а потом снова показывает её
- **THEN** в обоих состояниях закреплённые колонки примыкают друг к другу, без пустого места на месте скрытой и без наложения после показа
#### Scenario: Смена ширины закреплённой колонки
- **WHEN** пользователь тянет правую границу заголовка закреплённой колонки и отпускает кнопку мыши
- **THEN** закреплённые колонки правее неё сдвигаются на новую ширину и примыкают к ней без наложения
- **AND** до отпускания кнопки мыши допускается, что соседние закреплённые колонки ещё стоят по старой ширине
#### Scenario: Смена ширины и видимости незакреплённой колонки
- **WHEN** пользователь тянет ширину незакреплённой колонки или скрывает и показывает незакреплённую колонку
- **THEN** ширина и набор колонок меняются сразу, заголовок и тело таблицы остаются выровнены по колонкам
- **AND** закреплённые колонки не сдвигаются
### Requirement: Подсветка поиска и фильтров соответствует текущему запросу
Подсветка совпадений в ячейках таблицы листа SHALL соответствовать текущему значению глобального поиска и фильтров по колонкам. Подсветка — это выделение текста `<mark>` и обводка текущего совпадения. При изменении или сбросе запроса подсветка MUST обновляться во всех видимых ячейках, включая уже отрисованные.
#### Scenario: Ввод и изменение поискового запроса
- **WHEN** пользователь вводит запрос в поле поиска, а затем меняет его
- **THEN** после применения запроса во всех видимых ячейках подсвечено совпадение именно с текущим запросом, подсветки прежнего запроса нет
#### Scenario: Сброс поиска
- **WHEN** пользователь очищает поле поиска
- **THEN** ни в одной видимой ячейке нет подсветки `<mark>` и обводки текущего совпадения
#### Scenario: Переход к совпадению
- **WHEN** пользователь нажимает Enter или кнопки «следующий/предыдущий результат»
- **THEN** таблица прокручивается к совпадению, текущая ячейка обведена, обводка предыдущей снята
#### Scenario: Фильтр по колонке и его сброс
- **WHEN** пользователь задаёт значение фильтра по колонке, меняет его, а затем очищает
- **THEN** строки отфильтрованы по текущему значению, подсветка в ячейках этой колонки соответствует текущему значению, после очистки подсветки нет
### Requirement: Состояние вне данных строки отображается в ячейках как раньше
Таблица листа SHALL отображать в ячейках состояние, которого нет в данных строки, так же, как до введения мемоизации:
- блокировки соавторов;
- выделение строки;
- ошибки валидации;
- разрядность (рубли, тысячи, миллионы);
- редактируемость по этапу и роли;
- открытый редактор ячейки.
Изменение этого состояния MUST отражаться в видимых ячейках без прокрутки и без перезагрузки страницы.
#### Scenario: Блокировка соавтора
- **WHEN** второй пользователь в другом браузере открывает ячейку на редактирование, а затем закрывает её
- **THEN** у первого пользователя ячейка показывает признак блокировки, пока её редактирует второй, и снимает его после закрытия, в том числе если ячейка была на экране всё это время
#### Scenario: Выделение строки
- **WHEN** пользователь кликает по строке или по номеру строки, а потом кликает ещё раз
- **THEN** строка затемняется при выделении и возвращается к обычному виду при снятии выделения
#### Scenario: Ошибка валидации
- **WHEN** пользователь вводит значение, нарушающее правило валидации, а затем исправляет его
- **THEN** ячейка подсвечивается как ошибочная и появляется плашка «Контроль ошибок», после исправления подсветка снимается
#### Scenario: Разрядность
- **WHEN** пользователь меняет разрядность в панели настроек
- **THEN** все видимые числовые ячейки сразу показывают значения в новой разрядности
#### Scenario: Редактирование ячейки
- **WHEN** пользователь открывает редактор двойным кликом (в колонке ВСП — выпадающий список) и сохраняет значение Enter или кликом вне ячейки
- **THEN** редактор открывается в этой ячейке, после сохранения ячейка показывает новое значение, соавтор видит его
#### Scenario: Добавление и удаление строки
- **WHEN** пользователь добавляет строку и удаляет строку
- **THEN** таблица показывает актуальный набор строк, значения и признаки ячеек соседних строк не перепутаны
### Requirement: Контракт для правок таблицы листа: рендер ячейки зависит только от ячейки
Любая правка таблицы листа SHALL соблюдать контракт мемоизации ячеек. Контракт распространяется на AURORA-1889, 1890, 1891, 1893, 1910 и последующие задачи.
1. Содержимое и оформление ячейки MUST вычисляться только из объекта ячейки, то есть из данных строки и определения колонки.
2. Состояние вне данных строки MUST доходить до ячейки одним из трёх способов:
- CSS по атрибутам контейнера, строки или ячейки;
- React-контекст;
- внешнее хранилище с подпиской (`useSyncExternalStore`).
Его MUST NOT замыкать в свойствах ячейки из конфигурации таблицы (`muiTableBodyCellProps`, `col.Cell`, `col.Edit`).
3. Обработчики событий ячейки, например будущие `onClick`/`onKeyDown` из AURORA-1891, MUST читать изменяемое состояние в момент события через `ref` или `table.getState()`, а не из замыкания.
4. Состояние, которое меняет определения колонок, MUST явно пересоздавать набор колонок таблицы и тем сбрасывать мемоизацию. Это редактируемость по этапу и роли, разрядность, правила валидации, раскладка закреплённых колонок, поиск и фильтры.
5. Прокрутка MUST NOT вызывать рендер компонента, который владеет экземпляром таблицы (`RealtimeTable`).
#### Scenario: Новое оформление ячейки по состоянию вне данных
- **WHEN** правка добавляет оформление ячейки, зависящее от состояния вне данных строки (например, текущая ячейка навигации или ячейка, к которой перешли из плашки ошибок)
- **THEN** оформление задаётся CSS-правилом по атрибутам (`data-row-id`, `data-col-id` и т.п.), контекстом или хранилищем с подпиской
- **AND** уже отрисованные ячейки показывают его без прокрутки
#### Scenario: Обработчик видит актуальное состояние
- **WHEN** состояние таблицы (например, редактируемая ячейка) изменилось после последнего рендера ячейки, и пользователь кликает или нажимает клавишу в этой ячейке
- **THEN** обработчик принимает решение по текущему состоянию, а не по состоянию на момент рендера ячейки
#### Scenario: Прокрутка не рендерит владельца таблицы
- **WHEN** пользователь прокручивает таблицу листа
- **THEN** в профиле React за время прокрутки нет рендеров `RealtimeTable`, есть только рендеры тела таблицы и появившихся ячеек
### Requirement: Явный сброс мемоизации проверяется автотестом
Правила явного сброса мемоизации ячеек SHALL быть заданы чистой функцией (ключ сброса) и покрыты node-тестом в стандартной команде `cd web && node --test src/components/RealtimeTable/utils/*.test.js`.
Ключ MUST меняться:
- при закреплении и откреплении колонки;
- при скрытии и показе закреплённой колонки;
- при смене ширины закреплённой колонки (по отпусканию мыши);
- при изменении глобального поиска;
- при изменении значения фильтра по колонке.
Ключ MUST NOT меняться:
- при смене ширины незакреплённой колонки;
- при скрытии и показе незакреплённой колонки;
- при изменении данных;
- пока ширину колонки тянут.
Тест MUST также фиксировать поведение `@tanstack/table-core` 8.20.5: новый массив колонок даёт новые объекты ячеек; смена `columnSizing` при прежнем массиве колонок ячейки не пересоздаёт (поэтому ключ обязателен для ширины закреплённой колонки); смена `columnPinning` пересоздаёт ячейки закреплённых колонок (сторон), центральные ячейки остаются теми же.
#### Scenario: Ключ реагирует на раскладку закреплённых колонок и фильтры
- **WHEN** запускается `node --test` для модуля ключа сброса
- **THEN** тесты подтверждают каждое условие «меняется» и «не меняется» из этого требования
#### Scenario: Допущение о пересоздании ячеек
- **WHEN** запускается `node --test` для теста допущения на `@tanstack/table-core`
- **THEN** тест подтверждает: при новом массиве `columns` (даже с теми же определениями) объекты `row.getVisibleCells()[i]` новые; при смене `columnSizing` с прежним массивом `columns` — те же; при смене `columnPinning` с прежним массивом `columns` — новые у ячеек закреплённых колонок, у центральных — те же
### Requirement: Перетаскивание ширины закреплённой колонки не медленнее базы
Перетаскивание ширины закреплённой колонки на листе FORM_2/AHR SHALL стоить не больше главного потока на шаг движения мыши, чем на ветке AURORA-1837. Медиана `main_thread_ms_per_step` по трём повторам сценария `perf/e2e/scenarios/pinned-resize.spec.ts` на тех же данных и сборке того же типа. Цифры до и после, а также p95 интервала кадра MUST быть приведены в описании PR.
Порог — цель, а не условие слияния; недобор оформляется follow-up issue (см. требование «Шаг прокрутки…»). Корректная раскладка закреплённых колонок после отпускания мыши — требование корректности.
#### Scenario: Замер перетаскивания ширины закреплённой колонки
- **WHEN** сценарий закрепляет на FORM_2/AHR (роль rf, CPU 1×) две первые колонки данных и тянет правую границу первой из них шагами по 4 px вправо и обратно, с ожиданием кадра после каждого шага
- **THEN** медиана `main_thread_ms_per_step` на этой ветке не больше, чем на ветке AURORA-1837
- **AND** после отпускания мыши не менее четырёх `td[data-pinned="true"]` строки (`mrt-row-expand`, `sort_order`, две колонки данных) примыкают друг к другу (проверка геометрии в сценарии проходит)