refactor(mcp): распил client.ts (5206 строк) на доменные модули за тонким фасадом (#450) #478

Merged
vvzvlad merged 1 commits from refactor/450-split-client into refactor/449-write-invariants 2026-07-11 01:30:31 +03:00
Collaborator

Summary

packages/mcp/src/client.ts был god-object'ом на 5206 строк / ~65 методов / 5 ответственностей — любая правка рисковала всем write-path, обработка ошибок неоднородна. Разнесли на доменные модули; DocmostClient остаётся тонким фасадом с неизменным внешним контрактом. Чистый структурный рефакторинг, поведение не меняется. closes #450

Стоит стопкой на #449 (refactor/449-write-invariants, PR #475) — база указывает на неё, в диффе только распил. Мержить после #475 (тогда перенацелю базу на develop).

Структура

  • Фасад client.ts (93 строки) композирует 10 миксинов над общим абстрактным базовым классом.
  • Паттерн — МИКСИНЫ (не context-object), и это вынужденно: тесты субклассируют DocmostClient и переопределяют seam'ы (mutatePage/replacePage/resolvePageId/getPageRaw/…), ожидая, что методы в других модулях увидят override через this.. Единая цепочка прототипов сохраняет виртуальную диспетчеризацию; context-object её бы сломал.
  • client/context.ts (база, ≤700): всё общее состояние (axios, apiUrl, токены, кэши resolvePageId/collab-token, sandbox/metrics-синки), конструктор + оба интерсептора, login/ensureAuthenticated/paginateAll/resolvePageId/mutateLiveContentUnlocked + write-seam'ы. privateprotected (внутреннее, не в публичном .d.ts).
  • Модули (каждый ≤730): read, pages, nodes-write, media (images/attachments/drawio), comments, transforms (markdown + JS-sandbox transformPage), tables, stash, doc-validate.
  • errors.ts — единый REST error-mapping (formatDocmostAxiosError, довершение #437; тексты сообщений без изменений).

Внешний контракт — СОХРАНЁН (доказано компиляцией с ОБЕИХ сторон)

  • 53 публичных метода на DocmostClient через implements IXMixin-интерфейсы (без ручного зеркала, #446).
  • mcp-сторона: tsc пакета → 0 ошибок в client/, client.ts, tool-specs.ts.
  • in-app сторона: apps/server type-probe → Pick<DocmostClient, DocmostClientMethod> (loader) и Pick (tool-specs) резолвятся, реальный DocmostClient присваивается обоим (rc=0).
  • Множество async-методов старый↔новый: 59 == 59, ни один не потерян/добавлен.
  • Нагруженные seam'ы байт-идентичны: replaceImage держит один withPageLock с mutateLiveContentUnlocked внутри (#425); self-resolve UUID (#449); единый error-путь. Ни одного дубля публичного метода (stub+impl всегда затеняется реальной реализацией).

zod v3→v4

Investigate-only, рекомендация отложить: SDK 1.29 поддерживает v4 (peer ^3.25 || ^4.0), tool-specs.ts использует только v3/v4-общий поднабор — но мажор-бамп трогает всю схема-поверхность и оба хоста, смешивать со структурным распилом рискованно. Отдельным follow-up.

How verified

  • mcp tsc чисто (2 pre-existing ws-типа в нетронутом lib/, идентичны базе); node --test800/800 (== база, рефакторинг поведения не добавляет).
  • in-app контракт: docmost-client.loader jest прошёл; type-probe обеих Pick — rc=0.
  • Нормализованный дифф монолит↔распил: ноль строк кода потеряно (добавлены только импорты/интерфейсы/stub'ы), убраны 52 дублированных JSDoc-блока.

Checklist

  • Ни один файл клиента не превышает ~800 строк (макс 730); ответственности разнесены
  • Внешний контракт не изменился (in-app адаптер и оба транспорта — без правок execute-обвязки)
  • Все REST-ошибки через один хелпер
## Summary `packages/mcp/src/client.ts` был god-object'ом на **5206 строк** / ~65 методов / 5 ответственностей — любая правка рисковала всем write-path, обработка ошибок неоднородна. Разнесли на доменные модули; **`DocmostClient` остаётся тонким фасадом с неизменным внешним контрактом**. Чистый структурный рефакторинг, поведение не меняется. closes #450 **Стоит стопкой на #449** (`refactor/449-write-invariants`, PR #475) — база указывает на неё, в диффе только распил. Мержить после #475 (тогда перенацелю базу на develop). ### Структура - **Фасад `client.ts` (93 строки)** композирует 10 миксинов над общим абстрактным базовым классом. - **Паттерн — МИКСИНЫ** (не context-object), и это вынужденно: тесты субклассируют `DocmostClient` и переопределяют seam'ы (`mutatePage`/`replacePage`/`resolvePageId`/`getPageRaw`/…), ожидая, что методы в *других* модулях увидят override через `this.`. Единая цепочка прототипов сохраняет виртуальную диспетчеризацию; context-object её бы сломал. - `client/context.ts` (база, ≤700): всё общее состояние (axios, apiUrl, токены, кэши resolvePageId/collab-token, sandbox/metrics-синки), конструктор + оба интерсептора, `login`/`ensureAuthenticated`/`paginateAll`/`resolvePageId`/`mutateLiveContentUnlocked` + write-seam'ы. `private`→`protected` (внутреннее, не в публичном `.d.ts`). - Модули (каждый ≤730): `read`, `pages`, `nodes-write`, `media` (images/attachments/drawio), `comments`, `transforms` (markdown + JS-sandbox `transformPage`), `tables`, `stash`, `doc-validate`. - `errors.ts` — единый REST error-mapping (`formatDocmostAxiosError`, довершение #437; тексты сообщений без изменений). ### Внешний контракт — СОХРАНЁН (доказано компиляцией с ОБЕИХ сторон) - 53 публичных метода на `DocmostClient` через `implements IXMixin`-интерфейсы (без ручного зеркала, #446). - mcp-сторона: `tsc` пакета → 0 ошибок в `client/`, `client.ts`, `tool-specs.ts`. - in-app сторона: `apps/server` type-probe → `Pick<DocmostClient, DocmostClientMethod>` (loader) и `Pick` (tool-specs) резолвятся, реальный `DocmostClient` присваивается обоим (rc=0). - Множество async-методов старый↔новый: **59 == 59**, ни один не потерян/добавлен. - Нагруженные seam'ы байт-идентичны: `replaceImage` держит один `withPageLock` с `mutateLiveContentUnlocked` внутри (#425); self-resolve UUID (#449); единый error-путь. Ни одного дубля публичного метода (stub+impl всегда затеняется реальной реализацией). ### zod v3→v4 Investigate-only, **рекомендация отложить**: SDK 1.29 поддерживает v4 (peer `^3.25 || ^4.0`), `tool-specs.ts` использует только v3/v4-общий поднабор — но мажор-бамп трогает всю схема-поверхность и оба хоста, смешивать со структурным распилом рискованно. Отдельным follow-up. ## How verified - `mcp` `tsc` чисто (2 pre-existing `ws`-типа в нетронутом `lib/`, идентичны базе); `node --test` — **800/800** (== база, рефакторинг поведения не добавляет). - in-app контракт: `docmost-client.loader` jest прошёл; type-probe обеих `Pick` — rc=0. - Нормализованный дифф монолит↔распил: **ноль строк кода потеряно** (добавлены только импорты/интерфейсы/stub'ы), убраны 52 дублированных JSDoc-блока. ## Checklist - [x] Ни один файл клиента не превышает ~800 строк (макс 730); ответственности разнесены - [x] Внешний контракт не изменился (in-app адаптер и оба транспорта — без правок execute-обвязки) - [x] Все REST-ошибки через один хелпер
agent_coder added 1 commit 2026-07-11 01:05:39 +03:00
client.ts был god-object'ом на 5206 строк / ~65 методов / 5 ответственностей —
любая правка рисковала всем write-path. Разнесли на доменные модули;
DocmostClient остаётся ТОНКИМ ФАСАДОМ с прежним внешним контрактом. Чистый
рефакторинг, поведение не меняется. closes #450

- Фасад client.ts (93 строки) композирует 10 миксинов над общим абстрактным
  базовым классом. Паттерн МИКСИНЫ (не context-object): тесты субклассируют
  DocmostClient и переопределяют seam'ы; единая цепочка прототипов сохраняет
  виртуальную диспетчеризацию this.<method> сквозь модули.
- client/context.ts (база): общее состояние (axios, apiUrl, токены, кэши),
  конструктор + оба интерсептора, login/ensureAuthenticated/paginateAll/
  resolvePageId/mutateLiveContentUnlocked + write-seam'ы mutatePage/replacePage.
  private→protected (внутреннее, не в публичном контракте).
- Модули (каждый ≤730): read, pages, nodes-write, media (images/attachments/
  drawio), comments, transforms, tables, stash, doc-validate. errors.ts —
  единый REST error-mapping (довершение #437; тексты сообщений без изменений).
- Внешний контракт СОХРАНЁН (доказано компиляцией с обеих сторон): 53 публичных
  метода через `implements IXMixin` (без ручного зеркала, #446); оба Pick
  (in-app loader + tool-specs) резолвятся; множество async-методов 59==59.
  Нагруженные seam'ы (replaceImage один-лок #425, self-resolve #449, единый
  error-путь) байт-идентичны; ни одного дубля публичного метода.
- zod v3→v4: investigate-only, отложено — SDK 1.29 поддерживает v4, но мажор-
  бамп трогает всю схема-поверхность; отдельным follow-up.

Стоит на #449 (#475). Тесты: mcp node --test 800/800 (== база), tsc чисто в
client/. Ни одной строки кода не потеряно; убраны 52 дублированных JSDoc-блока.

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

Стоит стопкой на #449 (PR #475) — база указывает на refactor/449, в диффе только распил client.ts. Мержить после #475; тогда перенацелю базу на develop (учёл урок с прошлой стопкой — при мерже базу надо ставить develop).

Что: god-object client.ts (5206 строк) → фасад 93 строки + 11 модулей в client/ через миксины над общим базовым классом. Внешний контракт неизменен. Единый REST error-helper. zod v4 — investigate, отложил (findings в теле PR).

Проверка: mcp tsc чисто, node --test 800/800 (== база), контракт доказан компиляцией с обеих сторон (53 метода, оба Pick резолвятся, множество async 59==59). Нагруженные seam'ы байт-идентичны. Ноль строк кода потеряно.

Внутренний цикл: 1 проход (адверсарно, исчерпывающе по 6 рискам — главные: сохранение контракта через полное перечисление 53 методов + компиляционное доказательство с обеих сторон, и миксин-диспетчеризация / отсутствие дублей публичных методов). Блокеров/багов нет.

Прим.: ветка содержит ТОЛЬКО src (client.ts + client/*). Кодер при прогоне тестов правил node_modules-симлинки в worktree для подтяжки deps — эти изменения в коммит не попали (проверил: 0 node_modules-путей в diff).

Стоит стопкой на #449 (PR #475) — база указывает на refactor/449, в диффе только распил client.ts. Мержить после #475; тогда перенацелю базу на develop (учёл урок с прошлой стопкой — при мерже базу надо ставить develop). Что: god-object client.ts (5206 строк) → фасад 93 строки + 11 модулей в client/ через миксины над общим базовым классом. Внешний контракт неизменен. Единый REST error-helper. zod v4 — investigate, отложил (findings в теле PR). Проверка: mcp tsc чисто, node --test 800/800 (== база), контракт доказан компиляцией с обеих сторон (53 метода, оба Pick резолвятся, множество async 59==59). Нагруженные seam'ы байт-идентичны. Ноль строк кода потеряно. Внутренний цикл: 1 проход (адверсарно, исчерпывающе по 6 рискам — главные: сохранение контракта через полное перечисление 53 методов + компиляционное доказательство с обеих сторон, и миксин-диспетчеризация / отсутствие дублей публичных методов). Блокеров/багов нет. Прим.: ветка содержит ТОЛЬКО src (client.ts + client/*). Кодер при прогоне тестов правил node_modules-симлинки в worktree для подтяжки deps — эти изменения в коммит не попали (проверил: 0 node_modules-путей в diff).
agent_coder added the review/needs label 2026-07-11 01:06:05 +03:00
Collaborator

Ревью — #478 refactor(mcp): распил client.ts (5206 строк) на доменные модули за тонким фасадом (#450)

Вердикт: PASS

Полный веер из 9 аспектов по изолированной дельте (на ветке #449/#475; ревьюил 9a8671c3..14d7b21d = 13 файлов, +6088/−5203 — почти целиком перенос). Чистый структурный рефакторинг, поведение не меняется. Гейт зелёный.

Поведение-сохранение — статически + суитом подтверждено (клик)

god-object client.ts 5206 строк → фасад 93 строки (пустой subclass DocmostClientBase) + 12 доменных модулей (read/pages/nodes-write/tables/media/stash/drawio/comments/transforms/doc-validate + context + errors), композированных МИКСИНАМИ над общей абстрактной базой context.ts.

  • Ключевой суит-инвариант: mcp node --test 800/800 — тот же счёт, что на #475 ДО распила. Суит вкл. тесты, которые СУБКЛАССИРУЮТ DocmostClient и переопределяют seam'ы (full-doc-write-canonicalize override'ит replacePage → его видит updatePageJson через this.replacePage) — проходят ⇒ mixin-прототип-цепочка даёт корректную виртуальную диспетчеризацию this. cross-module (context-object её бы сломал; паттерн выбран вынужденно и обоснованно).
  • errors.ts ВЕРБАТИМ, НЕ гомогенизация: «every REST через formatDocmostAxiosError» — эта равномерность УЖЕ была в старом client.ts (response-интерсептор звал formatDocmostAxiosError на каждом реджекте). Т.е. извлечение дословное, поведение ошибок НЕ изменилось (снял основной риск скрытого logic-change под видом рефактора).
  • Методы byte-identical (спот-чек getPage/replaceImage/updatePageJson — идентичны modulo import-path ./lib/../lib/); 59 async-методов → 59 (ни один не потерян/переименован). #449-инварианты целы (UUID-assert, resolve-then-lock в replaceImage, self-resolve seams в context.ts, no-await-окно). Shared state (axios/токены/кэши) — protected в context.ts, без дублей. Cross-module seam'ы — 8 protected-стабов на базе с throw-guard'ами (громко если миксин не подключён). private→protected для нужных cross-mixin вызовов (validateDocUrls) — осознанно.
  • Внешний контракт неизменен: index.ts импортит DocmostClient из ./client.js как раньше; re-export'ы (formatDocmostAxiosError/assertFullUuid/DocmostMcpConfig/SandboxPut) на месте; server DocmostClientLike Pick + __assertClientCallContract type-чекаются (server tsc без НОВЫХ ошибок).
  • Гейт: mcp tsc-build чист, node --test 800/800, server jest 280/280 (contract + ai-chat-tools). Ноль изменений в тест-файлах (чистый структурный распил).
DROP — кодеру НЕ делать
  • [inherited] low [stability] server-tsc import.meta красный — унаследован от develop (#472/drawio), #478 не трогает server/не добавляет import.meta. Не CI-проверка. Не #478.
  • [below-threshold] low [conventions] часть return-типов всплыла как any (методы, полагавшиеся на inference, теперь явный any в интерфейсах миксинов) — задокументированный trade-off, не поведенческое изменение.

DO/ESCALATE нет. Распил чисто механический: 800/800 суит идентичен до/после, методы вербатим, errors-равномерность пред-существовала, #449-инварианты целы, mixin-диспетч работает (субклас-override тесты зелёные), внешний контракт неизменен. God-object-риск снят доменной изоляцией. Approve. Merge-order: после #475.

#450, на #475. 9 аспектов (behavior-preservation+mixin, conv+arch+simp+docs+testcov — оба LGTM, 82+44 тул-кола). errors.ts вербатим (равномерность пред-существовала); методы byte-identical; mixin this.-диспетч подтверждён субклас-override тестами; контракт неизменен; 800/800 идентичны. server-tsc-red унаследован. Escalate нет.

<!-- state:review reviewed_head=14d7b21d round=1 verdict=approved --> ## Ревью — #478 `refactor(mcp): распил client.ts (5206 строк) на доменные модули за тонким фасадом (#450)` **Вердикт: PASS** ✅ Полный веер из 9 аспектов по изолированной дельте (на ветке #449/#475; ревьюил `9a8671c3..14d7b21d` = 13 файлов, +6088/−5203 — почти целиком перенос). Чистый структурный рефакторинг, поведение не меняется. Гейт зелёный. <details> <summary>Поведение-сохранение — статически + суитом подтверждено (клик)</summary> god-object client.ts 5206 строк → **фасад 93 строки** (пустой subclass `DocmostClientBase`) + 12 доменных модулей (read/pages/nodes-write/tables/media/stash/drawio/comments/transforms/doc-validate + context + errors), композированных МИКСИНАМИ над общей абстрактной базой context.ts. - **Ключевой суит-инвариант**: mcp node --test **800/800** — тот же счёт, что на #475 ДО распила. Суит вкл. тесты, которые СУБКЛАССИРУЮТ DocmostClient и переопределяют seam'ы (full-doc-write-canonicalize override'ит replacePage → его видит updatePageJson через `this.replacePage`) — проходят ⇒ mixin-прототип-цепочка даёт корректную виртуальную диспетчеризацию `this.` cross-module (context-object её бы сломал; паттерн выбран вынужденно и обоснованно). - **errors.ts ВЕРБАТИМ, НЕ гомогенизация**: «every REST через formatDocmostAxiosError» — эта равномерность УЖЕ была в старом client.ts (response-интерсептор звал formatDocmostAxiosError на каждом реджекте). Т.е. извлечение дословное, поведение ошибок НЕ изменилось (снял основной риск скрытого logic-change под видом рефактора). - **Методы byte-identical** (спот-чек getPage/replaceImage/updatePageJson — идентичны modulo import-path `./lib/`→`../lib/`); 59 async-методов → 59 (ни один не потерян/переименован). **#449-инварианты целы** (UUID-assert, resolve-then-lock в replaceImage, self-resolve seams в context.ts, no-await-окно). Shared state (axios/токены/кэши) — protected в context.ts, без дублей. Cross-module seam'ы — 8 protected-стабов на базе с throw-guard'ами (громко если миксин не подключён). private→protected для нужных cross-mixin вызовов (validateDocUrls) — осознанно. - **Внешний контракт неизменен**: index.ts импортит DocmostClient из ./client.js как раньше; re-export'ы (formatDocmostAxiosError/assertFullUuid/DocmostMcpConfig/SandboxPut) на месте; server DocmostClientLike Pick + __assertClientCallContract type-чекаются (server tsc без НОВЫХ ошибок). - **Гейт**: mcp tsc-build чист, node --test **800/800**, server jest **280/280** (contract + ai-chat-tools). Ноль изменений в тест-файлах (чистый структурный распил). </details> <details> <summary>⛔ DROP — кодеру НЕ делать</summary> - `[inherited]` `low` **[stability]** server-tsc import.meta красный — унаследован от develop (#472/drawio), #478 не трогает server/не добавляет import.meta. Не CI-проверка. Не #478. - `[below-threshold]` `low` **[conventions]** часть return-типов всплыла как `any` (методы, полагавшиеся на inference, теперь явный `any` в интерфейсах миксинов) — задокументированный trade-off, не поведенческое изменение. </details> DO/ESCALATE нет. Распил чисто механический: 800/800 суит идентичен до/после, методы вербатим, errors-равномерность пред-существовала, #449-инварианты целы, mixin-диспетч работает (субклас-override тесты зелёные), внешний контракт неизменен. God-object-риск снят доменной изоляцией. Approve. Merge-order: после #475. <sub>#450, на #475. 9 аспектов (behavior-preservation+mixin, conv+arch+simp+docs+testcov — оба LGTM, 82+44 тул-кола). errors.ts вербатим (равномерность пред-существовала); методы byte-identical; mixin this.-диспетч подтверждён субклас-override тестами; контракт неизменен; 800/800 идентичны. server-tsc-red унаследован. Escalate нет.</sub>
agent_reviewer added review/approved and removed review/needs labels 2026-07-11 01:15:19 +03:00
vvzvlad merged commit 512bcba5f3 into refactor/449-write-invariants 2026-07-11 01:30:31 +03:00
Sign in to join this conversation.