feat(mcp): getPageContext — «где я / что вокруг» по pageId (#443, часть 3/3, closes #443) #470

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

Summary

Финальная, третья часть #443. Новый тул getPageContext — по pageId (из поиска, из ссылки) вернуть «где я / что вокруг»: сама страница + breadcrumbs + прямые дети, только метаданные, одним вызовом. Раньше это get_page + полное дерево. Только чтение. closes #443 (все три части: search #468, getTree #469, этот).

Стоит стопкой на части 2 (feat/443-get-tree, PR #469) — база указывает на getTree-ветку, в диффе только getPageContext. Порядок: … #468 search → #469 getTree → #470 getPageContext.

Реализация (бэкенд готов — серверных изменений нет)

  • client.ts getPageContext(pageId): resolvePageId (slugId→uuid; для uuid без round-trip) → POST /pages/breadcrumbs + listSidebarPages(spaceId, uuid). Два запроса для uuid-входа (spaceId из ответа breadcrumbs), +1 для slugId.
  • Выход: {page:{pageId,title,spaceId}, breadcrumbs:[{pageId,title}] (root→parent; [] для корня), children:[{pageId,title,hasChildren}]}.
  • Порядок breadcrumbs выверен по серверному источнику (getPageBreadCrumbs: CTE сидится на childPageId → вверх → .reverse() → root→page; страница — ПОСЛЕДНИЙ элемент): page = chain[last], breadcrumbs = chain.slice(0,-1), корень → []. Проекция явная — наружу только pageId (UUID), slugId/icon/position не утекают.
  • Ошибки: пустой chain / 404 / 403 → явная ошибка, не пустой объект.
  • children — прямые дети через listSidebarPages (cursor-пагинация #442/#451): страница с >20 детьми отдаёт всех без дублей, порядок по position.
  • Общий реестр: getPageContext на ОБОИХ хостах; mcpName===inAppKey==="getPageContext" (camelCase); DocmostClientLike/DocmostClientMethod += it; compile-time contract; TOOL_FAMILY READ + routing-проза.

How verified

  • mcp tsc чисто; node --test725/725 (6 новых get-page-context: 3-й уровень split, no-slugId-leak + 2 запроса, корень→[], slugId-resolve, >20 детей без cap/дублей, плохой id→ошибка, пустой chain→throw; +1 tool-specs); prosemirror-markdown tsc чисто; server jest (shared-tool-specs.contract + ai-chat-tools.service) 265/265.
  • Консистентность с getTree структурная (общий sidebar/position-источник детей, тот же UUID pageId).

Checklist

  • критерии приёмки #443 §3 (get_page_context) выполнены
  • #443 закрыт целиком (все 3 части); вне scope ничего; write-path/in-app search не тронуты
## Summary Финальная, третья часть #443. Новый тул **`getPageContext`** — по `pageId` (из поиска, из ссылки) вернуть «где я / что вокруг»: сама страница + breadcrumbs + прямые дети, только метаданные, одним вызовом. Раньше это `get_page` + полное дерево. Только чтение. **closes #443** (все три части: search #468, getTree #469, этот). **Стоит стопкой на части 2** (`feat/443-get-tree`, PR #469) — база указывает на getTree-ветку, в диффе только getPageContext. Порядок: … #468 search → #469 getTree → **#470 getPageContext**. ### Реализация (бэкенд готов — серверных изменений нет) - `client.ts` `getPageContext(pageId)`: `resolvePageId` (slugId→uuid; для uuid без round-trip) → `POST /pages/breadcrumbs` + `listSidebarPages(spaceId, uuid)`. **Два запроса** для uuid-входа (spaceId из ответа breadcrumbs), +1 для slugId. - Выход: `{page:{pageId,title,spaceId}, breadcrumbs:[{pageId,title}] (root→parent; [] для корня), children:[{pageId,title,hasChildren}]}`. - Порядок breadcrumbs **выверен по серверному источнику** (`getPageBreadCrumbs`: CTE сидится на childPageId → вверх → `.reverse()` → root→page; страница — ПОСЛЕДНИЙ элемент): `page = chain[last]`, `breadcrumbs = chain.slice(0,-1)`, корень → `[]`. Проекция явная — наружу только `pageId` (UUID), `slugId`/`icon`/`position` не утекают. - Ошибки: пустой chain / 404 / 403 → **явная ошибка, не пустой объект**. - `children` — прямые дети через `listSidebarPages` (cursor-пагинация #442/#451): страница с >20 детьми отдаёт всех без дублей, порядок по `position`. - Общий реестр: `getPageContext` на ОБОИХ хостах; `mcpName===inAppKey==="getPageContext"` (camelCase); `DocmostClientLike`/`DocmostClientMethod` += it; compile-time contract; `TOOL_FAMILY` READ + routing-проза. ## How verified - `mcp` `tsc` чисто; `node --test` — **725/725** (6 новых get-page-context: 3-й уровень split, no-slugId-leak + 2 запроса, корень→`[]`, slugId-resolve, >20 детей без cap/дублей, плохой id→ошибка, пустой chain→throw; +1 tool-specs); `prosemirror-markdown` tsc чисто; server jest (`shared-tool-specs.contract` + `ai-chat-tools.service`) **265/265**. - Консистентность с `getTree` структурная (общий sidebar/position-источник детей, тот же UUID `pageId`). ## Checklist - [x] критерии приёмки #443 §3 (get_page_context) выполнены - [x] #443 закрыт целиком (все 3 части); вне scope ничего; write-path/in-app search не тронуты
agent_coder added 1 commit 2026-07-10 22:19:50 +03:00
Финальная из трёх частей #443. Есть pageId (из поиска, из ссылки) — нужно понять
местоположение и окружение. Раньше это get_page + полное дерево. Только чтение,
только метаданные (без контента). closes #443.

- client.ts getPageContext(pageId): resolvePageId (slugId→uuid, для uuid без
  round-trip) → POST /pages/breadcrumbs + listSidebarPages(spaceId, uuid). Два
  запроса для uuid-входа (spaceId берётся из ответа breadcrumbs), +1 для slugId.
  Выход {page:{pageId,title,spaceId}, breadcrumbs:[{pageId,title}] (root→parent;
  [] для корня), children:[{pageId,title,hasChildren}]}.
- Порядок breadcrumbs выверен по серверному источнику (getPageBreadCrumbs: CTE
  сидится на childPageId, идёт вверх, .reverse() → root→page; страница —
  ПОСЛЕДНИЙ элемент). page = chain[last], breadcrumbs = chain.slice(0,-1); корень
  → []. Проекция явная — наружу только pageId (UUID), slugId/icon/position не
  утекают. Пустой chain / 404 / 403 → явная ошибка, не пустой объект.
- children — прямые дети через listSidebarPages (cursor-пагинация #442/#451,
  страница с >20 детьми отдаёт всех без дублей, порядок по position, hasChildren).
- Общий реестр: getPageContext на ОБОИХ хостах через цикл спеков (mcpName===
  inAppKey==="getPageContext", camelCase); DocmostClientLike/DocmostClientMethod
  += it; compile-time contract; TOOL_FAMILY READ + routing-проза.

Тесты: get-page-context.test.mjs +6 (3-й уровень split, no-slugId-leak + 2
запроса, корень→[], slugId-resolve, >20 детей без cap/дублей, плохой id→ошибка,
пустой chain→throw), tool-specs +1. mcp node --test 725/725, tsc чисто; server
jest (contract + ai-chat) 265/265.

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

Часть 3 из 3 по #443 (getPageContext) — финальная, closes #443. Стоит стопкой на части 2 (PR #469, feat/443-get-tree) — база указывает на getTree-ветку, в диффе только getPageContext. Мержить после #469.

Что сделал: getPageContext(pageId) — page + breadcrumbs + прямые дети одним вызовом, два запроса (breadcrumbs + sidebar). Порядок breadcrumbs выверил по серверному источнику (страница — последний элемент, root→page), не по памяти. Наружу только pageId. Плохой/недоступный id → явная ошибка, не пустой объект.

Проверка: mcp node --test 725/725 (6 новых), tsc чисто, server jest (contract + ai-chat) 265/265.

Внутренний цикл: 1 проход (адверсарно, 7 рисков — главные: split breadcrumbs+envelope и error-not-hollow — оба проверены против фактического page.service.ts::getPageBreadCrumbs, не по резюме). Блокеров нет; единственная находка — косметический юникод-апостроф, правки не требует.

Итого #443: три read-тула (search подстрочный, getTree, getPageContext), собраны стопкой search→getTree→getPageContext. Мержить в этом порядке после breaking-окна (#463→#466→#467).

Часть 3 из 3 по #443 (getPageContext) — финальная, **closes #443**. Стоит стопкой на части 2 (PR #469, feat/443-get-tree) — база указывает на getTree-ветку, в диффе только getPageContext. Мержить после #469. Что сделал: getPageContext(pageId) — page + breadcrumbs + прямые дети одним вызовом, два запроса (breadcrumbs + sidebar). Порядок breadcrumbs выверил по серверному источнику (страница — последний элемент, root→page), не по памяти. Наружу только pageId. Плохой/недоступный id → явная ошибка, не пустой объект. Проверка: mcp node --test 725/725 (6 новых), tsc чисто, server jest (contract + ai-chat) 265/265. Внутренний цикл: 1 проход (адверсарно, 7 рисков — главные: split breadcrumbs+envelope и error-not-hollow — оба проверены против фактического page.service.ts::getPageBreadCrumbs, не по резюме). Блокеров нет; единственная находка — косметический юникод-апостроф, правки не требует. Итого #443: три read-тула (search подстрочный, getTree, getPageContext), собраны стопкой search→getTree→getPageContext. Мержить в этом порядке после breaking-окна (#463→#466→#467).
agent_coder added the review/needs label 2026-07-10 22:20:09 +03:00
Collaborator

Ревью — #470 feat(mcp): getPageContext — «где я / что вокруг» по pageId (#443, часть 3/3, closes #443)

Вердикт: PASS

Полный веер из 9 аспектов по изолированной дельте (база — ветка #443-get-tree/#469; ревьюил bfb4c8d8..5dc7a270 = 7 файлов, +502/−1, mcp-only). Финал триады #443. Гейт зелёный.

Проверено — breadcrumbs-порядок/no-leak/edge-cases (клик)

getPageContext(pageId){page, breadcrumbs, children} одним вызовом. resolvePageId (UUID short-circuit, slugId +1 кэш) → POST /pages/breadcrumbs (цепочка root→page, page ПОСЛЕДНИЙ) + listSidebarPages (прямые дети).

  • Breadcrumbs-порядок сверен с серверным источником: сервер .reverse() → root→page; клиент self = chain[last], breadcrumbs = chain.slice(0,-1) (root→parent), [] для корня. Адверсариально: сломал slice(0,-1)slice(0) → 3 теста КРАСНЕЮТ (deep-page/root-empty/slugId), «empty chain throws» корректно остался зелёным. Порядок load-bearing.
  • pageId-only инвариант: page/breadcrumbs/children несут только pageId/title(/hasChildren), БЕЗ slugId/icon/position. Тест ассертит отсутствие «Slug» во всём дампе.
  • Edge-cases: пустая цепочка → чёткая ошибка (не hollow {}); корень → breadcrumbs:[]; без детей → children:[]; дети деду́плены + порядок сохранён через курсор-пагинацию (>20 детей, регресс против #442). Request-count: 2 (UUID) / 3 (slugId) — заассерчено моком.
  • Гейт: mcp node --test 725/725, server tsc 0. Аддитивно (новый тул), resolvePageId переиспользован без изменений, контракт на обоих хостах.
🔎 Вне scope — отдельная задача (НЕ часть вердикта #470) → issue #471

[security, server, ПРЕД-СУЩЕСТВУЮЩАЯ] getPageBreadCrumbs (page.service.ts:1072) — рекурсивный CTE поднимается по предкам, фильтруя ТОЛЬКО deletedAt, БЕЗ page-permission-фильтра; контроллер валидирует только целевую страницу. В гранулярной модели → утечка title ограниченного предка.

  • НЕ баг #470: #470 серверных файлов не трогает (mcp-only); тот же /pages/breadcrumbs УЖЕ зовёт веб-UI (space-tree.tsx:128) с той же авторизацией — breadcrumb-бар уже показывает эти title юзеру. getPageContext зовёт тот же эндпоинт в той же CASL-области — не вводит и не усугубляет (агент видит только title, открыть предка не может — permission на getPage/getNode сохранён).
  • Завёл отдельную серверную задачу #471 (фильтровать цепочку через filterAccessiblePageIds*, как sidebar/tree). Понизил security-агентский «CRITICAL/DO NOT MERGE» → out-of-scope: пред-существующее, web-UI-эквивалент, не блокирует #470.
DROP — кодеру НЕ делать
  • [below-threshold] low [documentation] нет CHANGELOG-энтри (паттерн общий с #468/#469; аддитивная фича, задокументирована в tool-desc + ROUTING_PROSE + doc-комменте). Рекомендую скоординированную CHANGELOG-запись на всю триаду #443.

DO/ESCALATE нет. breadcrumbs-порядок сверен с сервером + адверсариально пришпилен, pageId-only-инвариант enforced+tested, edge/error/dedup/request-count покрыты, аддитивно. Единственная security-находка — ПРЕД-СУЩЕСТВУЮЩАЯ серверная (issue #471), не #470. Гейт зелёный. Approve. Триада #443 (search/getTree/getPageContext) завершена.

Часть 3/3 #443, на #469. 9 аспектов. breadcrumbs-slice адверсариально краснит 3 теста; no-slugId/edge/dedup/request-count пришпилены. Ancestor-permission-leak — пред-существующая серверная (не трогает server), web-UI-эквивалент → out-of-scope, issue #471. Merge-order: после #469. Escalate нет.

<!-- state:review reviewed_head=5dc7a270 round=1 verdict=approved --> ## Ревью — #470 `feat(mcp): getPageContext — «где я / что вокруг» по pageId (#443, часть 3/3, closes #443)` **Вердикт: PASS** ✅ Полный веер из 9 аспектов по изолированной дельте (база — ветка #443-get-tree/#469; ревьюил `bfb4c8d8..5dc7a270` = 7 файлов, +502/−1, mcp-only). Финал триады #443. Гейт зелёный. <details> <summary>Проверено — breadcrumbs-порядок/no-leak/edge-cases (клик)</summary> `getPageContext(pageId)` → `{page, breadcrumbs, children}` одним вызовом. `resolvePageId` (UUID short-circuit, slugId +1 кэш) → `POST /pages/breadcrumbs` (цепочка root→page, page ПОСЛЕДНИЙ) + `listSidebarPages` (прямые дети). - **Breadcrumbs-порядок сверен с серверным источником**: сервер `.reverse()` → root→page; клиент `self = chain[last]`, `breadcrumbs = chain.slice(0,-1)` (root→parent), `[]` для корня. **Адверсариально**: сломал `slice(0,-1)`→`slice(0)` → 3 теста КРАСНЕЮТ (deep-page/root-empty/slugId), «empty chain throws» корректно остался зелёным. Порядок load-bearing. - **pageId-only инвариант**: page/breadcrumbs/children несут только `pageId`/`title`(/`hasChildren`), БЕЗ slugId/icon/position. Тест ассертит отсутствие «Slug» во всём дампе. - **Edge-cases**: пустая цепочка → чёткая ошибка (не hollow `{}`); корень → `breadcrumbs:[]`; без детей → `children:[]`; дети деду́плены + порядок сохранён через курсор-пагинацию (>20 детей, регресс против #442). Request-count: 2 (UUID) / 3 (slugId) — заассерчено моком. - **Гейт**: mcp node --test **725/725**, **server tsc 0**. Аддитивно (новый тул), resolvePageId переиспользован без изменений, контракт на обоих хостах. </details> <details> <summary>🔎 Вне scope — отдельная задача (НЕ часть вердикта #470) → issue #471</summary> **[security, server, ПРЕД-СУЩЕСТВУЮЩАЯ]** `getPageBreadCrumbs` (`page.service.ts:1072`) — рекурсивный CTE поднимается по предкам, фильтруя ТОЛЬКО `deletedAt`, БЕЗ page-permission-фильтра; контроллер валидирует только целевую страницу. В гранулярной модели → **утечка title ограниченного предка**. - **НЕ баг #470**: #470 серверных файлов не трогает (mcp-only); тот же `/pages/breadcrumbs` УЖЕ зовёт веб-UI (`space-tree.tsx:128`) с той же авторизацией — breadcrumb-бар уже показывает эти title юзеру. getPageContext зовёт тот же эндпоинт в той же CASL-области — не вводит и не усугубляет (агент видит только title, открыть предка не может — permission на getPage/getNode сохранён). - Завёл отдельную серверную задачу **#471** (фильтровать цепочку через `filterAccessiblePageIds*`, как sidebar/tree). Понизил security-агентский «CRITICAL/DO NOT MERGE» → out-of-scope: пред-существующее, web-UI-эквивалент, не блокирует #470. </details> <details> <summary>⛔ DROP — кодеру НЕ делать</summary> - `[below-threshold]` `low` **[documentation]** нет CHANGELOG-энтри (паттерн общий с #468/#469; аддитивная фича, задокументирована в tool-desc + ROUTING_PROSE + doc-комменте). Рекомендую скоординированную CHANGELOG-запись на всю триаду #443. </details> DO/ESCALATE нет. breadcrumbs-порядок сверен с сервером + адверсариально пришпилен, pageId-only-инвариант enforced+tested, edge/error/dedup/request-count покрыты, аддитивно. Единственная security-находка — ПРЕД-СУЩЕСТВУЮЩАЯ серверная (issue #471), не #470. Гейт зелёный. Approve. Триада #443 (search/getTree/getPageContext) завершена. <sub>Часть 3/3 #443, на #469. 9 аспектов. breadcrumbs-slice адверсариально краснит 3 теста; no-slugId/edge/dedup/request-count пришпилены. Ancestor-permission-leak — пред-существующая серверная (не трогает server), web-UI-эквивалент → out-of-scope, issue #471. Merge-order: после #469. Escalate нет.</sub>
agent_reviewer added review/approved and removed review/needs labels 2026-07-10 22:34:53 +03:00
vvzvlad merged commit 443cc0e88b into feat/443-get-tree 2026-07-10 23:45:40 +03:00
Sign in to join this conversation.