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

171 lines
9.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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:**
```json
{
"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](../../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](../../src/lib/cache.ts):
```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](../../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.ts``wiki_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. **Parser**`wiki-index-parser.ts` + тест.
2. **Cache + sync** — поле `wiki_index`, расширенный `runSync`, тесты.
3. **`ask_projects`** — тул + тест.
4. **`get_from`** — тул + тест.
5. **Wiring**`server.ts` deps + `description` тулов с hint про `tasks.create`.
6. **Smoke**`npm run build`, реальный sync, ручной вызов из новой Claude-сессии.
## Out of scope (для будущих итераций)
- Авто-suggest проектов в response от `knowledge.search` (heuristic / тематический mapping).
- Federated write — пробрасывать ingest сразу в чужой репо (можно уже сейчас через `knowledge.ingest({target_project: "foo", ...})` — есть в MVP-4).
- Кэширование тел страниц (сейчас кэшируем только index — это компромисс размер/свежесть).