Files
factory/lib/projects-meta-mcp/docs/superpowers/specs/2026-04-30-federated-knowledge-ask-projects-design.md
vitya ca669d96e1 feat(lib): bundle MCP servers from .common/lib into factory/lib/
Copies source (no node_modules, dist, .tasks, .wiki, __pycache__) for:
- projects-meta-mcp v2.25.0 (TypeScript/Node)
- wiki-graph v0.3.1 (TypeScript/Node)
- interns-mcp v0.3.3 (Python/FastMCP)

.gitignore: exclude lib build artefacts (node_modules, dist, .venv, __pycache__, *.pyc)
bootstrap.ps1: add MCP build step — npm install+build for TS servers, venv+pip for Python

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 13:17:29 +03:00

9.5 KiB
Raw Blame History

Federated knowledge query — knowledge.ask_projects / knowledge.get_from

Status: design approved 2026-04-30 Repo: projects-meta-mcp Adds: 2 read-only MCP tools + 1 sync extension + 1 cache field

Problem

Сейчас knowledge.search / knowledge.get читают только из shared projects-wiki (общая мета-вики). Если знания там нет — у агента тупик. При этом локальные .wiki/ отдельных проектов могут это знание содержать (например, node-utils/.wiki/concepts/windows-yarn-exec.md — частный кейс который никто ещё не промоутил в shared).

Хотим: если в shared не нашлось → опросить конкретные другие проекты (юзер указывает имена) на предмет того же знания. Если и там нет — оформить через уже существующий tasks.create({target_project, body}).

Non-goals

  • Broadcast по всем проектам. Спрашиваем только перечисленные явно — экономим токены и сетку, юзер знает кого спрашивать.
  • Авто-fallback внутри knowledge.search. search остаётся узким (только shared); агент сам решает звать ли ask_projects.
  • Новый "request-knowledge-prep" механизм. tasks.create уже умеет ставить задачу другому проекту — Оккам.
  • Confirm-gate / dry-run. Оба новых тула read-only.

Architecture

Tools (новые, read-only)

knowledge.ask_projects(query, projects[], limit?, types?)

Ищет в .wiki/ указанных репо.

Input:

field type required default
query string yes
projects string[] yes
limit int (120) no 5 (per project)
types string[] no ["entities","concepts","packages","sources"] (raw исключён по дефолту — тяжёлые dumps; чтобы включить, явно перечисли все 5)

Output:

{
  "asked_projects": ["foo", "bar"],
  "results": [
    {
      "project": "foo",
      "matches": [
        {"slug": "concepts/x", "type": "concepts", "title": "...", "snippet": "..."}
      ]
    },
    {"project": "bar", "error": "no wiki — repo has no .wiki/index.md"}
  ],
  "next_action_hint": "knowledge.get_from(project, slug) для полного текста; tasks.create(target_project, body) если знание не оформлено"
}

knowledge.get_from(project, slug)

Полный текст одной страницы из конкретного проекта (live через Gitea API).

Input:

field type required
project string yes
slug string yes — относительный путь под .wiki/ без .md, включает type-префикс (например concepts/foo или entities/bar)

Output: raw markdown body, либо {error} если не найдено.

Sync extension

Расширяем runSync в src/lib/sync-runner.ts:

  • В per-repo worker добавляется параллельный getRawFile(user, repo, '.wiki/index.md', branch) рядом с уже существующим .tasks/STATUS.md.
  • Если оба null → skip как и раньше. Если есть хоть один — пишем ProjectStatus.

Cache extension

В src/lib/cache.ts:

interface ProjectStatus {
  // ...existing fields
  wiki_index?: string;  // NEW — raw .wiki/index.md, undefined если нет
}

Один общий cache.json, не плодим параллельный кэш.

Index parser

Новый src/lib/wiki-index-parser.ts:

  • Парсит canonical index.md (формат из src/lib/wiki-writer.ts freshIndexMd() / insertIndexEntry()).
  • Возвращает Array<{slug: string; type: WikiPageType; title: string}>.
  • Структура — секции по типам, под каждой list - [title](type/slug.md).

Read flow ask_projects

  1. readCache(cacheFile) → найти проекты из projects[] параметра по name.
  2. Для каждого:
    • wiki_index === undefined{project, error: "no wiki — ..."}
    • Иначе: парсим index, фильтр по types, substring case-insensitive по slug + title против query.
    • Берём первые limit совпадений.
    • Для каждого — backend.getRawFile(user, project, '.wiki/' + type + '/' + slug + '.md', branch) (concurrency-pool).
    • Из body вырезаем snippet (~80 char вокруг матча, как в knowledge.search).
  3. Если проекта нет в кэше → {project, error: "unknown project — run sync first"}.
  4. Если live fetch упал → matched entry с snippet: "(fetch failed)", slug+title сохраняем.

Read flow get_from

  1. readCache → найти project → default_branch.
  2. backend.getRawFile(user, project, '.wiki/' + slug + '.md', branch) — slug включает type-префикс (тот же формат что отдаёт ask_projects в matches[].slug).
  3. null → error page not found: <project>/.wiki/<slug>.md.

Matching semantics

Реплицируем knowledge.search:

  • Substring case-insensitive
  • Сначала по slug + title (на index)
  • Потом по body (для snippet) при fetched-странице
  • raw тип исключён по дефолту (тяжёлые dumps); включается явным перечислением — types: ["concepts","raw"] или все 5 типов

Error branches

Случай Поведение
Index.md отсутствует в репо {project, error: "no wiki"} — тихо, остальные проекты обрабатываются
Project не в cache.projects[] {project, error: "unknown project — run sync first"}
Live fetch страницы упал matched entry с snippet: "(fetch failed)"
Парсинг index.md упал {project, error: "wiki index malformed: <reason>"}
backend не сконфигурирован (auth.toml нет) tool возвращает error — как сейчас у write-tools

Configuration

Новых конфиг-полей нет. Целевые проекты передаются параметром projects[]. Sync уже знает все репы юзера через listUserRepos.

Testing

Все новые тесты — vitest, stub Backend. Никаких реальных Gitea-вызовов.

Файл Что покрывает
tests/lib/wiki-index-parser.test.ts Валидный index.md, пустые секции, поломанный markdown, nested slugs (concepts/foo/bar)
tests/lib/sync-runner.test.ts (расширение) wiki_index подтягивается параллельно с STATUS.md; репо только с .wiki/ без .tasks/ попадает в кэш; ни того ни другого → skip
tests/tools/knowledge-ask-projects.test.ts Match по slug+title, default types-фильтр (raw исключён), limit per project, error-branches (no wiki, unknown project, fetch fail, malformed index), partial success (один проект упал, другие отдали)
tests/tools/knowledge-get-from.test.ts Happy path, project not found, page not found

Files

New:

  • src/lib/wiki-index-parser.ts
  • tests/lib/wiki-index-parser.test.ts
  • tests/tools/knowledge-ask-projects.test.ts
  • tests/tools/knowledge-get-from.test.ts

Modify:

  • src/lib/cache.tswiki_index?: string в ProjectStatus
  • src/lib/sync-runner.ts — параллельный getRawFile для .wiki/index.md
  • src/tools/knowledge.ts — 2 новых тула, описания включают цепочку с tasks.create для случая "знания нет"
  • src/server.ts — проверить что cacheFile + backend пробрасываются в makeKnowledgeTools (уже частично есть)
  • tests/lib/sync-runner.test.ts — расширить existing

Implementation phases

  1. Parserwiki-index-parser.ts + тест.
  2. Cache + sync — поле wiki_index, расширенный runSync, тесты.
  3. ask_projects — тул + тест.
  4. get_from — тул + тест.
  5. Wiringserver.ts deps + description тулов с hint про tasks.create.
  6. Smokenpm run build, реальный sync, ручной вызов из новой Claude-сессии.

Out of scope (для будущих итераций)

  • Авто-suggest проектов в response от knowledge.search (heuristic / тематический mapping).
  • Federated write — пробрасывать ingest сразу в чужой репо (можно уже сейчас через knowledge.ingest({target_project: "foo", ...}) — есть в MVP-4).
  • Кэширование тел страниц (сейчас кэшируем только index — это компромисс размер/свежесть).