From 472f2ea22213ddd7d3759159209b55dda874d796 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Wed, 28 Oct 2026 21:49:58 +0400 Subject: [PATCH] =?UTF-8?q?docs(openspec):=20aurora-1839=20=E2=80=94=20?= =?UTF-8?q?=D1=84=D0=B0=D0=BA=D1=82=D1=8B=20table-core=20=D0=BF=D0=BE=20?= =?UTF-8?q?=D0=B7=D0=B0=D0=BA=D1=80=D0=B5=D0=BF=D0=BB=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D1=8E,=20=D0=BD=D0=BE=D0=B2=D0=B0=D1=8F=20=D1=80=D0=B0=D1=81?= =?UTF-8?q?=D0=BA=D0=BB=D0=B0=D0=B4=D0=BA=D0=B0=20pinned-resize,=20=D1=80?= =?UTF-8?q?=D0=B5=D1=88=D0=B5=D0=BD=D0=B8=D1=8F=2021=20=D0=B8=2022?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 --- .../changes/aurora-1839-cell-memo/design.md | 27 +++++++++++-------- .../specs/sheet-table/spec.md | 8 +++--- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/openspec/changes/aurora-1839-cell-memo/design.md b/openspec/changes/aurora-1839-cell-memo/design.md index 9e467bd..6e07514 100644 --- a/openspec/changes/aurora-1839-cell-memo/design.md +++ b/openspec/changes/aurora-1839-cell-memo/design.md @@ -38,7 +38,7 @@ Исключение: пока `columnSizingInfo.isResizingColumn`, `draggingColumn` или `draggingRow` истинны, MRT подставляет прежний массив из `columnDefsRef` и колонки не пересоздаёт (`:1695-1700`). Проверено node-скриптом на `table-core`: - новый массив с теми же определениями даёт новые ячейки; - прежний массив — те же ячейки; - - смена `columnPinning` при прежнем массиве — **новая ячейка только у закрепляемой или открепляемой колонки**, ячейки остальных колонок те же. Причина: `getLeftVisibleCells`/`getRightVisibleCells` (`ColumnPinning.js:86-98`) возвращают копии `{...cell, position}`, а центральные ячейки берутся из тех же объектов `allCells`. Допущение черновика «ячейки пересоздаются только при смене `columns`/`data`» верно для ширины и видимости, но неверно для закрепляемой колонки (O-1, решение 18); + - смена `columnPinning` при прежнем массиве — **новые ячейки у всех колонок закреплённой стороны**, центральные ячейки те же (характеристика: закрепление второй колонки слева пересоздаёт обе закреплённые, скрытие закреплённой — оставшуюся). Причина: `getLeftVisibleCells`/`getRightVisibleCells` (`ColumnPinning.js:86-98`) возвращают копии `{...cell, position}`, а центральные ячейки берутся из тех же объектов `allCells`. Допущение черновика «ячейки пересоздаются только при смене `columns`/`data`» верно для ширины и видимости, но неверно для закреплённых колонок (O-1, решения 18, 19). Ключ сброса нужен из-за ширины закреплённой колонки (смена `columnSizing` ячейки не пересоздаёт) и как страховка на случай смены поведения библиотеки; - смена `columnSizing` при прежнем массиве — те же ячейки (тест-характеристика). 4. **Пока `isResizingColumn` истинно, тело таблицы заморожено.** `MRT_Table` рендерит `Memo_MRT_TableBody` (`:3780`), а тот сравнивает только `table.options.data` (`:2526`). Так MRT ведёт себя при собственном перетаскивании ширины. 5. **Прокрутка перерисовывает только внутренние компоненты MRT.** Виртуализаторы созданы в `MRT_TableBody` (строки) и `MRT_Table` (колонки), `@tanstack/react-virtual` перерисовывает свой компонент через `useReducer`. `RealtimeTable` при прокрутке не рендерится: в нём нет состояния, меняющегося по `scroll`. `ColumnSelectionOverlay` держит своё состояние, `useHeaderPortal` состояния не держит. @@ -292,11 +292,11 @@ const pinnedLayoutKey = useMemo( - после отпускания ключ соответствует новым ширинам. Тест-допущение на `@tanstack/table-core` — фикстура `createTable` и `updateState`, как в `columnLayoutUtils.test.js`: -- при прежних `columns`/`data` `row.getVisibleCells()[i]` те же; -- при новом массиве `columns` с теми же определениями — новые; -- при смене `columnPinning` с прежним массивом — те же. +- при новом массиве `columns` с теми же определениями `row.getVisibleCells()[i]` новые; +- при смене `columnSizing` с прежним массивом — те же (поэтому ключ обязателен для ширины закреплённой колонки); +- при смене `columnPinning` с прежним массивом — новые у ячеек закреплённых колонок, центральные те же (закрепление второй колонки слева пересоздаёт обе закреплённые; скрытие закреплённой — оставшуюся закреплённую). -Третье утверждение — причина, по которой нужен явный сброс. Тест ссылается на этот design.md. +Тест фиксирует характеристику table-core 8.20.5, а не ожидание дизайна, и ссылается на этот design.md. **Линт:** `cd web && npx biome check src`. `useExhaustiveDependencies` настроен на уровень `info`. Зависимость `cellMemoResetKey` в `useMemo` для `tableColumns` намеренная, помечается комментарием. @@ -339,10 +339,10 @@ const pinnedLayoutKey = useMemo( **Сценарий `perf/e2e/scenarios/pinned-resize.spec.ts` (добавить)** — по образцу `scroll.spec.ts`: `loginAs`, `PROBE_SCRIPT`, `startProbe`/`perStep`, `throttle`, `append`, `buildTargets` по манифесту, фильтр `PERF_TARGETS`. 1. Перед навигацией (через `addInitScript` или `page.evaluate` до загрузки листа) очистить в localStorage ключи `${formId}_${formType}_${sheetName}_${direction}_columnPinning`, `_columnSizing`, `_columnVisibility` этого листа, чтобы начальная раскладка была по умолчанию: закреплены `mrt-row-expand` и `sort_order`. 2. Открыть лист FORM_2/AHR под `rf`, дождаться строк (как `scroll.spec.ts`, `MIN_ROWS`). -3. Выбрать первую незакреплённую колонку данных: первый `[data-col-select-id]` в строке букв, чей id не `sort_order`/`mrt-row-expand`, а лучше первый `data.*`. Кликнуть его, затем кнопку «Закрепить область»: `[data-pin-panel] button[aria-label="Закрепить область"]`, MUI `Tooltip` ставит `aria-label` по `title`. Дождаться `data-active="true"` на кнопке. -4. Найти ресайзер `[data-col-resize-id=""]`, взять `boundingBox`, `page.mouse.move` в его центр, `page.mouse.down()`. +3. Выбрать две первые колонки `data.*` в DOM-порядке (нет второй — `skip_reason: 'second_column_not_found'`). Для каждой по очереди: кликнуть `[data-col-select-id]`, затем кнопку «Закрепить область»: `[data-pin-panel] button[aria-label="Закрепить область"]`, MUI `Tooltip` ставит `aria-label` по `title`. Дождаться `data-active="true"` на кнопке. +4. Найти ресайзер ПЕРВОЙ из закреплённых колонок `[data-col-resize-id=""]`, взять `boundingBox`, `page.mouse.move` в его центр, `page.mouse.down()`. 5. `startProbe`. Затем 50 шагов по +4 px и 50 шагов по −4 px: `page.mouse.move(x, y)` и после каждого шага ожидание кадра (`requestAnimationFrame`, как `tick` в `lib/scroll.ts`). Потом `page.mouse.up()`, ещё два кадра, `stop()`. `steps = 100`. -6. Проверка геометрии после отпускания: для первой видимой строки тела взять `td[data-pinned="true"]` по порядку и проверить, что `left[i+1]` равен `left[i] + width[i]` с допуском 1 px. Нарушение — `incomplete: true, skip_reason: 'pinned_overlap'`. +6. Проверка геометрии после отпускания: для первой видимой строки тела взять `td[data-pinned="true"]` по порядку и проверить, что `left[i+1]` равен `left[i] + width[i]` с допуском 1 px. Нарушение — `incomplete: true, skip_reason: 'pinned_overlap'`. Меньше 4 `td[data-pinned="true"]` в строке — `skip_reason: 'pinned_cells_missing'` (`pinned_cells_n`). Рост ширины на пике хода (шаг 50) меньше 100 px — `skip_reason: 'resize_no_effect'`. 7. `append({ scenario: 'pinned_resize', target: 'FORM_2/AHR/null@pinned', role: 'rf', cpu_rate, repeat, metrics: { ...probe, ...perStep(probe, steps), steps } })`. 8. Повторы — `PERF_REPEATS` (по умолчанию 3). Режим `PERF_HOT_MODE=react` по образцу `hotspots.spec.ts` — опционально, для числа `Styled(td)` на шаг перетаскивания. @@ -436,13 +436,18 @@ const pinnedLayoutKey = useMemo( - Обоснование: решение 12 и требование «PR открывается с кодом, тестами и чек-листом». Backend в задаче не меняется. - Цена ошибки: если ручной регресс найдёт нарушение корректности, исправление пойдёт коммитами в уже открытый PR. 18. **2026-10-28 (контроллер, R-2, O-1) — тест-допущение table-core заменено на фиксацию фактического поведения 8.20.5; ключ сброса по закреплению сохранён.** - - Факт: смена `columnPinning` при прежнем `columns` пересоздаёт ячейку только закрепляемой колонки; смена `columnSizing` ячейки не пересоздаёт. «Контекст» п. 3 и D1 исправлены. - - Обоснование: решение человека (сброс при закреплении, скрытии, фильтрах) не меняется. Остальные ячейки закрепление не пересоздаёт, а их `left`/`data-pinned` считает мемоизированная обёртка MRT. + - Факт: смена `columnPinning` при прежнем `columns` пересоздаёт ячейки закреплённых колонок (стороны), центральные те же; смена `columnSizing` ячейки не пересоздаёт. «Контекст» п. 3 и D1 исправлены. + - Обоснование: решение человека (сброс при закреплении, скрытии, фильтрах) не меняется. Центральные ячейки закрепление не пересоздаёт, а их `left`/`data-pinned` считает мемоизированная обёртка MRT. - Цена ошибки: если table-core когда-нибудь начнёт пересоздавать все ячейки, ключ станет частично избыточным (лишняя перерисовка на редких действиях). Тесты `table-core:` это покажут. -19. **2026-10-28 (исполнитель задачи 1, R-3) — формулировка «ключ избыточен по закреплению» из рулинга R-2 неточна и не принята.** Тест pinning утверждает: новая только ячейка закрепляемой колонки, остальные те же. Обоснование — решение 18. Цена ошибки: только формулировка. +19. **2026-10-28 (исполнитель задачи 1, R-3) — формулировка «ключ избыточен по закреплению» из рулинга R-2 неточна и не принята.** Исходный тест pinning утверждал: новая только ячейка закрепляемой колонки; уточнено финальным ревью (M-1): пересоздаются ячейки всех закреплённых колонок стороны. Обоснование — решение 18. Цена ошибки: только формулировка. 20. **2026-10-28 (исполнитель задачи 6, R-4) — в сценарий `pinned-resize` добавлены решения, которых нет в плане:** - новая причина пропуска `skip_reason: 'column_not_found'` (не нашлась колонка для закрепления); - если в манифесте нет пользователя `rf`, тесты не регистрируются; - ожидание не менее 20 строк сделано через `page.waitForFunction` с таймаутом `PERF_LOAD_TIMEOUT_MS`, без SETTLE-логики `scroll.spec.ts`. - Обоснование: аналогично `resizer_not_found`; сценарий не должен падать целым прогоном при отсутствии данных. - Цена ошибки: запись помечается `incomplete` вместо падения теста. Видно по `skip_reason` в результатах. +21. **2026-10-28 (контроллер, R-5, I-2) — сценарий `pinned-resize` изменён: закрепляются две data-колонки, тянется первая; добавлены проверки `pinned_cells_missing` (<4 закреплённых td) и `resize_no_effect`.** + - Обоснование: прежняя раскладка тянула последнюю закреплённую колонку, у которой нет закреплённого соседа справа, поэтому устаревший `left` появиться не мог и проверка `findPinnedGap` не могла упасть. + - Ширина измеряется одним `boundingBox` на шаге 50 (пик хода), до хода — один вызов; цена — один дополнительный вызов внутри пробы из 100 шагов. + - Цена ошибки: замеры нового сценария несравнимы со снятыми по старой раскладке; сравнение с базой делается тем же сценарием на обеих ветках. +22. **2026-10-28 (контроллер, R-6, M-3, O-3) — диапазон `['','']` не нормализуется в ключе; код не меняется.** В TanStack такой диапазон не снимает фильтр колонки, ключ отличается от «без фильтров»; это безвредный лишний сброс. Наблюдение O-3 (вне скоупа, follow-up): `Cell/Cell/Cell.jsx` ~112 `createHighlightedContent` вызывает `q.replace` на значении фильтра-массива (поведение базы). Цена ошибки: одна лишняя перерисовка на редком действии. diff --git a/openspec/changes/aurora-1839-cell-memo/specs/sheet-table/spec.md b/openspec/changes/aurora-1839-cell-memo/specs/sheet-table/spec.md index 3e6993e..a6cf0b3 100644 --- a/openspec/changes/aurora-1839-cell-memo/specs/sheet-table/spec.md +++ b/openspec/changes/aurora-1839-cell-memo/specs/sheet-table/spec.md @@ -180,7 +180,7 @@ - при изменении данных; - пока ширину колонки тянут. -Тест MUST также фиксировать допущение `@tanstack/table-core`: новый массив колонок даёт новые объекты ячеек, а смена закрепления при прежнем массиве колонок — нет. +Тест MUST также фиксировать поведение `@tanstack/table-core` 8.20.5: новый массив колонок даёт новые объекты ячеек; смена `columnSizing` при прежнем массиве колонок ячейки не пересоздаёт (поэтому ключ обязателен для ширины закреплённой колонки); смена `columnPinning` пересоздаёт ячейки закреплённых колонок (сторон), центральные ячейки остаются теми же. #### Scenario: Ключ реагирует на раскладку закреплённых колонок и фильтры - **WHEN** запускается `node --test` для модуля ключа сброса @@ -188,7 +188,7 @@ #### Scenario: Допущение о пересоздании ячеек - **WHEN** запускается `node --test` для теста допущения на `@tanstack/table-core` -- **THEN** тест подтверждает: при прежних `columns` и `data` объекты `row.getVisibleCells()[i]` те же; при новом массиве `columns` (даже с теми же определениями) — новые; при смене `columnPinning` с прежним массивом `columns` — те же +- **THEN** тест подтверждает: при новом массиве `columns` (даже с теми же определениями) объекты `row.getVisibleCells()[i]` новые; при смене `columnSizing` с прежним массивом `columns` — те же; при смене `columnPinning` с прежним массивом `columns` — новые у ячеек закреплённых колонок, у центральных — те же ### Requirement: Перетаскивание ширины закреплённой колонки не медленнее базы @@ -197,6 +197,6 @@ Порог — цель, а не условие слияния; недобор оформляется follow-up issue (см. требование «Шаг прокрутки…»). Корректная раскладка закреплённых колонок после отпускания мыши — требование корректности. #### Scenario: Замер перетаскивания ширины закреплённой колонки -- **WHEN** сценарий закрепляет на FORM_2/AHR (роль rf, CPU 1×) первую незакреплённую колонку данных и тянет её правую границу шагами по 4 px вправо и обратно, с ожиданием кадра после каждого шага +- **WHEN** сценарий закрепляет на FORM_2/AHR (роль rf, CPU 1×) две первые колонки данных и тянет правую границу первой из них шагами по 4 px вправо и обратно, с ожиданием кадра после каждого шага - **THEN** медиана `main_thread_ms_per_step` на этой ветке не больше, чем на ветке AURORA-1837 -- **AND** после отпускания мыши закреплённые колонки примыкают друг к другу (проверка геометрии в сценарии проходит) +- **AND** после отпускания мыши не менее четырёх `td[data-pinned="true"]` строки (`mrt-row-expand`, `sort_order`, две колонки данных) примыкают друг к другу (проверка геометрии в сценарии проходит)