refactor(mcp): вывести DocmostClientLike/SharedToolSpec из реального типа — убить ручные зеркала (#446) #458
Open
agent_coder
wants to merge 2 commits from
refactor/446-derive-client-types into develop
pull from: refactor/446-derive-client-types
merge into: vvzvlad:develop
vvzvlad:main
vvzvlad:fix/409-invalid-node-validation
vvzvlad:refactor/448-generate-inventory
vvzvlad:refactor/445-execute-mapping
vvzvlad:fix/447-registry-stamp-ci
vvzvlad:fix/444-agent-loop-guards
vvzvlad:fix/396-sendnow-detached-run
vvzvlad:fix/452-immutable-cache-control
vvzvlad:develop
vvzvlad:docs/footnote-authoring-comment-cleanup
vvzvlad:feat/437-error-diagnostics
vvzvlad:fix/442-cursor-pagination
vvzvlad:feat/419-footnote-normalize
vvzvlad:feat/424-drawio-rules
vvzvlad:perf/399-comment-resolve-async
vvzvlad:perf/435-collab-token-cache
vvzvlad:perf/344-background-rerenders
vvzvlad:perf/348-backend-lowhanging
vvzvlad:feat/423-drawio-crud
vvzvlad:perf/401-collab-hotpath
vvzvlad:feat/430-live-reconnect
vvzvlad:perf/400-collab-session
vvzvlad:refactor/414-dedup-node-ops
vvzvlad:feat/417-new-comments-signal
vvzvlad:feat/408-createcomment-hints
vvzvlad:feat/407-persist-tool-errors
vvzvlad:feat/420-footnote-render
vvzvlad:dummy-review/mcp-hang-fix
vvzvlad:dummy-review/mcp-hang-base
vvzvlad:feat/381-resumable-sse-pr1
vvzvlad:feat/git-sync-2
vvzvlad:docs/agents-workspace-build-order
vvzvlad:feature/offline-sync
vvzvlad:perf/342-code-splitting
vvzvlad:perf/346-compression-cache
vvzvlad:feat/196-multi-cursor
vvzvlad:feat/370-page-versioning
vvzvlad:perf/343-typing-latency
vvzvlad:docs/how-to-test
vvzvlad:fix/ai-sdk-partial-output-oom
vvzvlad:test/351-generative-converter
vvzvlad:feat/371-roles-catalog
vvzvlad:refactor/345-server-converter
vvzvlad:refactor/294-spec-registry-cont
vvzvlad:fix/363-migration-order
vvzvlad:fix/e2e-callout-and-gate-build
vvzvlad:fix/docker-re2-toolchain
vvzvlad:feat/git-sync
vvzvlad:fix/media-roundtrip-stability
vvzvlad:fix/340-comment-panel-perf
vvzvlad:fix/332-deferred-tools
vvzvlad:fix/329-ephemeral-suggestions
vvzvlad:fix/330-search-in-page
vvzvlad:fix/328-resolved-anchor-spam
vvzvlad:fix/331-intraline-diff
vvzvlad:fix/324-coverage-gate
vvzvlad:fix/325-mobile-390
vvzvlad:feat/293-A-git-sync-package
vvzvlad:feat/300-avatar-oklch
vvzvlad:fix/321-banner-mobile
vvzvlad:feat/300-avatar-colors
vvzvlad:feat/315-comment-suggestions
vvzvlad:feat/scroll-restore-stable-wait
vvzvlad:feat/300-agent-avatar-stack
vvzvlad:feat/300-avatar-polish
vvzvlad:refactor/294-tool-spec-registry
vvzvlad:feat/scroll-restore-ux
vvzvlad:fix/responsive-tablet-sidebar
vvzvlad:feature/ai-chat-page-change-observability
vvzvlad:image-inline-center
vvzvlad:fix/283-short-remap-title
vvzvlad:fix/283-slash-layout
vvzvlad:image-inline-row
vvzvlad:feat/276-ai-chat-dock
vvzvlad:fix/269-table-menu-refocus
vvzvlad:docs/dev-stand-guide
vvzvlad:feat/266-scroll-position
vvzvlad:fix/260-collab-docname-slugid
vvzvlad:test/244-phase2-tail
vvzvlad:fix/262-reindex-progress-realtime
vvzvlad:fix/258-changelog-compare-links
vvzvlad:fix/244-dataloss-bugs
vvzvlad:feat/246-spoiler
vvzvlad:feat/221-image-captions
vvzvlad:test/244-part-b
vvzvlad:feat/251-intentional-clear
vvzvlad:fix/embeddings-reindex-progress
vvzvlad:refactor/193-tool-spec-registry
vvzvlad:fix/255-ws-redis-adapter-leak
vvzvlad:fix/252-e2e-open-handles
vvzvlad:feat/229-catalog-yaml
vvzvlad:feat/243-blob-sandbox
vvzvlad:feat/228-inline-footnotes
vvzvlad:fix/qa-ui-bugs-216-218
vvzvlad:feature/agent-roles-catalog
vvzvlad:fix/share-alias-rename
vvzvlad:fix/ai-chat-empty-render
vvzvlad:feat/191-chat-doc-binding
vvzvlad:feat/201-temporary-notes
vvzvlad:feat/198-interrupt-agent
vvzvlad:feat/ai-chat-full-history
vvzvlad:feat/199-ai-generate-title
vvzvlad:feat/205-share-aliases
vvzvlad:batch/issues-189-187-170
vvzvlad:feat/170-mcp-test-button
vvzvlad:feat/189-context-badge
vvzvlad:feat/198-interrupt-agent-send-now
vvzvlad:fix/issues-190-159
vvzvlad:fix/ai-chat-new-chat-during-stream
vvzvlad:fix/ai-chat-stream-perf
vvzvlad:batch/issues-2026-06-25
vvzvlad:feat/ai-chat-persistent-history
vvzvlad:fix/ai-chat-copy-chat-wysiwyg
vvzvlad:fix/ai-stream-reset-resilience
vvzvlad:fix/ai-stream-undici-timeout
vvzvlad:fix/footnote-review-1227-followup
vvzvlad:fix/ai-chat-token-counter-realtime
vvzvlad:docs/manual-qa-test-plan
No Reviewers
Labels
Clear labels
epic
needs-human
review/approved
review/changes-requested
review/needs
возможно, баг
Large multi-phase effort spanning many changes
эскалация: нужно решение человека
в последнем ревью нет открытых blocking-находок
последнее ревью оставило открытые blocking-находки
head не ревьюился (head != reviewed_head)
Похоже на баг, но требует подтверждения/воспроизведения; чиним, если повторится
No Label
review/approved
Milestone
No items
No Milestone
Projects
Clear projects
No project
No Assignees
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: vvzvlad/gitmost#458
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Delete Branch "refactor/446-derive-client-types"
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
Восстановленный отложенный долг #294 — второй линк Фазы 1б роадмапа #416. closes #446
@docmost/mcpне отдавал.d.ts, поэтому в сервере жили ТРИ дрейфующие ручные копии одних и тех же имён/сигнатур:DocmostClientLike(~230-строчное зеркало 48 методовDocmostClient), копия интерфейсаSharedToolSpec, и name-onlyHOST_CONTRACT_METHODS-тест. In-app execute-тела зовут клиент ПОЗИЦИОННО (client.drawioGet(pageId, node, format ?? 'xml')), так что перестановка/смена типа параметра вclient.tsдоезжала до прода рантайм-ошибкой без единого сигнала на компиляции.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.__assertClientCallContract(client: DocmostClientLike)воспроизводит каждый позиционный вызов с типизированными плейсхолдерами (AI-SDK стирает вход execute-замыканий вany, иначе позиционные вызовы не проверялись). Перестановка параметровclient.ts→ ошибка компиляции сервера ровно тут. Looseas-касты в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, затем dynamicimport()резолвленного пути минует 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Ревью — #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 — рантайм по-прежнему через dynamicimport()),DocmostClientLike = Pick<DocmostClient, DocmostClientMethod>, ре-экспортSharedToolSpecиз пакета.__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. Гард реально ловит дрейф.Объективный гейт (клин-рум /home/claude/gm-ci, Node 22 = CI)
.d.tsэмитится зелёным (declaration:true; index.d.ts экспортит DocmostClient/SharedToolSpec/REGISTRY_STAMP) — без portability/private-name ошибок эмита деклараций.as-касты (второй блокер старого staged-плана снят — касты идут concrete→надмножество-looser, валидно).--runInBand).__assertClientCallContractс реальными execute-телами — совпадают; unionDocmostClientMethod— верное подмножество (корректно исключает login/ensureAuthenticated/uploadImage/updateComment/deleteComment).⛔ DROP — кодеру НЕ делать (лог калибровки)
[by-design]low[architecture] return-handlingas-касты остаются 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 нет.
View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.