# Federated knowledge — `knowledge.ask_projects` / `get_from` Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Расширить `projects-meta-mcp` двумя read-only тулами, позволяющими опросить `.wiki/` указанных проектов, если в shared wiki ответа нет. Постановка задачи на оформление знания идёт через уже существующий `tasks.create` — новых сущностей не плодим. **Architecture:** `runSync` дополнительно тянет `.wiki/index.md` каждого репо в существующий `cache.json` (новое поле `ProjectStatus.wiki_index`). Новый парсер извлекает `{slug, type, title}` из canonical index. Тул `knowledge.ask_projects` фильтрует по index из кэша, потом live через Gitea API тянет top-K матчей. Тул `knowledge.get_from` тянет одну страницу live. Никаких новых конфигов и кэш-файлов. **Tech Stack:** TypeScript, vitest, Gitea Contents API через `Backend` interface, `gray-matter` для frontmatter (только если потребуется — парсер index.md работает с plain markdown). **Spec:** [`docs/superpowers/specs/2026-04-30-federated-knowledge-ask-projects-design.md`](../specs/2026-04-30-federated-knowledge-ask-projects-design.md) **Branching policy:** Всё прямо в `main` — отдельные feature-branches не используем (per user preference). --- ## File Structure **Create:** - `src/lib/wiki-index-parser.ts` — парсер canonical `.wiki/index.md` → `Array<{slug, type, title}>` - `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` — два новых тула в `makeKnowledgeTools`, расширить `Opts` - `tests/lib/sync-runner.test.ts` — переписать `fakeClient` под path-aware mock + добавить тесты на `wiki_index` `src/server.ts` уже пробрасывает `cacheFile`, `backend`, `giteaUser` — менять не надо. --- ## Task 1: Index parser **Files:** - Create: `src/lib/wiki-index-parser.ts` - Test: `tests/lib/wiki-index-parser.test.ts` Парсер canonical `.wiki/index.md` (формат генерится `freshIndexMd` / `insertIndexEntry` в [src/lib/wiki-writer.ts](../../src/lib/wiki-writer.ts)). Структура: ``` ## Entities - [some/slug](entities/some/slug.md) — Some Title - [other](entities/other.md) — Other ## Concepts ``` Парсер: - Идёт по секциям `## Entities` / `## Concepts` / `## Packages` / `## Sources` / `## Raw`. - В каждой собирает строки вида `- [slug](type/slug.md) — title` (тире — Unicode em-dash `—`). - Игнорирует placeholder `` и пустые строки. - Возвращает `Array<{slug, type, title}>` — `slug` хранится с type-префиксом (`"concepts/foo"`) для совместимости с API tools. - [ ] **Step 1.1: Write the failing test** `tests/lib/wiki-index-parser.test.ts`: ```typescript import { describe, it, expect } from 'vitest'; import { parseWikiIndex } from '../../src/lib/wiki-index-parser.js'; describe('parseWikiIndex', () => { it('extracts entries from canonical index.md', () => { const md = `# Wiki Index ## Overview - [overview.md](overview.md) — project overview ## Entities - [some-entity](entities/some-entity.md) — Some Entity Title ## Concepts - [foo](concepts/foo.md) — Foo Concept - [bar/baz](concepts/bar/baz.md) — Nested Bar Baz ## Packages ## Sources `; const entries = parseWikiIndex(md); expect(entries).toEqual([ { slug: 'entities/some-entity', type: 'entities', title: 'Some Entity Title' }, { slug: 'concepts/foo', type: 'concepts', title: 'Foo Concept' }, { slug: 'concepts/bar/baz', type: 'concepts', title: 'Nested Bar Baz' }, ]); }); it('returns empty array when all sections are empty', () => { const md = `# Wiki Index ## Entities ## Concepts `; expect(parseWikiIndex(md)).toEqual([]); }); it('returns empty array on completely malformed input', () => { expect(parseWikiIndex('not markdown at all')).toEqual([]); expect(parseWikiIndex('')).toEqual([]); }); it('skips Overview section (special-cased: link is to overview.md, not type/slug.md)', () => { const md = `# Wiki Index\n\n## Overview\n\n- [overview.md](overview.md) — project overview\n`; expect(parseWikiIndex(md)).toEqual([]); }); it('skips entries whose link does not match expected type/slug.md pattern', () => { const md = `# Wiki Index ## Concepts - [external](https://example.com) — External Link - [proper](concepts/proper.md) — Proper Entry `; expect(parseWikiIndex(md)).toEqual([ { slug: 'concepts/proper', type: 'concepts', title: 'Proper Entry' }, ]); }); it('handles Raw section', () => { const md = `# Wiki Index\n\n## Raw\n\n- [doc](raw/doc.md) — Some PDF\n`; expect(parseWikiIndex(md)).toEqual([ { slug: 'raw/doc', type: 'raw', title: 'Some PDF' }, ]); }); }); ``` - [ ] **Step 1.2: Run tests and verify they fail** Run: `npx vitest run tests/lib/wiki-index-parser.test.ts` Expected: FAIL with "Cannot find module '../../src/lib/wiki-index-parser.js'" - [ ] **Step 1.3: Implement the parser** `src/lib/wiki-index-parser.ts`: ```typescript import type { WikiPageType } from './wiki-writer.js'; export interface WikiIndexEntry { /** Slug includes the type prefix, e.g. "concepts/foo" or "entities/bar/baz". */ slug: string; type: WikiPageType; title: string; } const SECTION_TYPES: Record = { Entities: 'entities', Concepts: 'concepts', Packages: 'packages', Sources: 'sources', Raw: 'raw', }; const ENTRY_RE = /^- \[(?