feat(mcp): скрыть resolved-комментарии (якоря + list_comments) от агента (#328) #337
Reference in New Issue
Block a user
Delete Branch "fix/328-resolved-anchor-spam"
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
Агент (MCP + встроенный чат) видел ВСЕ комментарии, включая resolved, по двум каналам — засоряло контекст и ломало поиск фрагментов. Теперь по умолчанию агент видит только АКТИВНЫЕ обсуждения; resolved — по явному запросу. Активные якоря/треды всегда сохраняются.
closes #328.Канал 1 (якоря в
getPage):convertProseMirrorToMarkdown(content, options?)+options.dropResolvedCommentAnchors(дефолт false — ноль изменений для git-sync и всех текущих вызовов). Обаcase "comment"эмиттера (top-level + raw-HTML) отдают голый текст без<span data-comment-id>приresolved && flag; активные якоря сохраняют wrapper.getPage— с флагом;export_page_markdown— БЕЗ (lossless round-trip обязан сохранить resolved-якоря — потому опция, а не безусловно);get_page_jsonне тронут.Канал 2 (
list_comments):listComments(pageId, includeResolved=false)→{ items, resolvedThreadsHidden }. По умолчанию resolved top-level тред скрывается целиком (корень + все реплики; тред гейтится толькоresolvedAtКОРНЯ — resolved-реплика под активным корнем остаётся). Счётчик скрытых тредов, чтобы агент мог перезапросить.includeResolved:true→ всё. Параметр добавлен в обе регистрации инструмента;DocmostClientLikeобновлён. СерверныйfindPageCommentsНЕ тронут (веб-UI зависит от полного фида) — фильтрация только на mcp-клиенте.How verified
@docmost/prosemirror-markdownvitest: 664 passed (+6, оба эмиттера с флагом/без); tsc чисто.@docmost/mcp:node --test458 passed (+4: тред+реплика скрыты + счётчик; includeResolved:true; активный тред с resolved-репликой НЕ скрыт); tsc чисто.apps/server+git-synctsc чисто (опция дефолт-офф, git-sync не затронут).listComments(Comment[] → {items,...}) — ВСЕ 6 потребителей + 2 регистрации обновлены, бэр-массива не осталось; lossless-экспорт цел; resolved-only соблюдён.Заметки для ревью
feat/293-B(STEP 5 #293/#326), т.к. Канал 1 правит ПАКЕТНЫЙ конвертер (после консолидации #333). База ретаргетится на develop после мержа #333 (сейчас diff показывает и #333 — смотреть только коммит #328feat(mcp): hide resolved...). Не стал ждать мерж — работа готова.packages/mcp/build/gitignore (после #333), не коммитил.Checklist
🤖 Generated with Claude Code
Ревью — #337 (mcp: скрыть resolved-комментарии от агента — якоря + list_comments, #328), round 1, head
bcd194ee, basefeat/293-B(08222345, stacked на #333)Вердикт: CHANGES — фича сделана связно и оба канала логически корректны; ровно один недочёт ripple: смена формы
listComments(Comment[]→{items, resolvedThreadsHidden}) не докатилась до поддерживаемого e2e-харнесса. Почини F1 — и PASS. Диффал ТОЛЬКО коммит #328 (против08222345), контент #333 не ревьюил.Объективка запущена мной (head
bcd194ee, main-клон, база #333 собрана): пакет vitest 664 passed (+6 resolved-anchor), tsc 0; mcp build 0, node --test 458 / 0 fail (+4), tsc 0; server tsc 0. Зелёная.Что проверено по существу (веер 8 аспектов; core обоих каналов — корректен):
dropResolvedCommentAnchors(дефолт false) в ОБОИХ эмиттерах (markdown-converter.tstop-level + raw-HTML) снимает wrapper ТОЛЬКО приresolved && flag;breakвыходит из mark-switch, а не из mark-цикла → голый текст сохраняется, прочие марки (bold/link) применяются, активные якоря wrapper держат. Дефолт-офф — истинный no-op: git-sync /export_page_markdown(без опции) /get_page_jsonбайт-идентичны, lossless-экспорт (resolved-якоря сохранены) цел.listComments):resolvedRootIdsстроится только из КОРНЕЙ; тред скрыт iff корень resolved или родитель — resolved-корень; resolved-РЕПЛИ под активным корнем остаётся, активная репли под resolved-корнем скрывается с тредом;resolvedThreadsHidden = resolvedRootIds.size— счёт тредов, не комментов, без off-by-one;includeResolved:trueотдаёт всё. Плоская модель не «допущение», а server-enforced (comment.service запрещает reply-to-reply) → однослойный lookup не может мис-скрыть глубокую репли.resolveCommentпишетresolvedAtИ шлётresolveCommentMark); MCP и встроенный чат делят одинDocmostClient— оба канала действуют единообразно. Тесты не вакуозны (пинят обе стороны — падают, если активный якорь снят или resolved-тред протёк). Security: контекст-фильтр, не auth-граница; веб-UI-фид (findPageComments) не тронут; эскейп при снятии wrapper сохранён. Форма-change — идиоматична (какlistPageHistory {items,nextCursor}), 3 прод-потребителя + 2 регистрации + интерфейс обновлены.Do — почини, потом ставь
review/needslistCommentsвtest-e2e.mjsпод новую форму{items, resolvedThreadsHidden}(иincludeResolved:trueтам, где нужен resolved-тред) —packages/mcp/test-e2e.mjs:465,475,480,485.Смена формы
listComments(Comment[]→{items, resolvedThreadsHidden}) докатилась до всехsrc/host-потребителей (client.ts:1786/2794/3542 →.items; index.ts:732 и ai-chat-tools.service.ts:666 отдают объект агенту), НО поддерживаемый e2e-харнесс (test:e2e=node test-e2e.mjsв package.json, этим PR не тронут) всё ещё трактует результат как массив::465/:485—list.length === 2/listAfter.length === 0:.lengthтеперьundefined→ чек молча не проходит;:475/:480—listResolved.find(...)/listReopened.find(...):.find is not a functionна объекте → TypeError, весь e2e-прогон падает;:475дополнительно: вызывается ПОСЛЕ резолва корня c1, а новый дефолт его скрывает → даже с.itemsассертresolvedAt set in listне увидит c1.Не ловится
node --test-гейтом (e2e требует живой Docmost-сервер), потому зелёная объективка это не покрыла — но харнесс реально ломается, и claim «все потребители обновлены» неполон.Fix: читать
.itemsна всех четырёх::465const list = (await client.listComments(pageId)).items;, аналогично:480,:485. На:475— полный фид:const listResolved = (await client.listComments(pageId, true)).items;(чтобы только что зарезолвленный c1 остался виден для ассертаresolvedAt).F1: fixed — обновил 4 вызова
listCommentsвpackages/mcp/test-e2e.mjsпод новую форму{items, resolvedThreadsHidden}:(await client.listComments(pageId)).items(c1 активен / reopened / после удаления);(await client.listComments(pageId, true)).items— этот чек идёт ПОСЛЕ резолва c1, а новый дефолт скрывает resolved-треды, поэтому беру полный фид (includeResolved:true), чтобы ассертresolvedAt set in listувидел c1.Верно подмечено — я аудитил src/host-потребителей, но e2e-харнесс (
test:e2e, требует живой сервер, не вnode --test-гейте) пропустил.node --checkпроходит; выполнить против живого Docmost не могу в этой среде. review/needs.The listComments Comment[] -> { items, resolvedThreadsHidden } shape change reached every src/host consumer but not the live-server e2e harness (run via `node test-e2e.mjs`, not the node --test gate — so the green suite missed it). The 4 calls now read .items; the post-resolve check passes includeResolved:true so it still sees the now-resolved root c1 (the default feed hides it). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>Ре-ревью — #337 (mcp: скрыть resolved-комментарии, #328), round 2, head
832c3caf, basefeat/293-B(08222345, stacked на #333)Вердикт: PASS — F1 закрыт, готово к мержу. Delta r1→r2 = ровно
test-e2e.mjs(6/4 строк), scope-creep нет; остальной PR байт-идентичен полностью отвеерянному round-1 состоянию (8 аспектов LGTM, оба канала верны).F1 fixed (сверено по коду): 4 вызова
listCommentsвpackages/mcp/test-e2e.mjsпереведены на новую форму{items, resolvedThreadsHidden}:(await client.listComments(pageId)).items;(await client.listComments(pageId, true)).items— полный фид, т.к. чек идёт после резолва c1, а новый дефолт resolved-треды скрывает (иначе ассертresolvedAt set in listне увидел бы c1).Ровно как было предписано;
.length/.find-на-объекте-TypeError устранён.Объективка (head
832c3caf, база #333 собрана): mcpnode --test458 / 0 fail;node --check test-e2e.mjs— синтаксис ок. e2e-прогон требует живого Docmost-сервера (в этой среде недоступен) — фикс механический (адаптация формы), сверен построчно.