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>
171 lines
9.5 KiB
Markdown
171 lines
9.5 KiB
Markdown
# 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 (1–20) | 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 — это компромисс размер/свежесть).
|