AURORA-1839: мемоизация ячеек таблицы листа #240

Open
aep wants to merge 15 commits from feat/234-aurora-1839-cell-memo into feat/226-aurora-1837-cell-styles-css
Owner

Closes #234. Jira: AURORA-1839.

Стек. База — feat/226-aurora-1837-cell-styles-css (PR #230, стоит на PR #222 / AURORA-1836). После слияния #230 в test PR перенацеливается на test с rebase.

Что сделано

  1. Задача 1: utils/cellMemoReset.js — чистый ключ сброса мемоизации (раскладка закреплённых, фильтры) и pickSettledSizing; 17 тестов, включая тесты-характеристики поведения @tanstack/table-core 8.20.5.
  2. Задача 2: utils/columnResizeInfo.js — апдейтеры columnSizingInfo (идемпотентный endColumnResize).
  3. Задача 3: ColumnResizer сообщает MRT начало и конец перетаскивания (mouseup, blur, размонтирование, страховка e.buttons); атрибут data-col-resize-id.
  4. Задача 4: columnFilters и columnSizingInfo — управляемое состояние RealtimeTable.
  5. Задача 5: memoMode: 'cells', tableColumns = [...columns] по ключу сброса, ширины закреплённых фиксируются на время перетаскивания.
  6. Задача 6: сценарий perf/e2e/scenarios/pinned-resize.spec.ts (две закреплённые data-колонки, тянется первая, проверка примыкания и эффекта), lib/pinnedGeometry.ts, README. Сценарий не запускался.
  7. Финальное ревью (opus) и одна fix-волна; документы openspec приведены к фактическому поведению table-core.

Тесты

  • cd web && node --test src/components/RealtimeTable/utils/*.test.js: 41 pass, 0 fail.
  • node --test perf/e2e/tests/pinned-geometry.test.ts: 5 pass.
  • npx biome check src/components/RealtimeTable: новых ошибок нет (RealtimeTable.jsx 2 errors / 5 warnings, TableHead.jsx 8 errors — как в базе). npm run build: успешно.
  • Backend не менялся (git diff --name-only origin/feat/226-aurora-1837-cell-styles-css...HEAD -- api/ пуст), pytest и PG15 не нужны.

Ручной регресс (до слияния)

FORM_2/AHR под исполнителем РФ и под админом; соавтор во втором браузере:

  • поиск: подсветка <mark>, Enter и Shift+Enter, кнопки «‹ ›», обводка текущей, очистка поиска;
  • фильтры колонок: текст, диапазон, очистка;
  • ошибки валидации: ввести нарушающее значение, исправить, плашка;
  • блокировка соавтора: появление и снятие 🔒 на ячейке, которая всё время на экране;
  • выделение строки кликом и по номеру строки;
  • редактирование: двойной клик, ВСП-дропдаун, Enter, клик вне ячейки;
  • закрепление, открепление, скрытие и показ закреплённой, ширина закреплённой — по отпусканию соседи не наложены; ширина незакреплённой — сразу;
  • сужение широкой колонки на 400 px — пустая полоса, если есть, исчезает по отпусканию;
  • Alt+Tab посреди перетаскивания — после возврата таблица реагирует на поиск;
  • разрядность, масштаб, раскрытие и свёртка групп;
  • добавление и удаление строки;
  • то же на одном листе FORM_4 (STRUCTURE) и отчёте проекта;
  • Alt+Tab посреди перетаскивания ширины — после возврата таблица реагирует на поиск и закрепление (Review Focus 1);
  • закрепление и смена поиска при открытом редакторе — редактор на месте, Enter сохраняет (Review Focus 4);
  • сужение широкой колонки на 400 px — полоса исчезает по отпусканию (Review Focus 5);
  • уже отрисованные ячейки обновляются без прокрутки при поиске, фильтре, закреплении, скрытии закреплённой, ширине закреплённой.

Замер до/после — добавит контроллер после прогона на стенде

Пороги — цель, а не условие слияния. pinned_overlap, pinned_cells_missing, resize_no_effect в skip_reason — нарушение корректности, блокирует слияние.

Что Метрика Порог До После
Скролл по y (scroll.spec.ts) медиана main_thread_ms_per_step ≤ 25 мс
Скролл по x медиана main_thread_ms_per_step не больше базы
Рендеры на шаг (hotspots, :8081) Styled(td)/шаг, рендеры RealtimeTable Styled(td) кратно меньше 1 696; RealtimeTable — 0
Соавтор (scroll-coauthor) main_thread_ms_per_step не хуже базы ±5 %
Перетаскивание ширины закреплённой (pinned-resize) main_thread_ms_per_step, frame_p95_ms, heavy_frame_n main_thread_ms_per_step не больше базы

Rulings I made

  • R-1: трейлер Co-Authored-By — Claude Sonnet 5.5 вместо Opus 5.5 из плана (системная атрибуция). Цена ошибки: косметика в истории (в двух коммитах трейлер Opus/Haiku, историю не переписывали).
  • R-2 (design #18): тест-допущение table-core заменено на фиксацию фактического поведения 8.20.5 (O-1); ключ сброса по закреплению остаётся. Рулинг главного контроллера. Цена: лишняя перерисовка только на редких действиях.
  • R-3 (design #19): формулировка «ключ избыточен по закреплению» из R-2 не принята: закрепление пересоздаёт ячейки закреплённой стороны, центр нет, ключ нужен. Цена: формулировка.
  • R-4 (design #20): допущения исполнителя сценария pinned-resize (column_not_found, нет rf — тесты не регистрируются, waitForFunction) приняты. Цена: запись incomplete вместо падения.
  • R-5 (design #21): сценарий pinned-resize изменён по финальному ревью — две закреплённые data-колонки, тянется первая, не менее 4 закреплённых td, проверка эффекта; прежняя проверка не могла упасть (I-2). Цена: правка сценария до снятия базы; поведение кнопки «Закрепить область» для второй колонки в браузере не проверено — покажет первый прогон (pinned_cells_missing).
  • R-6 (design #22): диапазон ['', ''] не снимает фильтр колонки в TanStack, лишний безвредный сброс, код не менялся (M-3).

Находки

Маркер Суть Статус
O-1 table-core 8.20.5: смена columnPinning (тот же columns) пересоздаёт cell 0_c, допущение design.md "Контекст" п.3/D1 не выполняется fixed (тесты 3bd4b53, design.md docs-коммит см. git log)
T1.1 pickSettledSizing корректна, только если ref пишет результат функции, isResizing булев, ref инициализирован columnSizing перенесено в бриф Task 5 (проверить на ревью)
T1.2 дубль теста columnSizing и устаревший комментарий над фикстурой в cellMemoReset.test.js fixed e0e460b
T1.3 нет защиты от нестандартных типов в ключе (битый localStorage) rejected: типы гарантирует useColumnSettings/MRT, Review Focus п.2 покрыт
T1.4 косметика теста "скрытие и показ закреплённой" rejected: эквивалентность закрыта отдельным тестом
O-1b трейлеры коммитов неоднородны: 3bd4b53 Opus 5.5 (из плана), 61cafb4 Haiku 5.5 rejected: переписывать запушенную историю не стоит; дальше в брифах трейлер Sonnet 5.5 (O-2)
T2.1 трейлер Haiku 5.5 в 61cafb4 rejected (см. O-1b)
T3.1 mouseup вне окна без blur не ловится; страховка e.buttons === 0 в handleMouseMove (ColumnResizer.jsx:34-43) fixed e0e460b
T3.2 один лишний mousemove между mouseup и снятием слушателей rejected: было и раньше, на корректность не влияет
O-2 biome-база TableHead.jsx+ColumnResizer.jsx: 8 errors до и после (organizeImports и др., не наши) info
T4.1 проп columnFilters в TableHead (RealtimeTable.jsx:825) по имени не читается, служит лишь триггером React.memo(TableHead) fixed e0e460b
T4.2 tableConfig пересоздаётся на каждый mousemove при перетаскивании (columnSizingInfo в зависимостях) rejected: задумано D3/D4, MRT замораживает тело
T5.1 заморозка ширин проверена только чистыми функциями и чтением кода, живой интеграции нет deferred: закрыть замером pinned-resize (главный контроллер) + ручной регресс
T5.2 параметр createTableConfig называется columns, а получает tableColumns rejected: как в плане
T6.1 pinnedGeometry: при <2 закреплённых td rects=[] и findPinnedGap=-1 -> запись валидна без проверки корректности (pinned-resize.spec.ts ~166) fixed 7d7a98f
T6.2 тип листа через ReturnType вместо Sheet, повторный loadManifest() fixed 7d7a98f (тип Sheet; повторный loadManifest остался)
T6.3 PERF_TRACE=0 в README-команде, сценарий не читает fixed 7d7a98f
I-1 spec.md/design.md «Тестирование» противоречат тесту и коду (pinning пересоздаёт) fixed 472f2ea
I-2 проверка геометрии pinned-resize не способна упасть (тянется последняя закреплённая) fixed 7d7a98f
M-1 формулировка факта table-core: пересоздаются ячейки закреплённой стороны, не только закрепляемой fixed e0e460b, 472f2ea
M-2 сценарий не проверяет, что перетаскивание произошло fixed 7d7a98f
M-3 ['',''] не снимает фильтр колонки, ключ отличен от "без фильтров" rejected: безвредно, зафиксировано в design #22
M-4 ссылки на openspec/changes/... в комментариях устареют после archive fixed e0e460b
O-3 Cell.jsx ~112 createHighlightedContent: q.replace на массиве range-фильтра, поведение базы deferred (follow-up, вне скоупа, Cell/** запрещён)
I-3 design.md:120 (D1 «Уточнение») противоречит исправленному факту (re-review) fixed (docs-коммит после 472f2ea)

deferred: T5.1 (интеграция заморозки ширин — закрыть замером pinned-resize и ручным регрессом), O-3 (Cell.jsx ~112 createHighlightedContent вызывает q.replace на массиве значения range-фильтра — поведение базы, Cell/** вне скоупа).

🤖 Generated with Claude Code

Closes #234. Jira: AURORA-1839. **Стек.** База — `feat/226-aurora-1837-cell-styles-css` (PR #230, стоит на PR #222 / AURORA-1836). После слияния #230 в `test` PR перенацеливается на `test` с rebase. ## Что сделано 1. Задача 1: `utils/cellMemoReset.js` — чистый ключ сброса мемоизации (раскладка закреплённых, фильтры) и `pickSettledSizing`; 17 тестов, включая тесты-характеристики поведения `@tanstack/table-core` 8.20.5. 2. Задача 2: `utils/columnResizeInfo.js` — апдейтеры `columnSizingInfo` (идемпотентный `endColumnResize`). 3. Задача 3: `ColumnResizer` сообщает MRT начало и конец перетаскивания (`mouseup`, `blur`, размонтирование, страховка `e.buttons`); атрибут `data-col-resize-id`. 4. Задача 4: `columnFilters` и `columnSizingInfo` — управляемое состояние `RealtimeTable`. 5. Задача 5: `memoMode: 'cells'`, `tableColumns = [...columns]` по ключу сброса, ширины закреплённых фиксируются на время перетаскивания. 6. Задача 6: сценарий `perf/e2e/scenarios/pinned-resize.spec.ts` (две закреплённые data-колонки, тянется первая, проверка примыкания и эффекта), `lib/pinnedGeometry.ts`, README. Сценарий не запускался. 7. Финальное ревью (opus) и одна fix-волна; документы openspec приведены к фактическому поведению table-core. ## Тесты - `cd web && node --test src/components/RealtimeTable/utils/*.test.js`: 41 pass, 0 fail. - `node --test perf/e2e/tests/pinned-geometry.test.ts`: 5 pass. - `npx biome check src/components/RealtimeTable`: новых ошибок нет (RealtimeTable.jsx 2 errors / 5 warnings, TableHead.jsx 8 errors — как в базе). `npm run build`: успешно. - Backend не менялся (`git diff --name-only origin/feat/226-aurora-1837-cell-styles-css...HEAD -- api/` пуст), pytest и PG15 не нужны. ## Ручной регресс (до слияния) FORM_2/AHR под исполнителем РФ и под админом; соавтор во втором браузере: - [ ] поиск: подсветка `<mark>`, Enter и Shift+Enter, кнопки «‹ ›», обводка текущей, очистка поиска; - [ ] фильтры колонок: текст, диапазон, очистка; - [ ] ошибки валидации: ввести нарушающее значение, исправить, плашка; - [ ] блокировка соавтора: появление и снятие 🔒 на ячейке, которая всё время на экране; - [ ] выделение строки кликом и по номеру строки; - [ ] редактирование: двойной клик, ВСП-дропдаун, Enter, клик вне ячейки; - [ ] закрепление, открепление, скрытие и показ закреплённой, ширина закреплённой — по отпусканию соседи не наложены; ширина незакреплённой — сразу; - [ ] сужение широкой колонки на 400 px — пустая полоса, если есть, исчезает по отпусканию; - [ ] Alt+Tab посреди перетаскивания — после возврата таблица реагирует на поиск; - [ ] разрядность, масштаб, раскрытие и свёртка групп; - [ ] добавление и удаление строки; - [ ] то же на одном листе FORM_4 (STRUCTURE) и отчёте проекта; - [ ] Alt+Tab посреди перетаскивания ширины — после возврата таблица реагирует на поиск и закрепление (Review Focus 1); - [ ] закрепление и смена поиска при открытом редакторе — редактор на месте, Enter сохраняет (Review Focus 4); - [ ] сужение широкой колонки на 400 px — полоса исчезает по отпусканию (Review Focus 5); - [ ] уже отрисованные ячейки обновляются без прокрутки при поиске, фильтре, закреплении, скрытии закреплённой, ширине закреплённой. ## Замер до/после — добавит контроллер после прогона на стенде Пороги — цель, а не условие слияния. `pinned_overlap`, `pinned_cells_missing`, `resize_no_effect` в `skip_reason` — нарушение корректности, блокирует слияние. | Что | Метрика | Порог | До | После | |---|---|---|---|---| | Скролл по y (scroll.spec.ts) | медиана main_thread_ms_per_step | ≤ 25 мс | | | | Скролл по x | медиана main_thread_ms_per_step | не больше базы | | | | Рендеры на шаг (hotspots, :8081) | Styled(td)/шаг, рендеры RealtimeTable | Styled(td) кратно меньше 1 696; RealtimeTable — 0 | | | | Соавтор (scroll-coauthor) | main_thread_ms_per_step | не хуже базы ±5 % | | | | Перетаскивание ширины закреплённой (pinned-resize) | main_thread_ms_per_step, frame_p95_ms, heavy_frame_n | main_thread_ms_per_step не больше базы | | | ## Rulings I made - R-1: трейлер Co-Authored-By — Claude Sonnet 5.5 вместо Opus 5.5 из плана (системная атрибуция). Цена ошибки: косметика в истории (в двух коммитах трейлер Opus/Haiku, историю не переписывали). - R-2 (design #18): тест-допущение table-core заменено на фиксацию фактического поведения 8.20.5 (O-1); ключ сброса по закреплению остаётся. Рулинг главного контроллера. Цена: лишняя перерисовка только на редких действиях. - R-3 (design #19): формулировка «ключ избыточен по закреплению» из R-2 не принята: закрепление пересоздаёт ячейки закреплённой стороны, центр нет, ключ нужен. Цена: формулировка. - R-4 (design #20): допущения исполнителя сценария pinned-resize (`column_not_found`, нет rf — тесты не регистрируются, waitForFunction) приняты. Цена: запись incomplete вместо падения. - R-5 (design #21): сценарий pinned-resize изменён по финальному ревью — две закреплённые data-колонки, тянется первая, не менее 4 закреплённых td, проверка эффекта; прежняя проверка не могла упасть (I-2). Цена: правка сценария до снятия базы; поведение кнопки «Закрепить область» для второй колонки в браузере не проверено — покажет первый прогон (`pinned_cells_missing`). - R-6 (design #22): диапазон `['', '']` не снимает фильтр колонки в TanStack, лишний безвредный сброс, код не менялся (M-3). ## Находки | Маркер | Суть | Статус | |---|---|---| | O-1 | table-core 8.20.5: смена columnPinning (тот же columns) пересоздаёт cell 0_c, допущение design.md "Контекст" п.3/D1 не выполняется | fixed (тесты 3bd4b53, design.md docs-коммит см. git log) | | T1.1 | pickSettledSizing корректна, только если ref пишет результат функции, isResizing булев, ref инициализирован columnSizing | перенесено в бриф Task 5 (проверить на ревью) | | T1.2 | дубль теста columnSizing и устаревший комментарий над фикстурой в cellMemoReset.test.js | fixed e0e460b | | T1.3 | нет защиты от нестандартных типов в ключе (битый localStorage) | rejected: типы гарантирует useColumnSettings/MRT, Review Focus п.2 покрыт | | T1.4 | косметика теста "скрытие и показ закреплённой" | rejected: эквивалентность закрыта отдельным тестом | | O-1b | трейлеры коммитов неоднородны: 3bd4b53 Opus 5.5 (из плана), 61cafb4 Haiku 5.5 | rejected: переписывать запушенную историю не стоит; дальше в брифах трейлер Sonnet 5.5 (O-2) | | T2.1 | трейлер Haiku 5.5 в 61cafb4 | rejected (см. O-1b) | | T3.1 | mouseup вне окна без blur не ловится; страховка `e.buttons === 0` в handleMouseMove (ColumnResizer.jsx:34-43) | fixed e0e460b | | T3.2 | один лишний mousemove между mouseup и снятием слушателей | rejected: было и раньше, на корректность не влияет | | O-2 | biome-база TableHead.jsx+ColumnResizer.jsx: 8 errors до и после (organizeImports и др., не наши) | info | | T4.1 | проп columnFilters в TableHead (RealtimeTable.jsx:825) по имени не читается, служит лишь триггером React.memo(TableHead) | fixed e0e460b | | T4.2 | tableConfig пересоздаётся на каждый mousemove при перетаскивании (columnSizingInfo в зависимостях) | rejected: задумано D3/D4, MRT замораживает тело | | T5.1 | заморозка ширин проверена только чистыми функциями и чтением кода, живой интеграции нет | deferred: закрыть замером pinned-resize (главный контроллер) + ручной регресс | | T5.2 | параметр createTableConfig называется columns, а получает tableColumns | rejected: как в плане | | T6.1 | pinnedGeometry: при <2 закреплённых td rects=[] и findPinnedGap=-1 -> запись валидна без проверки корректности (pinned-resize.spec.ts ~166) | fixed 7d7a98f | | T6.2 | тип листа через ReturnType вместо Sheet, повторный loadManifest() | fixed 7d7a98f (тип Sheet; повторный loadManifest остался) | | T6.3 | PERF_TRACE=0 в README-команде, сценарий не читает | fixed 7d7a98f | | I-1 | spec.md/design.md «Тестирование» противоречат тесту и коду (pinning пересоздаёт) | fixed 472f2ea | | | I-2 | проверка геометрии pinned-resize не способна упасть (тянется последняя закреплённая) | fixed 7d7a98f | | | M-1 | формулировка факта table-core: пересоздаются ячейки закреплённой стороны, не только закрепляемой | fixed e0e460b, 472f2ea | | | M-2 | сценарий не проверяет, что перетаскивание произошло | fixed 7d7a98f | | | M-3 | ['',''] не снимает фильтр колонки, ключ отличен от "без фильтров" | rejected: безвредно, зафиксировано в design #22 | | M-4 | ссылки на openspec/changes/... в комментариях устареют после archive | fixed e0e460b | | | O-3 | Cell.jsx ~112 createHighlightedContent: q.replace на массиве range-фильтра, поведение базы | deferred (follow-up, вне скоупа, Cell/** запрещён) | | I-3 | design.md:120 (D1 «Уточнение») противоречит исправленному факту (re-review) | fixed (docs-коммит после 472f2ea) | **deferred:** T5.1 (интеграция заморозки ширин — закрыть замером pinned-resize и ручным регрессом), O-3 (`Cell.jsx` ~112 `createHighlightedContent` вызывает `q.replace` на массиве значения range-фильтра — поведение базы, `Cell/**` вне скоупа). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
aep added 15 commits 2026-10-28 21:01:16 +03:00
Мемоизация ячеек таблицы листа (memoMode 'cells') с явным сбросом
по раскладке закреплённых колонок и фильтрам, контракт для правок
таблицы, критерии и сценарии замера.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Решения 28.10: пороги производительности — цель, недобор в follow-up;
машина замеров Apple M4, пороги абсолютные; замеры выполняет контроллер
после ночного прогона; пустая полоса при перетаскивании по умолчанию
принимается как штатное поведение MRT.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
План: 6 задач исполнителей (haiku: 1–2, sonnet: 3–6), финальное ревью opus,
PR в feat/226-aurora-1837-cell-styles-css и точка ожидания замеров.
Решения стадии плана дописаны в design.md (13–17).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ключ из раскладки закреплённых колонок и фильтров (AURORA-1839, design.md D2),
pickSettledSizing замораживает ширины, пока колонку тянут.
Тесты table-core фиксируют фактическое поведение 8.20.5 (R-2): закрепление
пересоздаёт только ячейку закрепляемой колонки, columnSizing ячейки не трогает.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
isResizingColumn выставляется на mousedown и снимается по mouseup, blur окна
и размонтированию: пока тянут, MRT замораживает тело таблицы (design.md D3).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Значения нужны до создания таблицы, чтобы войти в ключ сброса мемоизации (design.md D3, D4).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
tableColumns = [...columns] пересоздаётся по ключу сброса (utils/cellMemoReset.js);
ширины закреплённых колонок входят в ключ по отпусканию мыши (design.md D1, D2).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Закрепляет первую колонку данных FORM_2/AHR (rf), тянет её границу 50×(+4 px) и 50×(−4 px)
с ожиданием кадра, после отпускания проверяет примыкание td[data-pinned].
Для сборки базы (без data-col-resize-id) — запасной поиск ресайзера. Замеры не запускались.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- ColumnResizer: handleMouseMove завершает перетаскивание, если левая кнопка не зажата
- cellMemoReset.test: убран дубль теста columnSizing, добавлены характеристики закрепления второй колонки и скрытия закреплённой
- комментарии к columnFilters (триггер React.memo(TableHead)) и ссылки на архивную спеку

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This pull request can be merged automatically.
You are not authorized to merge this pull request.

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin feat/234-aurora-1839-cell-memo:feat/234-aurora-1839-cell-memo
git checkout feat/234-aurora-1839-cell-memo
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: RSHB/DFiP_Budget_planing#240
No description provided.