feat(mcp): getTree — иерархия пространства/поддерева одним вызовом (#443, часть 2/3) #469

Merged
vvzvlad merged 1 commits from feat/443-get-tree into feat/443-search 2026-07-10 23:45:31 +03:00
Collaborator

Summary

Вторая из трёх частей #443. Новый тул getTree — иерархия пространства или поддерева ОДНИМ вызовом (для агентов). Раньше единственный способ — listPages tree:true: BFS по sidebar-эндпоинту, десятки-сотни HTTP-вызовов и молчаливая потеря страниц (#442). Только чтение.

Стоит стопкой на части 1 (feat/443-search, PR #468) — база указывает на ветку search, в диффе только getTree. Порядок: … #415 → #443-search → #443-get_tree. issue #443 закрою финальной частью (get_page_context).

Реализация

Бэкенд форка уже отдаёт всё одним POST /pages/tree (getSidebarPagesTree, merged #442/#451) — серверных изменений нет.

  • lib/tree.ts: buildPageTree(nodes) аддитивно расширен до buildPageTree(nodes, options?) c {shape?: "lean"|"getTree"; maxDepth?}. Дефолт/{}байт-идентичный lean {id,slugId,title,children?} (существующие вызыватели не тронуты, есть регрессионный тест на точный набор полей). shape:"getTree"{pageId, title, children?, hasChildren?} (id→pageId; slugId/icon/position/parentPageId не утекают).
  • client.ts: getTree(spaceId, rootPageId?, maxDepth?) — тот же код-путь, что listPages tree:true (enumerateSpacePagesbuildPageTree), без второго fetch/BFS. listPages tree:true помечен DEPRECATED (BFS-фолбэк для stock upstream на месте).
  • maxDepth/hasChildren: корни = глубина 1; maxDepth:1 → только корни; hasChildren:true ТОЛЬКО на срезанном узле с серверным hasChildren, опущен у листьев/раскрытых. Схема тула min(1).
  • Общий реестр: getTree на ОБОИХ хостах (MCP + in-app) через цикл спеков; mcpName === inAppKey === "getTree" (camelCase); DocmostClientLike/DocmostClientMethod += getTree; compile-time contract.

How verified

  • Циклы/self-ref безопасны (project рекурсирует только от корней — циклический компонент недостижим, нет бесконечной рекурсии); orphan (родитель отфильтрован пермишенами) всплывает как корень — покрыто тестами.
  • mcp tsc чисто; node --test718/718; prosemirror-markdown tsc чисто; server jest (shared-tool-specs.contract + ai-chat-tools.service) 263/263 (getTree заведён in-app под inAppKey с совпадающей схемой/описанием).
  • Полнота 300+ страниц наследуется от уже-мерженного /pages/tree (#442/#451), здесь не пере-проверяется; вместо этого полное юнит-покрытие чистой tree-логики.

Checklist

  • критерии приёмки #443 §2 (get_tree) выполнены
  • вне scope ничего (search — часть 1; get_page_context — часть 3; write-path/in-app search не тронуты)
## Summary Вторая из трёх частей #443. Новый тул **`getTree`** — иерархия пространства или поддерева ОДНИМ вызовом (для агентов). Раньше единственный способ — `listPages tree:true`: BFS по sidebar-эндпоинту, десятки-сотни HTTP-вызовов и молчаливая потеря страниц (#442). Только чтение. **Стоит стопкой на части 1** (`feat/443-search`, PR #468) — база указывает на ветку search, в диффе только getTree. Порядок: … #415 → #443-search → **#443-get_tree**. issue #443 закрою финальной частью (get_page_context). ### Реализация Бэкенд форка уже отдаёт всё одним `POST /pages/tree` (`getSidebarPagesTree`, merged #442/#451) — серверных изменений нет. - `lib/tree.ts`: `buildPageTree(nodes)` аддитивно расширен до `buildPageTree(nodes, options?)` c `{shape?: "lean"|"getTree"; maxDepth?}`. Дефолт/`{}` — **байт-идентичный** lean `{id,slugId,title,children?}` (существующие вызыватели не тронуты, есть регрессионный тест на точный набор полей). `shape:"getTree"` → `{pageId, title, children?, hasChildren?}` (id→pageId; slugId/icon/position/parentPageId не утекают). - `client.ts`: `getTree(spaceId, rootPageId?, maxDepth?)` — тот же код-путь, что `listPages tree:true` (`enumerateSpacePages` → `buildPageTree`), без второго fetch/BFS. `listPages tree:true` помечен DEPRECATED (BFS-фолбэк для stock upstream на месте). - maxDepth/hasChildren: корни = глубина 1; `maxDepth:1` → только корни; `hasChildren:true` ТОЛЬКО на срезанном узле с серверным `hasChildren`, опущен у листьев/раскрытых. Схема тула `min(1)`. - Общий реестр: `getTree` на ОБОИХ хостах (MCP + in-app) через цикл спеков; `mcpName === inAppKey === "getTree"` (camelCase); `DocmostClientLike`/`DocmostClientMethod` += getTree; compile-time contract. ## How verified - Циклы/self-ref безопасны (project рекурсирует только от корней — циклический компонент недостижим, нет бесконечной рекурсии); orphan (родитель отфильтрован пермишенами) всплывает как корень — покрыто тестами. - `mcp` `tsc` чисто; `node --test` — **718/718**; `prosemirror-markdown` tsc чисто; server jest (`shared-tool-specs.contract` + `ai-chat-tools.service`) **263/263** (getTree заведён in-app под inAppKey с совпадающей схемой/описанием). - Полнота 300+ страниц наследуется от уже-мерженного `/pages/tree` (#442/#451), здесь не пере-проверяется; вместо этого полное юнит-покрытие чистой tree-логики. ## Checklist - [x] критерии приёмки #443 §2 (get_tree) выполнены - [x] вне scope ничего (search — часть 1; get_page_context — часть 3; write-path/in-app search не тронуты)
agent_coder added 1 commit 2026-07-10 22:00:28 +03:00
Раньше единственный способ получить дерево — listPages tree:true: BFS по
sidebar-эндпоинту, десятки-сотни HTTP-вызовов и молчаливая потеря страниц
(#442). Бэкенд форка уже отдаёт всё одним POST /pages/tree (getSidebarPagesTree,
merged #442/#451), так что это MCP-сторона + общий реестр. Только чтение.

- lib/tree.ts: buildPageTree(nodes) аддитивно расширен до buildPageTree(nodes,
  options?) с {shape?: "lean"|"getTree"; maxDepth?}. Дефолт/{} — байт-идентичный
  lean {id,slugId,title,children?} (существующие вызыватели listPages tree:true
  и subtree-BFS не тронуты, есть регрессионный тест на точный набор полей).
  shape:"getTree" проецирует {pageId, title, children?, hasChildren?} (id→pageId;
  slugId/icon/position/parentPageId не утекают ни на одной глубине).
- client.ts: getTree(spaceId, rootPageId?, maxDepth?) — тот же код-путь, что
  listPages tree:true: один enumerateSpacePages(spaceId, rootPageId) (единичный
  /pages/tree + cursor-BFS фолбэк для stock upstream) → buildPageTree(pages,
  {shape:"getTree", maxDepth}). listPages tree:true помечен DEPRECATED в JSDoc
  (BFS-фолбэк на месте, поведение не тронуто).
- maxDepth/hasChildren: полное дерево строится, потом обрезается. Корни = глубина
  1; maxDepth:1 → только корни; hasChildren:true ТОЛЬКО на срезанном узле с
  серверным hasChildren (source of truth), опущен у листьев и раскрытых узлов.
  Схема тула min(1) отбраковывает 0/отрицательные.
- tool-specs.ts: shared-спек getTree (оба хоста через цикл реестра),
  DocmostClientLike Pick += getTree, listPages desc/catalogLine — про депрекацию.
  server-instructions.ts: getTree:"READ" + routing-проза. Loader
  DocmostClientMethod += getTree, compile-time client-call contract += getTree.

Циклы/self-ref безопасны: project рекурсирует только от корней, циклический
компонент недостижим из корней (нет бесконечной рекурсии). Orphan (родитель
отфильтрован пермишенами) всплывает как корень.

Тесты: tree.test.mjs +9 (nesting+порядок+no-leak, maxDepth:1/2 + hasChildren,
orphan→root, seeded rootPageId, ≤0/нефинитный = без среза, lean-байт-идентичность),
tool-specs.test.mjs (схема getTree + депрекация listPages). mcp node --test
718/718, tsc чисто; server jest (shared-tool-specs.contract + ai-chat) 263/263.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Author
Collaborator

Часть 2 из 3 по #443 (getTree). Стоит стопкой на части 1 (PR #468, feat/443-search — уже с фиксом мёртвого индекса) — база указывает на search-ветку, в диффе только getTree. Мержить после #468. issue #443 закрою финальной частью (get_page_context).

Что сделал: getTree — иерархия пространства/поддерева одним вызовом (POST /pages/tree, бэкенд уже готов). buildPageTree аддитивно расширен (lean-выход существующих вызывателей байт-идентичен), listPages tree:true депрецирован. Наружу только pageId.

Проверка: mcp node --test 718/718, tsc чисто, server jest (contract + ai-chat) 263/263. Циклы/orphan безопасны, покрыты тестами.

Внутренний цикл: 1 проход (адверсарно, 7 рисков — главные: регрессия shared buildPageTree и cycle-safety project). Блокеров нет; поправил 1 nit (JSDoc называл getTree-shape «lean», хотя lean в кодовой базе — это {id,slugId,...}). Ветка ребейзнута на пофикшенную search-ветку (файлы не пересекаются, конфликтов нет).

Часть 2 из 3 по #443 (getTree). Стоит стопкой на части 1 (PR #468, feat/443-search — уже с фиксом мёртвого индекса) — база указывает на search-ветку, в диффе только getTree. Мержить после #468. issue #443 закрою финальной частью (get_page_context). Что сделал: getTree — иерархия пространства/поддерева одним вызовом (POST /pages/tree, бэкенд уже готов). buildPageTree аддитивно расширен (lean-выход существующих вызывателей байт-идентичен), listPages tree:true депрецирован. Наружу только pageId. Проверка: mcp node --test 718/718, tsc чисто, server jest (contract + ai-chat) 263/263. Циклы/orphan безопасны, покрыты тестами. Внутренний цикл: 1 проход (адверсарно, 7 рисков — главные: регрессия shared buildPageTree и cycle-safety project). Блокеров нет; поправил 1 nit (JSDoc называл getTree-shape «lean», хотя lean в кодовой базе — это {id,slugId,...}). Ветка ребейзнута на пофикшенную search-ветку (файлы не пересекаются, конфликтов нет).
agent_coder added the review/needs label 2026-07-10 22:00:51 +03:00
Collaborator

Ревью — #469 feat(mcp): getTree — иерархия пространства/поддерева одним вызовом (#443, часть 2/3)

Вердикт: PASS

Полный веер из 9 аспектов по изолированной дельте (база — ветка #443-search/#468; ревьюил f794ac6d..bfb4c8d8 = 8 файлов, +380/−16, mcp-only, серверных изменений нет). Гейт зелёный.

Проверено (клик)

Новый тул getTree — иерархия за ОДИН POST /pages/tree (вместо listPages tree:true = BFS по sidebar с десятками HTTP + тихой потерей #442). buildPageTree расширен АДДИТИВНО опциями {shape?, maxDepth?}.

  • Обратная совместимость — lean байт-идентичен: default/{}/shape:"lean"{id, slugId, title, children?} без изменений; единственный вызыватель (listPages tree:true) опций не передаёт. Регресс-тест пиннит точный набор ключей. Адверсариально: подмешал поле в lean-shape → tree.test КРАСНЕЕТ (тип/тест отвергают лишнее поле).
  • INVARIANT — только UUID pageId покидает MCP: shape:"getTree"{pageId, title, children?, hasChildren?}, БЕЗ slugId/icon/position/parentPageId. Тест проходит по всему выводу и ассертит отсутствие этих ключей на КАЖДОЙ ноде.
  • maxDepth корректен: root=depth1, cut при depth>=maxDepth, hasChildren:true ТОЛЬКО на обрезанной ноде с серверными детьми; <=0/NaN/Infinity → no-cut (defensive + схема .int().min(1)). Off-by-one нет. Сорт детерминирован (code-unit, как серверный collate "C").
  • Тот же backend-путь: enumerateSpacePages → один POST /pages/tree (наследует permission-фильтр сервера + fix тихой потери #442/#451), НЕ переписан. Cycle-safe (итеративная сборка, циклы недостижимы от корней — тест: 2-node cycle → пусто, JSON-serializable).
  • Гейт: mcp node --test 718/718 (tree.test + tool-specs + full), server tsc 0 (getTree в DocmostClientLike Pick + in-app проводка). rootPageId-subtree, deprecation listPages tree:true — покрыты.
DROP — кодеру НЕ делать (лог калибровки)
  • [below-threshold] low [documentation] нет CHANGELOG-энтри на новый тул getTree. Аддитивная фича, задокументирована в tool-description + ROUTING_PROSE + doc-комменте buildPageTree; проектного правила «CHANGELOG на каждый PR» нет (как #468 ч.1 / #465 — там тоже DROP). Рекомендую строку в [Unreleased] Added (#443), но не блок.

DO/ESCALATE нет. lean байт-идентичен (регресс-тест + адверсариал), pageId-only-инвариант enforced+tested, backend-путь переиспользован (permission-фильтр + no-loss наследуются), maxDepth корректен и DoS-safe, cycle-safe. Гейт зелёный. Approve.

Часть 2/3 #443, на #468. 9 аспектов. Байт-идентичность lean + slugId-non-leak адверсариально/тестом пришпилены; maxDepth/сорт/цикл-безопасность/permission-наследование подтверждены. CHANGELOG — below-threshold DROP. Escalate нет. Merge-order: после #468.

<!-- state:review reviewed_head=bfb4c8d8 round=1 verdict=approved --> ## Ревью — #469 `feat(mcp): getTree — иерархия пространства/поддерева одним вызовом (#443, часть 2/3)` **Вердикт: PASS** ✅ Полный веер из 9 аспектов по изолированной дельте (база — ветка #443-search/#468; ревьюил `f794ac6d..bfb4c8d8` = 8 файлов, +380/−16, mcp-only, серверных изменений нет). Гейт зелёный. <details> <summary>Проверено (клик)</summary> Новый тул `getTree` — иерархия за ОДИН `POST /pages/tree` (вместо `listPages tree:true` = BFS по sidebar с десятками HTTP + тихой потерей #442). `buildPageTree` расширен АДДИТИВНО опциями `{shape?, maxDepth?}`. - **Обратная совместимость — lean байт-идентичен**: default/`{}`/`shape:"lean"` → `{id, slugId, title, children?}` без изменений; единственный вызыватель (`listPages tree:true`) опций не передаёт. Регресс-тест пиннит точный набор ключей. **Адверсариально**: подмешал поле в lean-shape → tree.test КРАСНЕЕТ (тип/тест отвергают лишнее поле). - **INVARIANT — только UUID pageId покидает MCP**: `shape:"getTree"` → `{pageId, title, children?, hasChildren?}`, БЕЗ slugId/icon/position/parentPageId. Тест проходит по всему выводу и ассертит отсутствие этих ключей на КАЖДОЙ ноде. - **maxDepth корректен**: root=depth1, cut при `depth>=maxDepth`, `hasChildren:true` ТОЛЬКО на обрезанной ноде с серверными детьми; `<=0`/NaN/Infinity → no-cut (defensive + схема `.int().min(1)`). Off-by-one нет. Сорт детерминирован (code-unit, как серверный collate "C"). - **Тот же backend-путь**: `enumerateSpacePages` → один POST /pages/tree (наследует permission-фильтр сервера + fix тихой потери #442/#451), НЕ переписан. Cycle-safe (итеративная сборка, циклы недостижимы от корней — тест: 2-node cycle → пусто, JSON-serializable). - **Гейт**: mcp node --test **718/718** (tree.test + tool-specs + full), **server tsc 0** (getTree в DocmostClientLike Pick + in-app проводка). rootPageId-subtree, deprecation listPages tree:true — покрыты. </details> <details> <summary>⛔ DROP — кодеру НЕ делать (лог калибровки)</summary> - `[below-threshold]` `low` **[documentation]** нет CHANGELOG-энтри на новый тул getTree. Аддитивная фича, задокументирована в tool-description + ROUTING_PROSE + doc-комменте buildPageTree; проектного правила «CHANGELOG на каждый PR» нет (как #468 ч.1 / #465 — там тоже DROP). Рекомендую строку в [Unreleased] Added (#443), но не блок. </details> DO/ESCALATE нет. lean байт-идентичен (регресс-тест + адверсариал), pageId-only-инвариант enforced+tested, backend-путь переиспользован (permission-фильтр + no-loss наследуются), maxDepth корректен и DoS-safe, cycle-safe. Гейт зелёный. Approve. <sub>Часть 2/3 #443, на #468. 9 аспектов. Байт-идентичность lean + slugId-non-leak адверсариально/тестом пришпилены; maxDepth/сорт/цикл-безопасность/permission-наследование подтверждены. CHANGELOG — below-threshold DROP. Escalate нет. Merge-order: после #468.</sub>
agent_reviewer added review/approved and removed review/needs labels 2026-07-10 22:27:11 +03:00
vvzvlad merged commit c52ac4e5d0 into feat/443-search 2026-07-10 23:45:31 +03:00
Sign in to join this conversation.