diff --git a/openspec/changes/aurora-1839-cell-memo/.openspec.yaml b/openspec/changes/archive/2026-11-11-aurora-1839-cell-memo/.openspec.yaml similarity index 100% rename from openspec/changes/aurora-1839-cell-memo/.openspec.yaml rename to openspec/changes/archive/2026-11-11-aurora-1839-cell-memo/.openspec.yaml diff --git a/openspec/changes/aurora-1839-cell-memo/design.md b/openspec/changes/archive/2026-11-11-aurora-1839-cell-memo/design.md similarity index 100% rename from openspec/changes/aurora-1839-cell-memo/design.md rename to openspec/changes/archive/2026-11-11-aurora-1839-cell-memo/design.md diff --git a/openspec/changes/aurora-1839-cell-memo/proposal.md b/openspec/changes/archive/2026-11-11-aurora-1839-cell-memo/proposal.md similarity index 100% rename from openspec/changes/aurora-1839-cell-memo/proposal.md rename to openspec/changes/archive/2026-11-11-aurora-1839-cell-memo/proposal.md diff --git a/openspec/changes/aurora-1839-cell-memo/specs/sheet-table/spec.md b/openspec/changes/archive/2026-11-11-aurora-1839-cell-memo/specs/sheet-table/spec.md similarity index 100% rename from openspec/changes/aurora-1839-cell-memo/specs/sheet-table/spec.md rename to openspec/changes/archive/2026-11-11-aurora-1839-cell-memo/specs/sheet-table/spec.md diff --git a/openspec/changes/aurora-1984-pd-standalone-projects/.openspec.yaml b/openspec/changes/archive/2026-11-11-aurora-1984-pd-standalone-projects/.openspec.yaml similarity index 100% rename from openspec/changes/aurora-1984-pd-standalone-projects/.openspec.yaml rename to openspec/changes/archive/2026-11-11-aurora-1984-pd-standalone-projects/.openspec.yaml diff --git a/openspec/changes/aurora-1984-pd-standalone-projects/design.md b/openspec/changes/archive/2026-11-11-aurora-1984-pd-standalone-projects/design.md similarity index 100% rename from openspec/changes/aurora-1984-pd-standalone-projects/design.md rename to openspec/changes/archive/2026-11-11-aurora-1984-pd-standalone-projects/design.md diff --git a/openspec/changes/aurora-1984-pd-standalone-projects/proposal.md b/openspec/changes/archive/2026-11-11-aurora-1984-pd-standalone-projects/proposal.md similarity index 100% rename from openspec/changes/aurora-1984-pd-standalone-projects/proposal.md rename to openspec/changes/archive/2026-11-11-aurora-1984-pd-standalone-projects/proposal.md diff --git a/openspec/changes/aurora-1984-pd-standalone-projects/specs/form4-programs-projects/spec.md b/openspec/changes/archive/2026-11-11-aurora-1984-pd-standalone-projects/specs/form4-programs-projects/spec.md similarity index 100% rename from openspec/changes/aurora-1984-pd-standalone-projects/specs/form4-programs-projects/spec.md rename to openspec/changes/archive/2026-11-11-aurora-1984-pd-standalone-projects/specs/form4-programs-projects/spec.md diff --git a/openspec/specs/form4-programs-projects/spec.md b/openspec/specs/form4-programs-projects/spec.md new file mode 100644 index 0000000..39d22f0 --- /dev/null +++ b/openspec/specs/form4-programs-projects/spec.md @@ -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» diff --git a/openspec/specs/sheet-table/spec.md b/openspec/specs/sheet-table/spec.md new file mode 100644 index 0000000..f8d36fd --- /dev/null +++ b/openspec/specs/sheet-table/spec.md @@ -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 соответствовать текущему значению глобального поиска и фильтров по колонкам. Подсветка — это выделение текста `` и обводка текущего совпадения. При изменении или сбросе запроса подсветка MUST обновляться во всех видимых ячейках, включая уже отрисованные. + +#### Scenario: Ввод и изменение поискового запроса +- **WHEN** пользователь вводит запрос в поле поиска, а затем меняет его +- **THEN** после применения запроса во всех видимых ячейках подсвечено совпадение именно с текущим запросом, подсветки прежнего запроса нет + +#### Scenario: Сброс поиска +- **WHEN** пользователь очищает поле поиска +- **THEN** ни в одной видимой ячейке нет подсветки `` и обводки текущего совпадения + +#### 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`, две колонки данных) примыкают друг к другу (проверка геометрии в сценарии проходит)