refactor(ai-chat): shared parseNodeArg helper (dedupe quirk; full registry deferred) #114
Closed
Ghost
wants to merge 0 commits from
refactor/ai-chat-tool-spec-registry into develop
pull from: refactor/ai-chat-tool-spec-registry
merge into: vvzvlad:develop
vvzvlad:main
vvzvlad:test/244-part-b
vvzvlad:fix/255-ws-redis-adapter-leak
vvzvlad:feat/251-intentional-clear
vvzvlad:fix/252-e2e-open-handles
vvzvlad:feat/184-autonomous-agent-runs
vvzvlad:feat/221-image-captions
vvzvlad:feat/git-sync
vvzvlad:refactor/193-tool-spec-registry
vvzvlad:fix/244-dataloss-bugs
vvzvlad:fix/embeddings-reindex-progress
vvzvlad:develop
vvzvlad:feature/offline-sync
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
bug
documentation
duplicate
enhancement
epic
feature
good first issue
help wanted
idea
invalid
needs-human
question
refactor
review/approved
review/changes-requested
review/needs
security
status/blocked
status/done
status/in-progress
status/ready
test
wontfix
Something isn't working
Improvements or additions to documentation
This issue or pull request already exists
New feature or request
Large multi-phase effort spanning many changes
New functionality request
Good for newcomers
Extra attention is needed
Idea / proposal for discussion
This doesn't seem right
эскалация: нужно решение человека
Further information is requested
Code cleanup / refactoring
в последнем ревью нет открытых blocking-находок
последнее ревью оставило открытые blocking-находки
head не ревьюился (head != reviewed_head)
Security / hardening issue
ждёт зависимость blocked_by
закрыто и проверено
в активной работе (мягкая заявка)
специфицировано, не заблокировано, ждёт исполнителя
Test coverage / test infrastructure
This will not be worked on
No Label
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#114
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/ai-chat-tool-spec-registry"
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?
Рефакторинг: устранение дублирования определений инструментов (безопасный шаг)
Отчёт по скиллу архитектора.
1. Задача
Из
docs/backlog/ai-chat-tool-definitions-duplicated.md: один и тот же набор инструментов описан рукописно в нескольких местах (standalone MCPpackages/mcp/src/index.ts, in-app агентai-chat-tools.service.ts, плюс ручная копия сигнатурDocmostClientLike). Это forward-looking долг сопровождения (НЕ баг — код корректен сегодня): каждое добавление/правка инструмента требует синхронной правки в 2–3 местах, parity-баги (разъезд копий) приходится чинить дважды.2. Анализ — что реально достижимо безопасно
Полное устранение (единый transport-agnostic реестр спеков) архитектурно заблокировано, и это подтверждено фактами:
@docmost/mcp— ESM-only и НЕ отдаёт .d.ts (билд только JS), а сервер наmodule:commonjsфизически не может егоimport-ить (грузит в рантайме трюкомFunction('import()')). Значит рантайм-код нельзя разделить через границу обычным импортом, а type-only вывод требует эмита деклараций + правки tsconfig-paths.getCurrentPage/listSidebarPages, не отдаётdelete_comment/image-инструменты) и model-facing описания. Единый реестр поменял бы поведение И агента, И внешних MCP-клиентов — это отдельная фича со своей верификацией.Поэтому в этом PR — безопасный, поведенчески-нейтральный шаг, который убирает конкретный «материализованный parity-баг», на который указывает доку.
3. Решение
Квирк «node как объект ИЛИ JSON-строка» был рукописно скопирован в 6 точках (3 в standalone, 3 in-app). Вынес его в один хелпер
parseNodeArg()на пакет:packages/mcp/src/lib/parse-node-arg.ts(standalone: patch_node, insert_node, update_page_json);apps/server/src/core/ai-chat/tools/parse-node-arg.ts(in-app: patchNode, insertNode, updatePageJson).Две копии — намеренно (ESM/CJS-граница, объяснено в комментарии серверной копии): теперь каждая — единый источник внутри своего пакета (6 inline-копий → 2 хелпера). Поведение байт-в-байт: сообщения throw сохранены точно (
node was a string but not valid JSON/content was a string but not valid JSON); ветка null/object у update_page_json не тронута. Имена/описания/zod-схемы инструментов не менялись.packages/mcp/buildпересобран в синк.Отложено (зафиксировано здесь, т.к. доку удаляется этим коммитом): полный реестр спеков + вывод
DocmostClientLikeиз реального типа — по причинам из §2 (Part 2 пробовался и откатан: реальные точные типы клиента ломают тест-стабы in-app инструментов, tsc краснеет). Делать инкрементально.4. Найденные баги
Part 2 (DocmostClientLike из реального типа) — при попытке tsc дал 3 ошибки в
ai-chat-tools.service.spec.ts(стабы возвращают упрощённые{ ok: true }, несовместимые с точными типами). Откатил целиком, чтобы tsc остался зелёным — это и есть ожидаемая regression-поверхность, описанная в §2.Ревью-субагент: проблем не найдено — APPROVE, поведение тождественно, билд эмпирически байт-в-байт совпадает с исходником.
5. Тестирование — статистика
Юнит-тесты: server Jest
parse-node-arg.spec.ts5/5; mcpnode:test— 254 pass; servertsc --noEmitexit 0.Циклы ревью: 1 (APPROVE; 1 посторонний build-дрейф
docmost-schema.jsисключён из PR черезgit restore).Живая браузерная проверка: 1 цикл, автономный субагент, реальный стенд+LLM, 0 багов. Агент по инструкции вставил абзац
INSERTED-BY-AGENT-F4→ подтверждено в персистентном ProseMirror-документе черезPOST /api/pages/info(новая нода с собственным attrs.id), не только в клиенте. Значит путь insertNode/patchNode →parseNodeArgработает вживую без регресса. Скрины/tmp/f4-*.png.🤖 Generated with Claude Code
First, safe step of docs/backlog/ai-chat-tool-definitions-duplicated.md: the "node may be a JSON object OR a JSON string" quirk was hand-copied at 6 tool sites. Extract it into a single parseNodeArg() helper per package and call it at every site. Behavior-preserving — each site's throw message is byte-identical (patch/insert: 'node was a string but not valid JSON'; update_page_json: 'content was a string but not valid JSON'); no tool name/description/schema changed. Two helper copies (packages/mcp/src/lib/parse-node-arg.ts and apps/server/src/core/ai-chat/tools/parse-node-arg.ts) are intentional: the ESM-only @docmost/mcp cannot be imported by the CommonJS server (it is loaded at runtime via the Function('import()') trick), so runtime code cannot cross that boundary by a normal import. Each copy is now the single source within its package (6 inline copies -> 2 helpers). packages/mcp/build rebuilt in sync. Tests: parse-node-arg.spec.ts (server, Jest) + parse-node-arg.test.mjs (mcp, node:test) — object passthrough, valid-string parse, invalid-string throw with the right message. Server tsc clean; mcp suite 254 pass; agent structural-edit path verified live in-browser (agent inserted a node, persisted to the doc). Deferred (documented for the record, since the backlog doc is removed with this commit): the FULL transport-agnostic tool-spec registry (one name+schema+ description per tool shared by both transports) and deriving DocmostClientLike from the real client type. Both are blocked by the current architecture, not by effort: (1) @docmost/mcp ships no type declarations and is ESM-only, so a type-only derivation needs declaration emission + tsconfig path wiring, and the real client's precise return types break the in-app tool test stubs (attempted, reverted to keep tsc green); (2) the two transports intentionally DIVERGE in tool NAMES (snake_case x38 vs camelCase x41), membership (in-app adds getCurrentPage/ listSidebarPages, omits delete_comment/image tools) and model-facing DESCRIPTIONS, so a unified registry would change behavior on BOTH the agent and external MCP clients and needs its own verification pass. This is forward-looking debt (the code is correct today), to be done incrementally. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>Pull request closed