fix(mcp): курсорная пагинация — устранить тихую потерю страниц в list_pages/check_new_comments (#442) #451
Reference in New Issue
Block a user
Delete Branch "fix/442-cursor-pagination"
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
Data-loss фикс:
list_pages tree:trueмолча терял страницы и жёг до 50 холостых запросов на «широкого» родителя. Апстрим78b1c1a4перевёл серверные эндпоинты на КУРСОРНУЮ пагинацию, а глобальныйValidationPipe({whitelist:true})молча вырезает неизвестное полеpage. MCP-клиент так и слал офсетныйpage→ сервер отдавал ту же первую двадцатку сhasNextPage:true, цикл выкручивался доMAX_PAGES=50одинаковых запросов, дети №21+ не выгружались вместе с поддеревьями; дедупvisitedгасил дубли → «дырявое» дерево без единой ошибки. Netmap: 20/299 страниц терялось, 160 запросов вместо 62.closes #442
enumerateSpacePages→ одинPOST /pages/tree(весь спейс/поддерево разом, permission-filtered); fallback на курсорный BFS при 404/405 (stock upstream без форкового эндпоинта). Возврат{pages, truncated};truncatedчестный —trueтолько при реальном упоре fallback-BFS вMAX_NODES(primary-путь без cap → всегдаfalse).listSidebarPages→ курсорный цикл,limit:100, guard на неподвижный курсор (!next || next===cursor → break) — если протокол снова разойдётся, цикл не крутит дубли молча; warn при упоре вMAX_PAGES.paginateAll(/spaces,/shares) → та же курсорная миграция + guard + сохранённый truncation-warn.check_new_comments—/pages/treeподдерева включает корень (getPageAndDescendants), убран лишнийgetPageRaw; в fallback-ветке корень засевается явно (иначе его комментарии терялись — регрессия того же класса).listComments:do/while→forсMAX_PAGES+ guard неподвижного курсора (был безлимитный — тот же сценарий #442 дал бы бесконечный цикл).Эффект: Netmap 160 запросов → 1, потерь нет.
How verified
Прогон на стенде (
node --test):pnpm --filter @docmost/mcp build→ чисто (tsc).node --test test/mock/pagination-cursor.test.mjs test/mock/reauth.test.mjs→ 17/17 passed: курсорный цикл собирает все элементы (кол-во запросов = кол-ву страниц); regression-тест класса бага — сервер игнорирует курсор → цикл завершается за 1 лишнюю итерацию без дублей (не крутит до MAX_PAGES); happy-path/pages/treeодним запросом; fallback на 404; fallback поддерева засевает корень (проверено пойманным/pages/info+/api/commentsна root);listCommentsimmovable-cursor → 2 запроса без дублей.markdown-converter.test.mjs(отступы вложенных списков) — ортогональны, подтверждено на базе.Внутренний цикл: 2 прохода. Первый нашёл потерю комментариев корня в fallback поддерева (data-loss) → засев корня; догрёб honest-
truncated(убрал ложную эвристику>=10000на primary-пути), warn вlistSidebarPages, guard вlistComments. Второй проход — APPROVE: форма возврата{pages,truncated}распространена на оба вызова без пропусков, семантикаtruncatedчестна end-to-end.Checklist
Ревью — #451
fix(mcp): курсорная пагинация — устранить тихую потерю страниц в list_pages/check_new_comments (#442)Вердикт: PASS ✅
Полный веер из 9 аспектов по всему диффу PR (merge-base
ae790da1). Гейт зелёный (clean-room/home/claude/gm-ci): mcp build (tsc) 0, node --test 587/587 (pagination-cursor +440).Корректность пагинации — проверено (клик)
next = meta.hasNextPage ? nextCursor : null), продвижение завязано на курсор, а не на размер страницы. Все 3 пагинатора (paginateAll/listSidebarPages/listComments) идентичной структуры.if (!next || next === cursor) break;предотвращает: (а) бесконечный цикл и (б) runaway-дубли при «застрявшем» курсоре (сервер игнорит курсор → break на 2-м запросе, а не спин до MAX_PAGES). Мутационно покрыт (сломать гард → тесты «terminates no dups» падают).truncated(взводится только при упоре в потолок с продвижением).enumerateSpacePages: первичный/pages/tree(без капа) + BFS-фолбэк с seeding root через getPageRaw на 404.checkNewCommentsберёт{pages, truncated}напрямую.⛔ DROP — кодеру НЕ делать (лог калибровки)
Клиентская логика корректна и её ядро (курсор-цикл + immovable-гард) мутационно-покрыто; остаток — redundant/тривиально:
[below-threshold]med[test-coverage] нет теста «мутация датасета mid-pagination → нет пропуска/дубля» (ревьюер оценил critical). Мисскоуп для КЛИЕНТА: mcp-клиент следует курсору сервера ИДЕНТИЧНО, мутировал датасет или нет — иммунитет к mutation-loss это СЕРВЕРНОЕ свойство (стабильный keyset), вне зоны mcp-клиента; клиентский mutation-тест дублирует уже покрытый static-walk (collects-every-item). Ответственность клиента (обойти страницы, собрать всё, гардить кривой сервер) — покрыта.[below-threshold]low[test-coverage] нет отдельных тестов пустого результата и одностраничного случая — тривиальные пути, обрабатываются уже покрытым гардом!next(correctness-агент это подтвердил). Если хочешь для полноты — дёшево добавить, но не отдельный раунд.[below-threshold]low[simplification] гардif (!next || next === cursor) break;дублируется в 3 функциях — 2-3 строки, можно вынести вshouldStopCursorPagination(), но не критично.[by-design]low[coherence] убран fallbackitems.length < limitдля last-page — намеренно и корректно (сервер = источник истины для курсора), immovable-гард страхует протокол-дрейф.DO/ESCALATE нет. Курсорная пагинация корректна (продвижение по курсору, гард от потери/дублей/бесконечного цикла мутационно-держится, MAX_PAGES+truncated), гейт зелёный. Тихая потеря устранена. Approve.
9 аспектов. Корректность цикла (нет потери/дублей/инфинит-лупа) + immovable-гард-мутация проверены; mutation-scenario тест — мисскоуп (серверный keyset), edge-кейсы тривиальны → DROP с обоснованием (как #364/#439). Escalate нет.