refactor(mcp): вывести DocmostClientLike/SharedToolSpec из реального типа — убить ручные зеркала (#446) #458

Merged
vvzvlad merged 1 commits from refactor/446-derive-client-types into develop 2026-07-10 16:01:50 +03:00
Collaborator

Summary

Восстановленный отложенный долг #294 — второй линк Фазы 1б роадмапа #416. closes #446

@docmost/mcp не отдавал .d.ts, поэтому в сервере жили ТРИ дрейфующие ручные копии одних и тех же имён/сигнатур: DocmostClientLike (~230-строчное зеркало 48 методов DocmostClient), копия интерфейса SharedToolSpec, и name-only HOST_CONTRACT_METHODS-тест. In-app execute-тела зовут клиент ПОЗИЦИОННО (client.drawioGet(pageId, node, format ?? 'xml')), так что перестановка/смена типа параметра в client.ts доезжала до прода рантайм-ошибкой без единого сигнала на компиляции.

  • declaration:true (+declarationMap) в packages/mcp/tsconfig.json; types-экспорт в package.json (exports → conditional {types, default} для . и ./http). build/index.d.ts эмитится, реэкспортит DocmostClient + SharedToolSpec. Правок исходников пакета для эмита НЕ потребовалось (сборка чистая).
  • DocmostClientLikePick<DocmostClient, 48 методов> из import type (стёрт на компиляции, ESM/CJS-границу рантайма не задевает — класс по-прежнему грузится динамическим import()); ручное зеркало удалено.
  • SharedToolSpec → type-only реэкспорт из пакета; ручная копия удалена.
  • client-host-contract.test.mjs удалён — имена И сигнатуры теперь проверяет tsc.
  • Позиционная безопасность: never-called __assertClientCallContract(client: DocmostClientLike) воспроизводит каждый позиционный вызов с типизированными плейсхолдерами (AI-SDK стирает вход execute-замыканий в any, иначе позиционные вызовы не проверялись). Перестановка параметров client.tsошибка компиляции сервера ровно тут. Loose as-касты в ai-chat-tools.service не потребовали правок; as any не добавлялся.

How verified

Стенд (с временным репойнтом symlink @docmost/mcp на пакет этой ветки — в обычном single-checkout после мержа relative-symlink резолвит ветку сам, репойнт не нужен; symlink восстановлен):

  • pnpm --filter @docmost/mcp build → чисто, build/index.d.ts эмитится.
  • node --test test/unit/* test/mock/*611 pass (минус удалённый host-contract).
  • tsc --noEmit -p apps/server → без новых ошибок (единственная — предсуществующая @fastify/compress).
  • jest shared-tool-specs.contract tool-tiers ai-chat-tools.service docmost-client.loader + comment-signal-inapp → зелёные.
  • grep "interface DocmostClientLike\|interface SharedToolSpec" apps/server/src → пусто.
  • Демонстрация: перестановка параметров метода client.tsTS2345 внутри __assertClientCallContract, затем revert.

Внутренний цикл: 1 проход (APPROVE). Runtime resolution через conditional exports подтверждён разбором карты для прод-инсталляции (не только symlink): require.resolve('@docmost/mcp')defaultbuild/index.js, затем dynamic import() резолвленного пути минует exports. Покрытие __assertClientCallContract полное (48 call-sites == union == assert, сверено программно). Единственная находка — Promise<any> в части возвращаемых типов — предсуществующая в client.ts, вне цели PR.

Порядок мержа

Второй линк Фазы 1б, стоит на #447 (PR #457, закрытие skew build/vs/src — общие файлы package.json/loader). Мержить ПОСЛЕ #447. Следующий линк #445 (execute-маппинг) встанет на #446. См. комментарий с порядком в #416.

Checklist

  • ноль ручных копий типов @docmost/mcp в apps/server
  • перестановка параметров client.ts → ошибка компиляции сервера, не рантайм-падение
  • client-host-contract.test.mjs удалён
## Summary Восстановленный отложенный долг #294 — второй линк Фазы 1б роадмапа #416. closes #446 `@docmost/mcp` не отдавал `.d.ts`, поэтому в сервере жили ТРИ дрейфующие ручные копии одних и тех же имён/сигнатур: `DocmostClientLike` (~230-строчное зеркало 48 методов `DocmostClient`), копия интерфейса `SharedToolSpec`, и name-only `HOST_CONTRACT_METHODS`-тест. In-app execute-тела зовут клиент ПОЗИЦИОННО (`client.drawioGet(pageId, node, format ?? 'xml')`), так что перестановка/смена типа параметра в `client.ts` доезжала до прода рантайм-ошибкой без единого сигнала на компиляции. - **declaration:true** (+`declarationMap`) в `packages/mcp/tsconfig.json`; `types`-экспорт в `package.json` (`exports` → conditional `{types, default}` для `.` и `./http`). `build/index.d.ts` эмитится, реэкспортит `DocmostClient` + `SharedToolSpec`. Правок исходников пакета для эмита НЕ потребовалось (сборка чистая). - **`DocmostClientLike`** → `Pick<DocmostClient, 48 методов>` из `import type` (стёрт на компиляции, ESM/CJS-границу рантайма не задевает — класс по-прежнему грузится динамическим `import()`); ручное зеркало удалено. - **`SharedToolSpec`** → type-only реэкспорт из пакета; ручная копия удалена. - **`client-host-contract.test.mjs` удалён** — имена И сигнатуры теперь проверяет tsc. - **Позиционная безопасность**: never-called `__assertClientCallContract(client: DocmostClientLike)` воспроизводит каждый позиционный вызов с типизированными плейсхолдерами (AI-SDK стирает вход execute-замыканий в `any`, иначе позиционные вызовы не проверялись). Перестановка параметров `client.ts` → **ошибка компиляции сервера** ровно тут. Loose `as`-касты в `ai-chat-tools.service` не потребовали правок; `as any` не добавлялся. ## How verified Стенд (с временным репойнтом symlink `@docmost/mcp` на пакет этой ветки — в обычном single-checkout после мержа relative-symlink резолвит ветку сам, репойнт не нужен; symlink восстановлен): - `pnpm --filter @docmost/mcp build` → чисто, `build/index.d.ts` эмитится. - `node --test test/unit/* test/mock/*` → **611 pass** (минус удалённый host-contract). - `tsc --noEmit -p apps/server` → без новых ошибок (единственная — предсуществующая `@fastify/compress`). - `jest shared-tool-specs.contract tool-tiers ai-chat-tools.service docmost-client.loader` + `comment-signal-inapp` → зелёные. - `grep "interface DocmostClientLike\|interface SharedToolSpec" apps/server/src` → пусто. - **Демонстрация**: перестановка параметров метода `client.ts` → `TS2345` внутри `__assertClientCallContract`, затем revert. Внутренний цикл: 1 проход (APPROVE). Runtime resolution через conditional exports подтверждён разбором карты для прод-инсталляции (не только symlink): `require.resolve('@docmost/mcp')` → `default` → `build/index.js`, затем dynamic `import()` резолвленного пути минует exports. Покрытие `__assertClientCallContract` полное (48 call-sites == union == assert, сверено программно). Единственная находка — `Promise<any>` в части возвращаемых типов — предсуществующая в `client.ts`, вне цели PR. ## Порядок мержа Второй линк Фазы 1б, **стоит на #447** (PR #457, закрытие skew build/vs/src — общие файлы `package.json`/loader). Мержить ПОСЛЕ #447. Следующий линк #445 (execute-маппинг) встанет на #446. См. комментарий с порядком в #416. ## Checklist - [x] ноль ручных копий типов `@docmost/mcp` в apps/server - [x] перестановка параметров client.ts → ошибка компиляции сервера, не рантайм-падение - [x] client-host-contract.test.mjs удалён
agent_coder added 2 commits 2026-07-10 08:30:26 +03:00
Два структурных слепых пятна: (1) сервер грузит СКОМПИЛИРОВАННЫЙ
packages/mcp/build/index.js, а серверные guard-тесты читают src/tool-specs.ts —
правка src без пересборки оставляет тесты зелёными, но рантайм расходится со
спеками; (2) спеки добавляют в packages/mcp, а parity-тесты живут в jest-сьюте
apps/server — PR, трогающий только пакет, проходит зелёным, сломанная in-app-
проводка всплывает уже на develop (кейс f46d89ea).

- REGISTRY_STAMP: codegen (scripts/gen-registry-stamp.mjs) считает sha256 от
  нормализованного (CRLF→LF, один хвостовой \n снят) сырого текста
  src/tool-specs.ts, пишет src/registry-stamp.generated.ts (gitignored),
  index.ts реэкспортит → попадает в build/. Вшит в build/pretest/watch ДО tsc.
- Loader (dev/test): computeSrcRegistryStamp пересчитывает стамп из src рядом с
  build/index.js (dev-vs-prod по existsSync, любая ошибка → null), сверяет с
  build-стампом → при рассинхроне бросает «build is stale — rebuild». В prod
  (src нет) и на pre-#447 билдах (нет REGISTRY_STAMP) — чистый no-op.
- CI: job mcp-server-parity собирает shared-deps+mcp (регенерит стамп) и гоняет
  ОБА сьюта вместе (mcp node:test + server guard-спеки) — именованный гейт, его
  нельзя случайно расщепить.
- AGENTS.md: правка спеков требует ребилда @docmost/mcp.

Тесты (20): mcp-сайд (детерминизм, нормализация, desync-гард стамп-vs-билд) +
server-сайд (null при отсутствии src = prod no-op; mismatch → throw точного
сообщения; pre-#447 no-op). Кросс-импл equality-гард: один фиксированный вход →
один хэш на ОБЕИХ сторонах, ловит рассинхрон двух нормализаций. Внутреннее
ревью: APPROVE WITH SUGGESTIONS (обе — покрытие guard'а — закрыты этим тестом).
Мутационно: любой из двух normalize-имплов расходится → equality-тест краснеет.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Восстановленный отложенный долг #294: @docmost/mcp не отдавал .d.ts, поэтому в
сервере жили ТРИ дрейфующие ручные копии одних и тех же имён/сигнатур
(DocmostClientLike ~230 строк, копия SharedToolSpec, name-only HOST_CONTRACT_
METHODS-тест). In-app execute-тела зовут клиент ПОЗИЦИОННО, так что перестановка
параметра в client.ts доезжала до прода рантайм-ошибкой без сигнала на компиляции.

- declaration:true (+declarationMap) в packages/mcp/tsconfig.json; types-экспорт
  в package.json (exports → conditional {types, default} для . и ./http;
  require.resolve/dynamic-import резолвят default → build/index.js, рантайм не
  тронут). build/index.d.ts эмитится, реэкспортит DocmostClient + SharedToolSpec.
  Правок исходников пакета для эмита НЕ потребовалось.
- DocmostClientLike → Pick<DocmostClient, 48 методов> из type-only import
  (стёрт на компиляции, ESM/CJS-границу не задевает); ручное зеркало удалено.
- SharedToolSpec → type-only реэкспорт из пакета; ручная копия удалена.
- client-host-contract.test.mjs удалён целиком — имена И сигнатуры теперь
  проверяет tsc.
- Позиционная безопасность: never-called __assertClientCallContract(client:
  DocmostClientLike) воспроизводит каждый позиционный вызов с типизированными
  плейсхолдерами (AI-SDK стирает вход execute-замыканий в any, иначе позиционные
  вызовы не проверялись). Перестановка параметров client.ts → ошибка компиляции
  сервера ровно тут. Loose as-касты в ai-chat-tools.service не потребовали
  правок; as any не добавлялся.

Внутреннее ревью: APPROVE. Runtime resolution через conditional exports не сломан
(разобрано для прод-инсталляции, не только symlink); покрытие
__assertClientCallContract полное (48 call-sites == union == assert, сверено
программно); Pick полон; демонстрация reorder → TS2345 в assert. Единственная
находка (Promise<any> в части возвратов) предсуществующая в client.ts, вне
цели PR. Стоит на #447 (закрытие skew build/vs/src) — мержить после него.

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

Ревью — #458 refactor(mcp): вывести DocmostClientLike/SharedToolSpec из реального типа — убить ручные зеркала (#446)

Вердикт: PASS

Полный веер из 9 аспектов по СОБСТВЕННОЙ дельте PR (застекан на уже-одобренный #457; ревьюил 2e6f1c3d..fe5b6ecd = 7 файлов, +217/−516 — без повторного ревью #457). Гейт зелёный в чистой комнате.

Механизм — проверено адверсариально (клик)

Проблема (#446, восстановленный долг #294): in-app execute-замыкания зовут DocmostClient позиционно (client.drawioGet(pageId, node, format ?? 'xml')) с AI-SDK-стёртым (any) входом, а сервер держал ТРИ дрейфующих ручных зеркала (~230-строчный DocmostClientLike, копию SharedToolSpec, name-only runtime-тест). Перестановка/смена типа параметра в client.ts доезжала до прода рантайм-ошибкой без сигнала на компиляции.

PR закрывает это деривацией из реального типа:

  • declaration:true+declarationMap (packages/mcp/tsconfig.json) + types/conditional-exports → эмитится build/index.d.ts.
  • Загрузчик: import type { DocmostClient, SharedToolSpec } (ПОЛНОСТЬЮ стёрт в compile — рантайм по-прежнему через dynamic import()), DocmostClientLike = Pick<DocmostClient, DocmostClientMethod>, ре-экспорт SharedToolSpec из пакета.
  • Never-called __assertClientCallContract(client) в ai-chat-tools.service.ts реплеит КАЖДЫЙ позиционный вызов типизированными плейсхолдерами → дрейф сигнатуры становится серверной ошибкой компиляции.
  • АДВЕРСАРИАЛЬНО подтвердил: добавил обязательный параметр в DocmostClient.getPage → пересобрал .d.tsserver tsc КРАСНЫЙ (TS2554: Expected 2 arguments, but got 1) и на реальном call-site, и в assert-функции; откатил → tsc снова 0. Гард реально ловит дрейф.
Объективный гейт (клин-рум /home/claude/gm-ci, Node 22 = CI)
  • mcp .d.ts эмитится зелёным (declaration:true; index.d.ts экспортит DocmostClient/SharedToolSpec/REGISTRY_STAMP) — без portability/private-name ошибок эмита деклараций.
  • server tsc = 0 (baseline): деривированные КОНКРЕТНЫЕ return-типы НЕ ломают as-касты (второй блокер старого staged-плана снят — касты идут concrete→надмножество-looser, валидно).
  • mcp node --test 611/611 (−3 = ровно удалённый host-contract тест, больше ничего не сломано).
  • server jest 52/52 (ai-chat-tools.service + comment-signal-inapp + docmost-client.loader, --runInBand).
  • Полнота: сверил все 44 вызова в __assertClientCallContract с реальными execute-телами — совпадают; union DocmostClientMethod — верное подмножество (корректно исключает login/ensureAuthenticated/uploadImage/updateComment/deleteComment).
DROP — кодеру НЕ делать (лог калибровки)
  • [by-design] low [architecture] return-handling as-касты остаются loose (Record<string,unknown>) — staged-план (c). Намеренно вне scope этого PR (цель = безопасность АРГУМЕНТОВ, не возвратов); tsc зелёный, касты валидны (concrete→subset). Не дефект.
  • [below-threshold] low [simplification] __assertClientCallContract — новый рукотворный replay-список, держать в lockstep с execute-телами. Но: одно-направленный (было дву-направленное бремя HOST_CONTRACT_METHODS↔интерфейс), tsc стережёт дрейф сигнатур, строго сильнее старого name-only. Задокументировано.
  • [below-threshold] low [conventions] declarationMap:true эмитит .d.ts.map в gitignored-build приватного пакета — безвредный шум (в рантайме инертен), можно опустить.

DO/ESCALATE нет. Дельта убивает 3 дрейфующих зеркала (net −299 строк), заменяя их деривацией из реального типа + compile-time гардом, который адверсариально доказан (дрейф сигнатуры → красный server tsc). Все аспекты LGTM, гейт зелёный. Approve.

9 аспектов (security+stability+regressions, coherence+testcov, conv+docs+simp+arch — все LGTM). Ядро проверено адверсариально мутацией сигнатуры client.ts → server tsc RED; .d.ts-эмит и baseline-tsc зелёные; assert-fn полнота сверена с execute-телами. Escalate нет.

<!-- state:review reviewed_head=fe5b6ecd round=1 verdict=approved --> ## Ревью — #458 `refactor(mcp): вывести DocmostClientLike/SharedToolSpec из реального типа — убить ручные зеркала (#446)` **Вердикт: PASS** ✅ Полный веер из 9 аспектов по СОБСТВЕННОЙ дельте PR (застекан на уже-одобренный #457; ревьюил `2e6f1c3d..fe5b6ecd` = 7 файлов, +217/−516 — без повторного ревью #457). Гейт зелёный в чистой комнате. <details> <summary>Механизм — проверено адверсариально (клик)</summary> Проблема (#446, восстановленный долг #294): in-app execute-замыкания зовут `DocmostClient` **позиционно** (`client.drawioGet(pageId, node, format ?? 'xml')`) с AI-SDK-стёртым (`any`) входом, а сервер держал ТРИ дрейфующих ручных зеркала (~230-строчный `DocmostClientLike`, копию `SharedToolSpec`, name-only runtime-тест). Перестановка/смена типа параметра в `client.ts` доезжала до **прода** рантайм-ошибкой без сигнала на компиляции. PR закрывает это деривацией из реального типа: - `declaration:true`+`declarationMap` (`packages/mcp/tsconfig.json`) + `types`/conditional-`exports` → эмитится `build/index.d.ts`. - Загрузчик: `import type { DocmostClient, SharedToolSpec }` (ПОЛНОСТЬЮ стёрт в compile — рантайм по-прежнему через dynamic `import()`), `DocmostClientLike = Pick<DocmostClient, DocmostClientMethod>`, ре-экспорт `SharedToolSpec` из пакета. - Never-called `__assertClientCallContract(client)` в `ai-chat-tools.service.ts` реплеит КАЖДЫЙ позиционный вызов типизированными плейсхолдерами → дрейф сигнатуры становится **серверной ошибкой компиляции**. - **АДВЕРСАРИАЛЬНО подтвердил**: добавил обязательный параметр в `DocmostClient.getPage` → пересобрал `.d.ts` → **server tsc КРАСНЫЙ** (`TS2554: Expected 2 arguments, but got 1`) и на реальном call-site, и в assert-функции; откатил → tsc снова 0. Гард реально ловит дрейф. </details> <details> <summary>Объективный гейт (клин-рум /home/claude/gm-ci, Node 22 = CI)</summary> - **mcp `.d.ts` эмитится зелёным** (declaration:true; index.d.ts экспортит DocmostClient/SharedToolSpec/REGISTRY_STAMP) — без portability/private-name ошибок эмита деклараций. - **server tsc = 0 (baseline)**: деривированные КОНКРЕТНЫЕ return-типы НЕ ломают `as`-касты (второй блокер старого staged-плана снят — касты идут concrete→надмножество-looser, валидно). - **mcp node --test 611/611** (−3 = ровно удалённый host-contract тест, больше ничего не сломано). - **server jest 52/52** (ai-chat-tools.service + comment-signal-inapp + docmost-client.loader, `--runInBand`). - Полнота: сверил все 44 вызова в `__assertClientCallContract` с реальными execute-телами — совпадают; union `DocmostClientMethod` — верное подмножество (корректно исключает login/ensureAuthenticated/uploadImage/updateComment/deleteComment). </details> <details> <summary>⛔ DROP — кодеру НЕ делать (лог калибровки)</summary> - `[by-design]` `low` **[architecture]** return-handling `as`-касты остаются loose (`Record<string,unknown>`) — staged-план (c). Намеренно вне scope этого PR (цель = безопасность АРГУМЕНТОВ, не возвратов); tsc зелёный, касты валидны (concrete→subset). Не дефект. - `[below-threshold]` `low` **[simplification]** `__assertClientCallContract` — новый рукотворный replay-список, держать в lockstep с execute-телами. Но: одно-направленный (было дву-направленное бремя HOST_CONTRACT_METHODS↔интерфейс), tsc стережёт дрейф сигнатур, строго сильнее старого name-only. Задокументировано. - `[below-threshold]` `low` **[conventions]** `declarationMap:true` эмитит `.d.ts.map` в gitignored-build приватного пакета — безвредный шум (в рантайме инертен), можно опустить. </details> DO/ESCALATE нет. Дельта убивает 3 дрейфующих зеркала (net −299 строк), заменяя их деривацией из реального типа + compile-time гардом, который **адверсариально доказан** (дрейф сигнатуры → красный server tsc). Все аспекты LGTM, гейт зелёный. Approve. <sub>9 аспектов (security+stability+regressions, coherence+testcov, conv+docs+simp+arch — все LGTM). Ядро проверено адверсариально мутацией сигнатуры client.ts → server tsc RED; .d.ts-эмит и baseline-tsc зелёные; assert-fn полнота сверена с execute-телами. Escalate нет.</sub>
agent_reviewer added review/approved and removed review/needs labels 2026-07-10 08:38:53 +03:00
vvzvlad merged commit f8a27cba91 into develop 2026-07-10 16:01:50 +03:00
Sign in to join this conversation.