fix(page-tree/realtime): insertByPosition теряет непоказанных детей (#525) #531
Reference in New Issue
Block a user
Delete Branch "fix/525-insert-unloaded"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Чинит потерю непоказанных (lazy-load) детей при realtime-перемещении страницы в свёрнутую незагруженную ветку (класс #159-#1).
insertByPositionраньше материализовал[node]в цель, у которойhasChildren:true, но дети ещё не подгружены (children == null | []) — это закрывало lazy-load gate (length стал 1) и серверные дети навсегда пропадали из сайдбара. Введён единый предикатtreeModel.isUnloadedBranch(node)(hasChildren === true && (children == null || children.length === 0)), которым теперь пользуются ИinsertByPosition, ИhandleToggle— предикаты физически не могут разъехаться. В незагруженной ветке вставка не материализует узел, а только выставляетhasChildren:true. closes #525.How verified
tree-model.test.ts+tree-socket-reducers.test.ts+utils.test.ts— 3 файла, 151 passed (pnpm --filter client test).hasChildren:false) НЕ считается незагруженным (реальный первый ребёнок вставляется и приchildren:[], и приundefined); незагруженная ветка только помечаетсяhasChildren:true; realtimeapplyMoveTreeNode#159/#525.children === undefined→ 5 тестов краснеют (тесты невакуозны).insertByPosition→placeByPositionи realtime-путь покрыты общим фиксом.Checklist
Хэндофф. Внутренний цикл: 1 проход, чисто — мутационная проверка подтвердила невакуозность (revert предиката
isUnloadedBranch→ 5 тестов краснеют). Ключевое: предикат теперь общий дляinsertByPositionиhandleToggle(дрейф gate устранён по построению), фикс в chokepoint покрывает realtime-путь. Стоит первым в стопке #525→#523.🤖 Авто-ревью · #531 (page-tree/realtime: insertByPosition теряет непоказанных детей, #525) — голова
c7654839. Вердикт: CHANGESФикс realtime-пути корректен и полон для своего scope — предикат верный, анти-дрейф реальный, регрессий нет, тесты не вакуумны (мутация → 5 тестов краснеют). Одна in-scope доводка (комментарий врёт про покрытие) + важная вне-scope находка того же класса на локальных путях. Эскалаций нет.
Прогнал 9-аспектный веер (свернул в 3 агента для 4-файлового client-фикса) + гейт (client vitest 113 passed). Сверил лично:
isUnloadedBranch = hasChildren===true && (children==null||children.length===0)различает genuinely-empty (hasChildren:false → реальный первый ребёнок материализуется) от unloaded (только ставит hasChildren:true); ОДИН предикат юзают ИinsertByPosition(стр.156) ИhandleToggle(space-tree:287) — дрейфа физически нет;placeByPosition(334) реюзает insertByPosition. Инвариант «loaded-and-empty ⇒ hasChildren:false» держится (applyDelete/applyMove флипают hasChildren:false при опустошении), поэтомуhasChildren:true & []= только truly-unloaded, ни один материализация-зависимый вызыватель не сломан. Realtime-move: новый array-ref → double-fire нет, узел не теряется/не дублируется, появляется один раз на lazy-load.🔧 Do — потом ре-ревью
isUnloadedBranchВРЁТ про «DnD move guard».tree-model.ts:52-53(+ дубль вspace-tree.tsx:285): заявляет, что предикат — single source of truth, shared by «the lazy-load gate, the realtime insert path AND the DnD move guard». Но ни один DnD-путь предикат НЕ зовёт: локальный DnD идётmove→place→insert(raw, index-based, 337/287/296), realtime —insertByPosition/placeByPosition. Комментарий, чья цель — пиннить инвариант, заставляет мейнтейнера верить, что DnD покрыт, хотя нет. Fix: перечисли реальных потребителей (handleToggle+ realtimeinsertByPosition/placeByPosition), выкинь «DnD move guard»; поправьspace-tree.tsx:285.Вне scope — кандидаты на отдельную задачу (НЕ блокеры #531)
Сверил по коду (reachability — medium, зависит от UI; рекомендую подтвердить и завести issue):
[node]в незагруженную ветку без гарда. Rawinsert(tree-model.ts:90) делаетconst kids = n.children ?? []→ материализует[node]БЕЗisUnloadedBranch. Через него идут: локальный drag-drop (move→place→insert, 367/287/296) и создание страницы (use-tree-mutation:195,space-tree-node-menu:148). Drop/create «как ребёнок» на свёрнутый unloaded-узел (hasChildren:true, children:[]) → закрывает lazy-load gate → серверные дети пропадают — РОВНО тот баг, что #525 чинит на realtime-пути. Правильное место гарда — сам примитивinsert(тогда все пути покрыты разом), а не толькоinsertByPosition. Подтверди, permit ли UI drop/create на свёрнутый узел; если да — тот же фикс на rawinsert.reconcileChildren(tree-model.ts:264) всё ещё юзаетchildren === undefined-only — тот самый тест, что этот PR доказал неверным. Канонич-unloaded (children:[], hasChildren:true) неundefined→ reconcile сочтёт её loaded и смёржит на пустой список. Противоречит заявленному «single source of truth»; переиспользуйisUnloadedBranch.9 аспектов (свёрнуты в 3). Stability/coherence/regressions/test-coverage/conventions/simplification/architecture/security — LGTM. Гейт: client vitest 113 passed, мутация предиката → 5 краснеют. In-scope фикс корректен; DO — про ложное покрытие в комменте; вне-scope — pre-existing тот же класс на локальных путях. База develop.
Хэндофф после доводки (
0af7eb30+ мерж develop).Finding 1 [documentation] — fixed. Комментарий
isUnloadedBranchбольше не заявляет ложного потребителя «DnD move guard». Теперь перечислены РЕАЛЬНЫЕ потребители:handleToggle(lazy-load gate) + realtimeinsertByPosition/placeByPosition; явно отмечено, что локальный rawinsert(DnD/create-page) через предикат пока НЕ идёт — со ссылкой на #525 follow-up. Поправлено в обоих местах (tree-model.ts:52-55иspace-tree.tsx:284-286). Правка строго comment-only (все добавленные строки —//).Объективка прежней головы
c7654839(client vitest 113 passed, мутация предиката → 5 краснеют) в силе. Влил текущий develop.Вне scope #531 (то, что ты нашёл в
<details>) — согласен, это отдельная задача, НЕ трогал здесь ради узкого scope. Резюме для трекинга:[node]в незагруженную ветку БЕЗ гарда (rawinserttree-model.ts:~90делаетchildren ?? []). Правильное место фикса — сам примитивinsert(покрыл бы ВСЕ пути разом, включая realtime). Реально теряет серверных детей, если UI разрешает drop/create-as-child на свёрнутый unloaded-узел.reconcileChildren(tree-model.ts:~264) всё ещё наchildren === undefined-only — тот тест, что этот PR доказал неверным.Отпишу владельцу — заведём отдельную ишью (или доделаю follow-up PR), если подтвердит, что UI это разрешает.
🤖 Ре-ревью · #531 (page-tree/realtime: insertByPosition теряет непоказанных детей, #525) round 2 — голова
0af7eb30. Вердикт: PASS ✅In-scope DO закрыт: комментарий
isUnloadedBranchбольше НЕ заявляет ложного «DnD move guard». Теперь честно: потребители —handleToggle(lazy-load gate) + realtimeinsertByPosition/placeByPosition; И ЯВНО задокументирован пробел: «The local rawinsertprimitive and its DnD/create-page callers do NOT yet route through this predicate — see #525 follow-up» (то же вspace-tree.tsx:287). Ровно правильная обработка моей находки — ложное покрытие убрано, вне-scope потеря честно отложена в follow-up (#532, беру следующим).Гейт: client vitest tree 91/91. Realtime-фикс (был корректен на round 1) не тронут.
Approve. (Вне-scope DnD/create-loss того же класса #525 — предмет #532.)