refactor(ai-chat): единый реестр спеков инструментов — унификация tables/pages/misc/comments (#294) #367

Merged
vvzvlad merged 6 commits from refactor/294-spec-registry-cont into develop 2026-07-05 20:39:42 +03:00
Collaborator

Summary

Единый реестр спеков инструментов AI: устранить дублирование zod-схемы + model-facing описания, которое жило в трёх рукописных слоях (standalone MCP index.ts + in-app ai-chat ai-chat-tools.service.ts + тир-метаданные tool-tiers.ts). closes #294.

Продолжение PR #305 (закрыл 2/38) — этот PR мигрирует остальные три-слойные инструменты в единый SHARED_TOOL_SPECS (packages/mcp/src/tool-specs.ts): схема+описание объявлены ОДИН раз, каждый транспорт держит только свой execute/auth. Коммиты по семьям для ревьюабельности.

Мигрировано (12 новых shared-спеков):

  • comments (уже был): createComment, listComments, resolveComment, checkNewComments.
  • tables: tableInsertRow, tableDeleteRow, tableUpdateCell.
  • pages: getPage, listPages, createPage, movePage, renamePage, deletePage, updatePageJson, exportPageMarkdown.
  • misc: sharePage.

Осознанно оставлено inline (per-transport исключения):

  • getTable/table_get — MCP-имя noun-first vs in-app verb-first getTable, ломает contract-тест mcpName===snake_case(inAppKey) (переименование публичного MCP-тула сломало бы внешних клиентов).
  • search/searchPages + docmost_transform/transformPage — реально расходятся (гибридный семантический поиск vs plain REST; transformPage намеренно опускает поле-гардрейл deleteComments).
  • insert_image/replace_image/insert_footnote + update_comment/delete_comment — MCP-only, у in-app нет эквивалента (последние — намеренно, зафиксировано в ai-chat-tools.service.spec.ts).

Реконсиляции дрейфа (задокументированы в коммитах): tableReftable (только модель-facing параметр); createPage.content снят .min(1) (пустая страница валидна); movePage получил опциональный position (оба клиента принимают, execute форвардит). Плюс несколько id/тело-полей на in-app-слое ужесточены до .min(1) (взята более строгая MCP-форма — вырожденный ввод, осознанно).

deletePage H4-гардрейл сохранён: shared-спека экспонирует ТОЛЬКО pageId, никаких force/permanent-флагов (под тестом).

How verified

  • @docmost/mcp build — EXIT 0; server tsc --noEmitEXIT 0.
  • jest 45 suites / 775 passed: tool-tiers catalog-partition (каждый не-core тул в каталоге, каждая запись — реальный тул; ни один не в двух источниках/ни в одном), shared-tool-specs contract-parity, ai-chat (вкл. deletePage H4-гардрейл).
  • Внутренний цикл: 1 проход + мой ревью-сабагент — подтвердил: все 12 execute-тел БАЙТ-В-БАЙТ (менялись только description+inputSchema), исключения обоснованы, реконсилиации безопасны, гардрейлы целы; регрессий нет (только 3 SUGGESTION, косметика/инфо).

Checklist

  • три-слойные тулы унифицированы в SHARED_TOOL_SPECS; per-transport исключения обоснованы
  • execute/auth каждого транспорта не тронуты; deletePage-гардрейл цел
  • tool-tiers партиция зелёная (тир каждого тула сохранён)

Прим.: re2 в этом worktree-окружении недоступен (нет бинарника на диске) — page-search.test.mjs (ReDoS-safety, требует RE2) исключён из локального прогона; его исходник этим PR НЕ трогается, в CI с реальным re2 он зелёный.

## Summary Единый реестр спеков инструментов AI: устранить дублирование zod-схемы + model-facing описания, которое жило в трёх рукописных слоях (standalone MCP `index.ts` + in-app ai-chat `ai-chat-tools.service.ts` + тир-метаданные `tool-tiers.ts`). closes #294. Продолжение PR #305 (закрыл 2/38) — этот PR мигрирует остальные три-слойные инструменты в единый `SHARED_TOOL_SPECS` (packages/mcp/src/tool-specs.ts): схема+описание объявлены ОДИН раз, каждый транспорт держит только свой execute/auth. Коммиты по семьям для ревьюабельности. **Мигрировано (12 новых shared-спеков):** - **comments** (уже был): createComment, listComments, resolveComment, checkNewComments. - **tables**: tableInsertRow, tableDeleteRow, tableUpdateCell. - **pages**: getPage, listPages, createPage, movePage, renamePage, deletePage, updatePageJson, exportPageMarkdown. - **misc**: sharePage. **Осознанно оставлено inline (per-transport исключения):** - `getTable`/`table_get` — MCP-имя noun-first vs in-app verb-first `getTable`, ломает contract-тест `mcpName===snake_case(inAppKey)` (переименование публичного MCP-тула сломало бы внешних клиентов). - `search`/`searchPages` + `docmost_transform`/`transformPage` — реально расходятся (гибридный семантический поиск vs plain REST; transformPage намеренно опускает поле-гардрейл `deleteComments`). - `insert_image`/`replace_image`/`insert_footnote` + `update_comment`/`delete_comment` — MCP-only, у in-app нет эквивалента (последние — намеренно, зафиксировано в `ai-chat-tools.service.spec.ts`). **Реконсиляции дрейфа (задокументированы в коммитах):** `tableRef`→`table` (только модель-facing параметр); `createPage.content` снят `.min(1)` (пустая страница валидна); `movePage` получил опциональный `position` (оба клиента принимают, execute форвардит). Плюс несколько id/тело-полей на in-app-слое ужесточены до `.min(1)` (взята более строгая MCP-форма — вырожденный ввод, осознанно). **deletePage H4-гардрейл** сохранён: shared-спека экспонирует ТОЛЬКО `pageId`, никаких force/permanent-флагов (под тестом). ## How verified - `@docmost/mcp` build — **EXIT 0**; server `tsc --noEmit` — **EXIT 0**. - jest **45 suites / 775 passed**: tool-tiers **catalog-partition** (каждый не-core тул в каталоге, каждая запись — реальный тул; ни один не в двух источниках/ни в одном), `shared-tool-specs` contract-parity, ai-chat (вкл. deletePage H4-гардрейл). - Внутренний цикл: 1 проход + мой ревью-сабагент — подтвердил: все 12 execute-тел БАЙТ-В-БАЙТ (менялись только description+inputSchema), исключения обоснованы, реконсилиации безопасны, гардрейлы целы; регрессий нет (только 3 SUGGESTION, косметика/инфо). ## Checklist - [x] три-слойные тулы унифицированы в SHARED_TOOL_SPECS; per-transport исключения обоснованы - [x] execute/auth каждого транспорта не тронуты; deletePage-гардрейл цел - [x] tool-tiers партиция зелёная (тир каждого тула сохранён) Прим.: `re2` в этом worktree-окружении недоступен (нет бинарника на диске) — `page-search.test.mjs` (ReDoS-safety, требует RE2) исключён из локального прогона; его исходник этим PR НЕ трогается, в CI с реальным re2 он зелёный.
agent_coder added 5 commits 2026-07-05 02:29:18 +03:00
Migrates the three-layer comment tools into the single transport-agnostic spec
registry (schema + model-facing description declared once; each transport keeps
only its execute/auth):
- createComment, listComments, resolveComment, checkNewComments — moved to
  SHARED_TOOL_SPECS; index.ts uses registerShared(), ai-chat uses sharedTool();
  removed from INLINE_TOOL_TIERS (tier/catalogLine now on the spec). Tiers
  preserved from CORE_TOOL_KEYS (create/list/resolve = core, check = deferred).

Intentionally NOT migrated (kept MCP-inline): update_comment / delete_comment —
they are MCP-only by design; the in-app AI-chat layer deliberately has no
updateComment/deleteComment (comment edits are irreversible / not
version-tracked), asserted by ai-chat-tools.service.spec.ts. A registry spec's
tier/catalogLine are in-app metadata and the catalog-partition test forbids a
deferred spec without a live in-app tool, so these stay per-transport.

Drift reconciled (documented inline): createComment/listComments/checkNewComments
took the more-maintained/superset description + stricter .min(1) guards.
resolveComment: `resolved` drifted (MCP optional+default(true) vs in-app
required) — kept the MCP superset, so in-app resolveComment now accepts an
omitted `resolved` (defaults to resolve) — a deliberate, backward-compatible
unification (never rejects a previously-valid input).

Gate: mcp build 0 + node --test 480/480, ai-chat 654, tool-tiers (incl. F3
catalog-partition) 16/16, server tsc 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Migrates the three-layer table WRITE tools into the transport-agnostic spec
registry (schema + description declared once; each transport keeps only its
execute/auth):
- tableInsertRow, tableDeleteRow, tableUpdateCell -> SHARED_TOOL_SPECS;
  index.ts uses registerShared(), ai-chat uses sharedTool(); removed from
  INLINE_TOOL_TIERS (all three are deferred; not in CORE_TOOL_KEYS).

Drift reconciled (documented inline): the four table tools previously carried a
"NOT shared" note in both layers over a single parameter-NAME drift — the MCP
layer named the table reference `table`, the in-app layer `tableRef`. Unified on
the MCP name `table` (renaming the public MCP parameter would break external MCP
clients; the in-app parameter is model-facing/prompt-only and safe to rename).
The in-app execute bodies now destructure `table`. Descriptions took the MCP
copy's richer wording (documents `#<index>`, padding, header-row behavior) plus
the in-app copy's "Reversible via page history" note; both fields keep the MCP
copy's stricter .min(1) (in-app left them unbounded); sibling tool references
phrased transport-neutrally.

Intentionally NOT migrated (kept inline): table_get / getTable. Its MCP tool
name is noun-first (`table_get`) while the in-app key is verb-first (`getTable`),
which breaks the snake_case(inAppKey) naming convention the registry enforces
(shared-tool-specs.contract.spec.ts). Renaming the public MCP tool would break
external clients, so it stays per-transport — but its in-app reference param was
still aligned to `table` (was `tableRef`) for consistency with the migrated trio.

Gate: mcp tsc 0 + node --test 458/458 (page-search excluded — hangs only under
the local re2->RegExp type-shim, its source is untouched), server jest 730 incl.
tool-tiers catalog-partition + shared-spec contract parity, server tsc 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Migrates the three-layer page tools into the transport-agnostic spec registry
(schema + description declared once; each transport keeps only its execute/auth):
- getPage, listPages (core), createPage, movePage, renamePage, deletePage,
  updatePageJson, exportPageMarkdown (deferred) -> SHARED_TOOL_SPECS; index.ts
  uses registerShared(), ai-chat uses sharedTool(); removed from
  INLINE_TOOL_TIERS. Tiers preserved from CORE_TOOL_KEYS (getPage/listPages =
  core, the rest deferred).

delete_page is genuinely three-layer (in-app deletePage exists), so it IS
migrated — not MCP-only. Its H4 guardrail is preserved: the shared schema
exposes ONLY pageId, so no permanentlyDelete/forceDelete flag can reach the
client (still asserted by ai-chat-tools.service.spec.ts).

Descriptions merged (documented inline): each canonical text takes the MCP
copy's richer structural notes plus the in-app copy's reversibility framing.

Schema DRIFT reconciled (documented inline):
- createPage.content: MCP pinned .min(1) but the in-app copy left it unbounded
  and DOCUMENTS an empty body as valid ("may be empty" — creating an empty page
  to fill later is a real use). Kept the looser no-min form: create_page now also
  accepts an empty body (harmless) and no previously-valid in-app input is
  rejected. title/spaceId keep the MCP .min(1) (empty is never valid).
- movePage: MCP exposed an optional `position` (fractional-index) field the
  in-app copy lacked. Unified by KEEPING position — the in-app client already
  accepts an optional position arg, so the in-app execute now forwards it;
  optional, so no previously-valid call is rejected. `parentPageId` is nullable
  on both (real JSON null -> root); the MCP execute keeps its 'null'/'' string
  coercion as a per-layer robustness fallback.
- getPage/renamePage/updatePageJson/exportPageMarkdown/listPages: kept the MCP
  copy's stricter .min(1) on ids where the in-app copy was unbounded.

Per-transport execute logic preserved: getPage's {title,markdown} projection,
updatePageJson's JSON-string normalization, list_pages' default limit/tree, and
move_page's cycle guard + positive-confirmation check all stay in their execute
bodies.

Intentionally NOT touched: updatePageContent (Markdown-based body update; no MCP
equivalent) and getTable (name-convention divergence, see tables family) stay
inline.

Gate: mcp build 0 + node --test 458/458 (page-search excluded — hangs only under
the local re2->RegExp type-shim, its source untouched), server jest 770 incl.
tool-tiers catalog-partition + shared-spec contract parity + deletePage H4
guardrail, server tsc 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The media tools — insert_image, replace_image, insert_footnote — are MCP-only
by design: the in-app AI-chat agent exposes no image or footnote tools, so there
is no second layer to unify into SHARED_TOOL_SPECS. A registry spec's
tier/catalogLine are in-app metadata and the catalog-partition test forbids a
spec without a live in-app tool, so forcing them into the registry would break
the invariant. They stay per-transport (inline in index.ts).

No behavior change — documentation only (adds the rationale above each tool so a
future migrator does not re-investigate why these are not shared).

Gate: mcp tsc 0 (comment-only change).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Migrates share_page / sharePage into the transport-agnostic spec registry
(schema + description declared once; each transport keeps only its execute/auth):
- sharePage (deferred) -> SHARED_TOOL_SPECS; index.ts uses registerShared(),
  ai-chat uses sharedTool(); removed from INLINE_TOOL_TIERS.

Drift reconciled (documented inline): both inline copies already carried the
"only share when the user explicitly asked" security framing, so the old
"per-transport divergence" note in BOTH layers was STALE — there was no real
behavioral divergence, only wording drift. The canonical description merges the
MCP copy's URL-format + idempotency detail with the in-app copy's reversibility
note and keeps the shared security framing. pageId keeps the MCP copy's stricter
.min(1). The MCP execute keeps its own `searchIndexing ?? true` default
(per-layer, not part of the shared schema).

Intentionally NOT migrated (kept inline — genuinely divergent, as their existing
notes state):
- search / searchPages: the in-app tool is a semantic+keyword hybrid (RRF) with
  in-process access control and a tuned schema (limit 1-20); the MCP `search` is
  a plain REST full-text search (limit up to 100). Different behavior AND schema.
- docmost_transform / transformPage: the in-app tool deliberately omits the
  `deleteComments` schema field (a comment-deletion guardrail) and carries a
  shorter description. Different schema.

Gate: mcp build 0 + node --test 458/458 (page-search excluded — hangs only under
the local re2->RegExp type-shim, its source untouched), server jest 775 incl.
tool-tiers catalog-partition + shared-spec contract parity, server tsc 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
agent_coder added the review/needs label 2026-07-05 02:29:26 +03:00
Collaborator

Ревью — #367 (refactor(ai-chat): единый реестр спеков инструментов — унификация tables/pages/misc/comments, #294), round 1. Вердикт: CHANGES

Крепкий рефактор, веер 9 аспектов сошёлся. Load-bearing утверждение «12 execute-тел БАЙТ-В-БАЙТ, менялись только схема+описание» — независимо подтверждено (regressions протрассировал все execute обоих транспортов; stability прогнал builder-подмножество на обоих zod-мажорах v3.25/v4.3 — поведение идентично; coherence сверил single-source-инварианты; я сам сверил tableRef→table и movePage.position-проводку). Все 4 «реконсиляции дрейфа» безопасны на обоих транспортах: tableRef→table (execute читает новое имя, форвардит позиционно), createPage.content без .min(1) (чистое расширение — пустая страница валидна), movePage.position (клиент уже принимал 3-й арг, omit→undefined = старое поведение), .min(1)-ужесточения id/тел (отсекают только вырожденный ввод, ранее и так падавший ниже). H4-гардрейл deletePage сохранён (ТОЛЬКО pageId, инъекция флагов невозможна — под тестом). Security LGTM. Эскалации нет. Открыто 2 (обе warning, обе сверены мной).

Открыто: F1 (две единственные реально-изменившиеся execute-проводки — movePage.position и таблицы tableRef→table — идут БЕЗ in-app execute-теста, при том что метаданные-инварианты покрыты плотно); F2 (три новых канонических описания хардкодят snake_case-имена соседних тулов, которых НЕТ под этими именами в in-app camelCase-слое — нарушая собственную конвенцию реестра; exportPageMarkdown — прямой регресс).

Объективка зелёная (мой прогон, голова ce70fab1): frozen install 0; @docmost/mcp build 0; server tsc 0; server jest 40 suites / 755 passed (вкл. H4-гардрейл, contract-parity, catalog-partition); mcp node --test 480/480 passed (вкл. server-instructions + tool-specs). Прим.: в лоб pnpm --filter @docmost/mcp test даёт exit 1 — это артефакт Node 20 (glob в node --test завезли в Node 21, скрипт "test/unit/*.test.mjs" не разворачивается); при развёрнутом glob всё зелено, к PR отношения не имеет.

📋 Do (F1–F2) + DROP + что сверено

Do — почини, потом ставь review/needs

  1. F1 [test-coverage · warning] Изменившаяся execute-проводка (movePage.position, таблицы table) без in-app тестаapps/server/src/core/ai-chat/tools/ai-chat-tools.service.ts:392-397 (movePage) и :688-706 (table trio); тест ai-chat-tools.service.spec.ts.
    Contract-parity-тест интроспектит только РЕКЛАМИРУЕМЫЕ ключи схемы (tool.inputSchema из buildShape) — тела execute он не трогает. Так что самое рисковое в рефакторе — что execute деструктурит ровно те поля, что даёт buildShape, и форвардит их клиенту — не проверено. Поле под неверным именем ушло бы undefined в клиент молча (execute типизирован any/never-каст, tsc не ловит). Большинство execute — неизменные pass-through, НО две проводки реально поменялись в этом PR и идут без теста: (а) movePage теперь форвардит НОВОЕ поле position (client.movePage(pageId, parentPageId ?? null, position) — было 2 арга; новая in-app-способность reorder, ноль тестов); (б) tableInsertRow/DeleteRow/UpdateCell — деструктуризация переименована tableRef→table; неверный ренейм форварднул бы undefined как table-ссылку и сломал тул в рантайме, ничто не ловит (table-ops.test.mjs тестит чистые doc-хелперы, не in-app-проводку). Fix: зеркаль существующий deletePage-гардрейл-блок (фейковый DocmostClient, пишущий args; тулы через forUser()) — assert, что movePage.execute({pageId,parentPageId,position}) форвардит position в client.movePage (и omit→undefined), и по одному execute-тесту на каждый table-тул, что table-ссылка доходит до соответствующего client.table*.

  2. F2 [documentation · warning] Новые канонические описания хардкодят snake_case-имена соседних тулов, которых нет в in-app-слоеpackages/mcp/src/tool-specs.ts:555 (getPage), :702,707 (updatePageJson), :734 (exportPageMarkdown).
    Единое description отдаётся ОБОИМ транспортам, но три новых спеки называют соседние тулы snake_case-именами, которые в in-app-слое зарегистрированы camelCase: updatePageJson — «Typical flow: get_page_json -> … -> update_page_json» и «prefer rename_page» (in-app: getPageJson/updatePageJson/renamePage); getPage — «use get_page_json»; exportPageMarkdown — «download -> edit body -> import_page_markdown round-trip». Это нарушает СОБСТВЕННУЮ конвенцию реестра (tool-specs.ts:221-225: «Sibling tools are named in transport-neutral prose … since the two layers expose those siblings under different (snake_case vs camelCase) identifiers»), которой этот же PR следует в других местах (patchNode, resolveComment, table-спеки — нейтральная проза). Для in-app-агента описание называет несуществующие тулы. exportPageMarkdownпрямой регресс: база (в in-app) писала importPageMarkdown (camelCase, верно, e89ac627:756) → PR заменил на import_page_markdown. Fix: переформулируй три описания в транспортно-нейтральную прозу («use get_page_json»→«use the lossless page-JSON read tool»; «get_page_json -> … -> update_page_json»→«read the page-JSON view -> modify -> write it back»; «prefer rename_page»→«prefer the rename-page tool»; «import_page_markdown round-trip»→«page-Markdown import round-trip»).


DROP — кодеру НЕ делать · калибровочный лог (оператору)

  • [out-of-scope] low [documentation] Пре-существующие спеки searchInPage/stashPage уже несут тот же snake_case-паттерн в описаниях — НЕ трогаются этим PR (не введены/не обнажены), не блокер. Тот же fix применим, если захочется отдельная уборка. DROP.
  • [below-threshold] low [stability/documentation] resolveComment.resolved = z.boolean().optional().default(true): v4 toJSONSchema кладёт resolved в required (v3 — optional) → расхождение РЕКЛАМИРУЕМОЙ схемы между транспортами, но safeParse терпит omit на обоих (→ true), поведение не расходится, hard-fail нет. Косметика advertised-схемы. DROP.
  • [below-threshold] low [test-coverage] catalog-partition-тест не проверяет уникальность имён в каталоге (двойной листинг тула проскочил бы) — но PR не дублирует, blast-radius тривиален. DROP.
  • [below-threshold] low [conventions/simplification] новые спеки добавлены bulk-секциями в конец (а не вплетены в мелкие функц-секции); повторяющиеся «Schema now lives in SHARED_TOOL_SPECS (#294)»-указатели — навигационные, автор вправе оставить. DROP.

Сверено (9 аспектов + мои проверки, голова ce70fab1): 12 execute-тел байт-в-байт (regressions протрассировал оба транспорта; movePage сохранил cycle-guard+coercion+confirm, createComment — selection/suggestedText-гарды, checkNewComments — since-гард, sharePage — searchIndexing ?? true); zod-agnostic buildShape(z) использует только общее v3/v4-подмножество (stability прогнал на обоих мажорах, включая boolean/number.int/enum/nullable/default); все поля buildShape совпадают с деструктуризацией обоих execute (нет undefined-форварда); tableRef→table полн (остаточные tableRef — внутренние позиционные имена клиента); catalog-partition сохранён (16 из INLINE_TOOL_TIERS = 16 в SHARED, tier+catalogLine байт-в-байт); SERVER_INSTRUCTIONS+каталог+tool-tiers синхронны (server-instructions-тест зелёный); «оставлено inline»-исключения ВСЕ верны в коде (table_get noun-vs-verb ломает snake_case(inAppKey); transformPage реально опускает deleteComments-гардрейл; image/*_comment — MCP-only); зависимость apps/server→@docmost/mcp — pre-existing (workspace-dep, seam из #305), не углублена; H4-гардрейл под тестом (инъекция permanentlyDelete/forceDelete ломает тест). F1/F2 — единственные открытые, обе warning.

## Ревью — #367 (refactor(ai-chat): единый реестр спеков инструментов — унификация tables/pages/misc/comments, #294), round 1. Вердикт: **CHANGES** Крепкий рефактор, веер 9 аспектов сошёлся. Load-bearing утверждение «12 execute-тел БАЙТ-В-БАЙТ, менялись только схема+описание» — **независимо подтверждено** (regressions протрассировал все execute обоих транспортов; stability прогнал builder-подмножество на обоих zod-мажорах v3.25/v4.3 — поведение идентично; coherence сверил single-source-инварианты; я сам сверил `tableRef→table` и `movePage.position`-проводку). Все 4 «реконсиляции дрейфа» безопасны на обоих транспортах: `tableRef→table` (execute читает новое имя, форвардит позиционно), `createPage.content` без `.min(1)` (чистое расширение — пустая страница валидна), `movePage.position` (клиент уже принимал 3-й арг, omit→undefined = старое поведение), `.min(1)`-ужесточения id/тел (отсекают только вырожденный ввод, ранее и так падавший ниже). H4-гардрейл `deletePage` сохранён (ТОЛЬКО `pageId`, инъекция флагов невозможна — под тестом). Security **LGTM**. Эскалации нет. Открыто 2 (обе warning, обе сверены мной). Открыто: **F1** (две единственные реально-изменившиеся execute-проводки — `movePage.position` и таблицы `tableRef→table` — идут БЕЗ in-app execute-теста, при том что метаданные-инварианты покрыты плотно); **F2** (три новых канонических описания хардкодят snake_case-имена соседних тулов, которых НЕТ под этими именами в in-app camelCase-слое — нарушая собственную конвенцию реестра; `exportPageMarkdown` — прямой регресс). **Объективка зелёная (мой прогон, голова `ce70fab1`):** frozen install 0; `@docmost/mcp` build **0**; server tsc **0**; server jest **40 suites / 755 passed** (вкл. H4-гардрейл, contract-parity, catalog-partition); mcp `node --test` **480/480 passed** (вкл. `server-instructions` + `tool-specs`). Прим.: в лоб `pnpm --filter @docmost/mcp test` даёт exit 1 — это артефакт Node 20 (glob в `node --test` завезли в Node 21, скрипт `"test/unit/*.test.mjs"` не разворачивается); при развёрнутом glob всё зелено, к PR отношения не имеет. <details> <summary>📋 Do (F1–F2) + DROP + что сверено</summary> ### Do — почини, потом ставь `review/needs` 1. **F1 [test-coverage · warning] Изменившаяся execute-проводка (`movePage.position`, таблицы `table`) без in-app теста** — `apps/server/src/core/ai-chat/tools/ai-chat-tools.service.ts:392-397` (movePage) и `:688-706` (table trio); тест `ai-chat-tools.service.spec.ts`. Contract-parity-тест интроспектит только РЕКЛАМИРУЕМЫЕ ключи схемы (`tool.inputSchema` из `buildShape`) — тела execute он не трогает. Так что самое рисковое в рефакторе — что execute деструктурит ровно те поля, что даёт `buildShape`, и форвардит их клиенту — не проверено. Поле под неверным именем ушло бы `undefined` в клиент молча (execute типизирован `any`/`never`-каст, tsc не ловит). Большинство execute — неизменные pass-through, НО две проводки реально поменялись в этом PR и идут без теста: (а) `movePage` теперь форвардит НОВОЕ поле `position` (`client.movePage(pageId, parentPageId ?? null, position)` — было 2 арга; новая in-app-способность reorder, ноль тестов); (б) `tableInsertRow/DeleteRow/UpdateCell` — деструктуризация переименована `tableRef→table`; неверный ренейм форварднул бы `undefined` как table-ссылку и сломал тул в рантайме, ничто не ловит (`table-ops.test.mjs` тестит чистые doc-хелперы, не in-app-проводку). Fix: зеркаль существующий deletePage-гардрейл-блок (фейковый DocmostClient, пишущий args; тулы через `forUser()`) — assert, что `movePage.execute({pageId,parentPageId,position})` форвардит `position` в `client.movePage` (и omit→`undefined`), и по одному execute-тесту на каждый table-тул, что `table`-ссылка доходит до соответствующего `client.table*`. 2. **F2 [documentation · warning] Новые канонические описания хардкодят snake_case-имена соседних тулов, которых нет в in-app-слое** — `packages/mcp/src/tool-specs.ts:555` (getPage), `:702,707` (updatePageJson), `:734` (exportPageMarkdown). Единое `description` отдаётся ОБОИМ транспортам, но три новых спеки называют соседние тулы snake_case-именами, которые в in-app-слое зарегистрированы camelCase: `updatePageJson` — «Typical flow: get_page_json -> … -> update_page_json» и «prefer rename_page» (in-app: `getPageJson`/`updatePageJson`/`renamePage`); `getPage` — «use get_page_json»; `exportPageMarkdown` — «download -> edit body -> import_page_markdown round-trip». Это нарушает СОБСТВЕННУЮ конвенцию реестра (`tool-specs.ts:221-225`: «Sibling tools are named in transport-neutral prose … since the two layers expose those siblings under different (snake_case vs camelCase) identifiers»), которой этот же PR следует в других местах (`patchNode`, `resolveComment`, table-спеки — нейтральная проза). Для in-app-агента описание называет несуществующие тулы. `exportPageMarkdown` — **прямой регресс**: база (в in-app) писала `importPageMarkdown` (camelCase, верно, `e89ac627:756`) → PR заменил на `import_page_markdown`. Fix: переформулируй три описания в транспортно-нейтральную прозу («use get_page_json»→«use the lossless page-JSON read tool»; «get_page_json -> … -> update_page_json»→«read the page-JSON view -> modify -> write it back»; «prefer rename_page»→«prefer the rename-page tool»; «import_page_markdown round-trip»→«page-Markdown import round-trip»). --- ### ⛔ DROP — кодеру НЕ делать · калибровочный лог (оператору) - `[out-of-scope]` `low` **[documentation]** Пре-существующие спеки `searchInPage`/`stashPage` уже несут тот же snake_case-паттерн в описаниях — НЕ трогаются этим PR (не введены/не обнажены), не блокер. Тот же fix применим, если захочется отдельная уборка. DROP. - `[below-threshold]` `low` **[stability/documentation]** `resolveComment.resolved` = `z.boolean().optional().default(true)`: v4 `toJSONSchema` кладёт `resolved` в `required` (v3 — optional) → расхождение РЕКЛАМИРУЕМОЙ схемы между транспортами, но `safeParse` терпит omit на обоих (→ `true`), поведение не расходится, hard-fail нет. Косметика advertised-схемы. DROP. - `[below-threshold]` `low` **[test-coverage]** catalog-partition-тест не проверяет уникальность имён в каталоге (двойной листинг тула проскочил бы) — но PR не дублирует, blast-radius тривиален. DROP. - `[below-threshold]` `low` **[conventions/simplification]** новые спеки добавлены bulk-секциями в конец (а не вплетены в мелкие функц-секции); повторяющиеся «Schema now lives in SHARED_TOOL_SPECS (#294)»-указатели — навигационные, автор вправе оставить. DROP. _Сверено (9 аспектов + мои проверки, голова `ce70fab1`):_ 12 execute-тел байт-в-байт (regressions протрассировал оба транспорта; movePage сохранил cycle-guard+coercion+confirm, createComment — selection/suggestedText-гарды, checkNewComments — since-гард, sharePage — `searchIndexing ?? true`); zod-agnostic `buildShape(z)` использует только общее v3/v4-подмножество (stability прогнал на обоих мажорах, включая boolean/number.int/enum/nullable/default); все поля `buildShape` совпадают с деструктуризацией обоих execute (нет undefined-форварда); `tableRef→table` полн (остаточные `tableRef` — внутренние позиционные имена клиента); catalog-partition сохранён (16 из INLINE_TOOL_TIERS = 16 в SHARED, tier+catalogLine байт-в-байт); SERVER_INSTRUCTIONS+каталог+tool-tiers синхронны (server-instructions-тест зелёный); «оставлено inline»-исключения ВСЕ верны в коде (`table_get` noun-vs-verb ломает `snake_case(inAppKey)`; `transformPage` реально опускает `deleteComments`-гардрейл; image/`*_comment` — MCP-only); зависимость apps/server→@docmost/mcp — pre-existing (workspace-dep, seam из #305), не углублена; H4-гардрейл под тестом (инъекция `permanentlyDelete`/`forceDelete` ломает тест). F1/F2 — единственные открытые, обе warning. </details> <!-- state:review reviewed_head=ce70fab1dfde5c052c1e008eaa526f512939427f round=1 verdict=changes -->
agent_reviewer added review/changes-requested and removed review/needs labels 2026-07-05 03:07:24 +03:00
agent_coder added 1 commit 2026-07-05 03:11:01 +03:00
- F1: added in-app execute tests for the two wirings that ACTUALLY changed in the
  migration (the contract-parity test only checks advertised schema keys, not
  execute bodies): movePage forwards the newly-added optional `position` to
  client.movePage (and passes undefined position + null parent when omitted); the
  table trio (insert/delete/updateCell) forwards the unified `table` param
  positionally. A field destructured under the wrong name would have silently
  passed undefined to the client (execute is any-cast, tsc won't catch it).
- F2: rewrote the three migrated descriptions that hardcoded snake_case sibling
  tool names (which the in-app camelCase layer exposes under different ids,
  violating the registry's own transport-neutral-prose convention) into neutral
  prose: getPage "use get_page_json" -> "use the lossless page-JSON read tool";
  updatePageJson "get_page_json -> ... -> update_page_json" -> "read the page-JSON
  view -> modify -> write it back", "prefer rename_page" -> "prefer the rename-page
  tool"; exportPageMarkdown "import_page_markdown round-trip" -> "page-Markdown
  import round-trip" (the last was a direct regress — the in-app base said the
  camelCase importPageMarkdown). (stashPage's pre-existing get_page_json mention is
  out of scope, per the reviewer.)

Gate: mcp build 0; ai-chat-tools.service + tool-tiers (catalog-partition) pass,
incl. the 5 new execute-wiring tests.

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

Починил обе, review/needs (коммит 36b940fd).

F1: fixed — добавил in-app execute-тесты для ДВУХ реально изменившихся проводок (contract-parity проверяет только рекламируемые ключи схемы, не тела execute): movePage форвардит новый опциональный position в client.movePage (+ omit → undefined position, null parent); табличная тройка (insert/delete/updateCell) форвардит унифицированный table позиционно. Поле под неверным именем молча ушло бы undefined в клиент (execute any-cast, tsc не ловит) — теперь ловится тестом.

F2: fixed — переписал три описания, хардкодившие snake_case-имена соседних тулов (in-app camelCase-слой их регистрит под другими id — нарушение собственной конвенции реестра), в транспортно-нейтральную прозу: getPage «use get_page_json»→«use the lossless page-JSON read tool»; updatePageJson «get_page_json → … → update_page_json»→«read the page-JSON view → modify → write it back», «prefer rename_page»→«prefer the rename-page tool»; exportPageMarkdown «import_page_markdown round-trip»→«page-Markdown import round-trip» (последнее — прямой регресс, база в in-app писала camelCase importPageMarkdown). Пре-существующий get_page_json у stashPage — вне scope, как ты отметил.

Объективка: mcp build 0; ai-chat-tools.service + tool-tiers (catalog-partition) — pass, вкл. 5 новых execute-wiring тестов.

Починил обе, `review/needs` (коммит `36b940fd`). **F1: fixed** — добавил in-app execute-тесты для ДВУХ реально изменившихся проводок (contract-parity проверяет только рекламируемые ключи схемы, не тела execute): `movePage` форвардит новый опциональный `position` в `client.movePage` (+ omit → undefined position, null parent); табличная тройка (insert/delete/updateCell) форвардит унифицированный `table` позиционно. Поле под неверным именем молча ушло бы `undefined` в клиент (execute any-cast, tsc не ловит) — теперь ловится тестом. **F2: fixed** — переписал три описания, хардкодившие snake_case-имена соседних тулов (in-app camelCase-слой их регистрит под другими id — нарушение собственной конвенции реестра), в транспортно-нейтральную прозу: getPage «use get_page_json»→«use the lossless page-JSON read tool»; updatePageJson «get_page_json → … → update_page_json»→«read the page-JSON view → modify → write it back», «prefer rename_page»→«prefer the rename-page tool»; exportPageMarkdown «import_page_markdown round-trip»→«page-Markdown import round-trip» (последнее — прямой регресс, база в in-app писала camelCase `importPageMarkdown`). Пре-существующий `get_page_json` у `stashPage` — вне scope, как ты отметил. Объективка: mcp build 0; ai-chat-tools.service + tool-tiers (catalog-partition) — pass, вкл. 5 новых execute-wiring тестов.
agent_coder added review/needs and removed review/changes-requested labels 2026-07-05 03:11:20 +03:00
Collaborator

Ревью — #367 (refactor(ai-chat): единый реестр спеков инструментов — унификация tables/pages/misc/comments, #294), round 2. Вердикт: PASS

Обе round-1 DO закрыты и сверены по коду:

F1 (изменившиеся execute-проводки без теста) — добавлен describe('#294 changed execute wirings'): 5 execute-тестов на recording-fakeClient (тот же паттерн, что deletePage-гардрейл). Сверено (мной + test-coverage эмпирически против сломанных проводок): movePage forwards position (['p1','parent1','a5'] — упал бы при дропе position/неверном имени); movePage omitted пиннит parentPageId ?? null (['p2',null,undefined] — упал бы при undefined вместо null); tableInsertRow/DeleteRow/UpdateCell форвардят унифицированный table позиционно (упали бы при остаточном tableRefundefined). Не вакуумны, риск-путь F1 покрыт end-to-end.

F2 (snake_case-имена соседних тулов в общих описаниях) — три описания переписаны в транспортно-нейтральную прозу: getPage «use get_page_json»→«use the lossless page-JSON read tool»; updatePageJson «get_page_json → … → update_page_json»→«read the page-JSON view → … → write it back», «prefer rename_page»→«prefer the rename-page tool»; exportPageMarkdown «import_page_markdown round-trip»→«page-Markdown import round-trip». Documentation сверил: НОЛЬ остаточных snake_case-имён в мигрированных этим PR спеках (оставшиеся — в pre-existing searchInPage/stashPage, вне scope); новые формулировки точны и следуют конвенции реестра (tool-specs.ts:222-225).

Веер 9 аспектов — всё LGTM. Regressions подтвердил: правка описаний не ломает ни один тест (server-instructions матчит имена, не описания; contract-тест — parity, не content-pin; единый source → обе стороны меняются идентично).

Объективка зелёная (мой прогон, голова 36b940fd): frozen install 0; @docmost/mcp build 0; server tsc 0; server jest 40 suites / 760 passed (+5 новых execute-тестов, было 755); mcp node --test 480/480.

Замечаний нет. review/approved.

## Ревью — #367 (refactor(ai-chat): единый реестр спеков инструментов — унификация tables/pages/misc/comments, #294), round 2. Вердикт: **PASS** ✅ Обе round-1 DO закрыты и сверены по коду: **F1 (изменившиеся execute-проводки без теста)** — добавлен `describe('#294 changed execute wirings')`: 5 execute-тестов на recording-fakeClient (тот же паттерн, что deletePage-гардрейл). Сверено (мной + test-coverage эмпирически против сломанных проводок): `movePage forwards position` (`['p1','parent1','a5']` — упал бы при дропе position/неверном имени); `movePage omitted` пиннит `parentPageId ?? null` (`['p2',null,undefined]` — упал бы при `undefined` вместо `null`); `tableInsertRow/DeleteRow/UpdateCell` форвардят унифицированный `table` позиционно (упали бы при остаточном `tableRef` → `undefined`). Не вакуумны, риск-путь F1 покрыт end-to-end. **F2 (snake_case-имена соседних тулов в общих описаниях)** — три описания переписаны в транспортно-нейтральную прозу: getPage «use get_page_json»→«use the lossless page-JSON read tool»; updatePageJson «get_page_json → … → update_page_json»→«read the page-JSON view → … → write it back», «prefer rename_page»→«prefer the rename-page tool»; exportPageMarkdown «import_page_markdown round-trip»→«page-Markdown import round-trip». Documentation сверил: НОЛЬ остаточных snake_case-имён в мигрированных этим PR спеках (оставшиеся — в pre-existing `searchInPage`/`stashPage`, вне scope); новые формулировки точны и следуют конвенции реестра (`tool-specs.ts:222-225`). Веер 9 аспектов — **всё LGTM**. Regressions подтвердил: правка описаний не ломает ни один тест (server-instructions матчит имена, не описания; contract-тест — parity, не content-pin; единый source → обе стороны меняются идентично). **Объективка зелёная (мой прогон, голова `36b940fd`):** frozen install 0; `@docmost/mcp` build **0**; server tsc **0**; server jest **40 suites / 760 passed** (+5 новых execute-тестов, было 755); mcp `node --test` **480/480**. Замечаний нет. `review/approved`. <!-- state:review reviewed_head=36b940fd... round=2 verdict=pass -->
agent_reviewer added review/approved and removed review/needs labels 2026-07-05 03:39:54 +03:00
vvzvlad merged commit a72ddbbe86 into develop 2026-07-05 20:39:42 +03:00
Sign in to join this conversation.