From 2ddb546029ab086d6200f5ccc375f409374a2aa9 Mon Sep 17 00:00:00 2001 From: jimpablo <194239734+jimpablo@users.noreply.github.com> Date: Thu, 10 Sep 2026 13:27:55 +0800 Subject: [PATCH] feat(codebase): expose wiki deep-enrich Expose the existing deep-enrich engine as `teamai codebase --deep-enrich`, so the bundled wiki skill can generate deep knowledge without the hidden top-level command or a separate team-wiki CLI. Related to #360 (slice 3; does not close the full issue). --- README.md | 2 + README.zh-CN.md | 2 + docs/usage-guide.md | 3 + docs/usage-guide.zh-CN.md | 3 + skills/team-wiki-codebase/README.md | 1 + skills/team-wiki-codebase/SKILL.md | 1 + src/__tests__/codebase-deep-enrich.test.ts | 134 ++++++++++++++++++ .../e2e/codebase-extract-cli.test.ts | 27 ++++ src/__tests__/index-codebase-help.test.ts | 32 +++-- .../team-wiki-codebase-skill.test.ts | 5 + src/codebase-cmd.ts | 60 +++++++- src/index.ts | 3 +- 12 files changed, 259 insertions(+), 14 deletions(-) create mode 100644 src/__tests__/codebase-deep-enrich.test.ts diff --git a/README.md b/README.md index 8c076b3d..229884bf 100644 --- a/README.md +++ b/README.md @@ -191,6 +191,7 @@ Matched: conflict | Missing: port teamai import --from-repo https://github.com/org/repo teamai import --from-org myorg # batch import all repos teamai codebase --extract /path/to/repo # local extract into teamwiki/ +teamai codebase --deep-enrich --project my-service --output /path/to/repo # generate deep knowledge docs teamai codebase --reconcile --output /path/to/repo # map product docs to code pages teamai codebase --lint --output /path/to/repo # check the locally extracted graph ``` @@ -244,6 +245,7 @@ Insight into how the team actually uses its AI tools, and a starting point for t | `teamai recall maintenance` | Maintain knowledge base health: prune low-confidence learnings, writeback confidence scores, flag stale entries | | `teamai import` | Import knowledge (`--dir`, `--from-repo`, `--from-org`, `--from-repo-list`, `--from-mr`) | | `teamai codebase --extract [path]` | Extract code facts and build the local graph under `teamwiki/` | +| `teamai codebase --deep-enrich` | Generate deep knowledge docs from extracted evidence | | `teamai codebase --reconcile` | Reconcile product documentation with extracted code knowledge | | `teamai codebase --lint` | Knowledge graph health check | | `teamai ci extract-mr --url ` | CI: extract knowledge from MR, post comments, write after merge | diff --git a/README.zh-CN.md b/README.zh-CN.md index d858c4b0..7104f6d7 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -191,6 +191,7 @@ Matched: conflict | Missing: port teamai import --from-repo https://github.com/org/repo teamai import --from-org myorg # 批量导入所有仓库 teamai codebase --extract /path/to/repo # 本地提取到 teamwiki/ +teamai codebase --deep-enrich --project my-service --output /path/to/repo # 从提取结果生成深度知识文档 teamai codebase --reconcile --output /path/to/repo # 将产品文档映射到代码页面 teamai codebase --lint --output /path/to/repo # 检查本地提取的图谱 ``` @@ -244,6 +245,7 @@ teamai recall maintenance --update-quality # 为过时 skills / docs 生 | `teamai recall maintenance` | 维护知识库健康:清理低置信度 learnings、回写置信度、标记过时条目 | | `teamai import` | 导入知识(`--dir`、`--from-repo`、`--from-org`、`--from-repo-list`、`--from-mr`) | | `teamai codebase --extract [path]` | 提取代码事实并在 `teamwiki/` 下构建本地图谱 | +| `teamai codebase --deep-enrich` | 从已提取的 evidence 生成深度知识文档 | | `teamai codebase --reconcile` | 将产品文档与提取的代码知识进行对账 | | `teamai codebase --lint` | 知识图谱健康检查 | | `teamai ci extract-mr --url ` | CI:从 MR 提取知识、发评论、合并后写入 | diff --git a/docs/usage-guide.md b/docs/usage-guide.md index a2e1cb49..6e9f9389 100644 --- a/docs/usage-guide.md +++ b/docs/usage-guide.md @@ -1159,6 +1159,9 @@ teamai codebase --extract /path/to/repo --project my-service # Incremental refresh: reuse the original repository path and project slug teamai codebase --extract /path/to/repo --project my-service --incremental +# Generate deep knowledge docs from extracted evidence (--output is the repository root) +teamai codebase --deep-enrich --project my-service --output /path/to/repo + # Reconcile teamwiki/product and teamwiki/docs with extracted code pages teamai codebase --reconcile --output /path/to/repo diff --git a/docs/usage-guide.zh-CN.md b/docs/usage-guide.zh-CN.md index 9c501bac..60db411f 100644 --- a/docs/usage-guide.zh-CN.md +++ b/docs/usage-guide.zh-CN.md @@ -1134,6 +1134,9 @@ teamai codebase --extract /path/to/repo --project my-service # 增量刷新:复用首次提取的仓库路径和项目名 teamai codebase --extract /path/to/repo --project my-service --incremental +# 从已提取的 evidence 生成深度知识文档(--output 指向仓库根目录) +teamai codebase --deep-enrich --project my-service --output /path/to/repo + # 将 teamwiki/product 和 teamwiki/docs 与提取的代码页面进行对账 teamai codebase --reconcile --output /path/to/repo diff --git a/skills/team-wiki-codebase/README.md b/skills/team-wiki-codebase/README.md index f4dfe520..861b4402 100644 --- a/skills/team-wiki-codebase/README.md +++ b/skills/team-wiki-codebase/README.md @@ -23,6 +23,7 @@ - 每次生成后有准确性统计,超标自动警告 - AI 读知识库而非读源码,**约 1/50 的 token 消耗**获得全局架构认知 - Phase 0 可用 `teamai codebase --extract` 生成可证据化的结构边(TS/JS/Python/Go AST + 多语言 heuristic) +- 提取后可用 `teamai codebase --deep-enrich --project --output ` 生成确定性图谱文档(G1/G2/G3)与深度知识;无需单独的 team-wiki CLI --- diff --git a/skills/team-wiki-codebase/SKILL.md b/skills/team-wiki-codebase/SKILL.md index 5f753c88..bf752c43 100644 --- a/skills/team-wiki-codebase/SKILL.md +++ b/skills/team-wiki-codebase/SKILL.md @@ -889,6 +889,7 @@ _review/ ← 过程文件(不入知识库) | 阶段 | 命令 / 路径 | |------|-------------| | Phase 0 结构基线 | `teamai codebase --extract --project `(writes `/teamwiki/`) | +| Deep knowledge | Use `teamai codebase --deep-enrich --project --output ` after extract has written `teamwiki/evidence/code//`. `--output` is the repository root, not the `teamwiki/` directory. Prefix with `teamai --dry-run` to preview without writing. TeamAI does not ship a separate team-wiki CLI. No extra plugin is required. | | K3 后编译进 wiki | Skip. TeamAI does not ship a separate team-wiki CLI. Continue with this skill using `teamai` and the files under this skill directory. No extra plugin is required. | | 产品文档入图 | Skip. Same English note as above. | | 产品↔代码桥接 | Use `teamai codebase --reconcile --output ` after product pages and extracted code pages are under `/teamwiki/`. Prefix with `teamai --dry-run` to preview without updating the graph. | diff --git a/src/__tests__/codebase-deep-enrich.test.ts b/src/__tests__/codebase-deep-enrich.test.ts new file mode 100644 index 00000000..9d9474c0 --- /dev/null +++ b/src/__tests__/codebase-deep-enrich.test.ts @@ -0,0 +1,134 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; + +// Mock the AI client so deepEnrich's LLM calls fail instantly instead of +// waiting on real 600s timeouts. deepEnrich treats these as non-blocking skips. +vi.mock('../utils/ai-client.js', () => ({ + getAICliName: () => 'mock-cli', + callClaude: vi.fn(async () => { + throw new Error('mock: AI unavailable'); + }), + callClaudeParallel: vi.fn(async () => { + throw new Error('mock: AI batch unavailable'); + }), +})); + +import { codebaseCmd } from '../codebase-cmd.js'; + +const temporaryDirectories: string[] = []; + +function createEnrichFixture(project = 'faketest'): string { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'teamai-deep-enrich-unit-')); + temporaryDirectories.push(root); + const evidenceDir = path.join(root, 'teamwiki', 'evidence', 'code', project); + fs.mkdirSync(evidenceDir, { recursive: true }); + fs.writeFileSync( + path.join(evidenceDir, '_manifest.json'), + JSON.stringify({ + schemaVersion: 'team-wiki.codebase-output-manifest.v2', + project, + generatedAt: '2026-01-01T00:00:00Z', + components: [ + { + slug: 'Auth', + docPath: `evidence/code/${project}/Auth.md`, + title: 'Auth', + category: 'component', + confidence: 'INFERRED', + responsibilities: ['Authenticate users'], + entrypoints: [], + }, + ], + edges: [{ from: 'Auth', to: 'Store', relation: 'DEPENDS_ON' }], + }, null, 2), + ); + return root; +} + +afterEach(() => { + process.exitCode = undefined; + vi.restoreAllMocks(); + for (const directory of temporaryDirectories.splice(0)) { + fs.rmSync(directory, { recursive: true, force: true }); + } +}); + +describe('codebase deep-enrich', () => { + it('sets a failing exit code when the requested output has no teamwiki', async () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'teamai-deep-enrich-missing-')); + temporaryDirectories.push(root); + vi.spyOn(console, 'log').mockImplementation(() => undefined); + + await codebaseCmd({ deepEnrich: true, project: 'faketest', output: root }); + + expect(process.exitCode).toBe(1); + }); + + it('lists deep-enrich in the handler help', async () => { + const log = vi.spyOn(console, 'log').mockImplementation(() => undefined); + + await codebaseCmd({}); + + expect(log.mock.calls.flat()).toContain( + ' teamai codebase --deep-enrich Generate deep knowledge from extracted evidence', + ); + }); + + it('writes deterministic graph docs from extracted evidence when AI is unavailable', async () => { + const root = createEnrichFixture(); + const log = vi.spyOn(console, 'log').mockImplementation(() => undefined); + + await codebaseCmd({ deepEnrich: true, project: 'faketest', output: root, json: true }); + + expect(process.exitCode).toBeUndefined(); + const report = JSON.parse(String(log.mock.calls.at(-1)?.[0])); + expect(report).toMatchObject({ project: 'faketest', complete: true }); + + const docsDir = path.join(root, 'teamwiki', 'evidence', 'code', 'faketest', 'docs'); + expect(fs.existsSync(path.join(docsDir, 'graph-g1-relations.md'))).toBe(true); + expect(fs.existsSync(path.join(docsDir, 'graph-g2-dataflow.md'))).toBe(true); + expect(fs.existsSync(path.join(docsDir, 'graph-g3-interfaces.md'))).toBe(true); + expect(fs.readFileSync(path.join(docsDir, 'graph-g1-relations.md'), 'utf8').length).toBeGreaterThan(0); + }); + + it('does not report success when the evidence dir has no components', async () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'teamai-deep-enrich-empty-')); + temporaryDirectories.push(root); + fs.mkdirSync(path.join(root, 'teamwiki', 'evidence', 'code', 'faketest'), { recursive: true }); + const log = vi.spyOn(console, 'log').mockImplementation(() => undefined); + + await codebaseCmd({ deepEnrich: true, project: 'faketest', output: root, json: true }); + + expect(process.exitCode).toBe(1); + expect(log.mock.calls.flat().join('\n')).toContain('No components in evidence'); + expect(fs.existsSync(path.join(root, 'teamwiki', 'evidence', 'code', 'faketest', 'docs'))).toBe(false); + }); + + it('previews without writing when --dry-run is set', async () => { + const root = createEnrichFixture(); + const log = vi.spyOn(console, 'log').mockImplementation(() => undefined); + + await codebaseCmd({ deepEnrich: true, project: 'faketest', output: root, json: true, dryRun: true }); + + expect(process.exitCode).toBeUndefined(); + expect(JSON.parse(String(log.mock.calls.at(-1)?.[0]))).toMatchObject({ + project: 'faketest', + dryRun: true, + }); + expect(fs.existsSync(path.join(root, 'teamwiki', 'evidence', 'code', 'faketest', 'docs'))).toBe(false); + }); + + it('rejects a project slug that escapes the evidence directory', async () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'teamai-deep-enrich-escape-')); + temporaryDirectories.push(root); + fs.mkdirSync(path.join(root, 'teamwiki'), { recursive: true }); + const log = vi.spyOn(console, 'log').mockImplementation(() => undefined); + + await codebaseCmd({ deepEnrich: true, project: '..', output: root }); + + expect(process.exitCode).toBe(1); + expect(log.mock.calls.flat().join('\n')).toMatch(/outside|traversal/i); + }); +}); diff --git a/src/__tests__/e2e/codebase-extract-cli.test.ts b/src/__tests__/e2e/codebase-extract-cli.test.ts index d479ab52..2cec24fb 100644 --- a/src/__tests__/e2e/codebase-extract-cli.test.ts +++ b/src/__tests__/e2e/codebase-extract-cli.test.ts @@ -192,3 +192,30 @@ describe('teamai codebase reconcile CLI (issue #360 slice 2)', () => { } }); }); + +describe('teamai codebase deep-enrich CLI (issue #360 slice 3)', () => { + it('lists --deep-enrich, documents it in the skill, and fails when teamwiki is missing', async () => { + const help = await runCLI(['codebase', '--help']); + expect(help.code, help.output).toBe(0); + expect(help.stdout).toContain('--deep-enrich'); + + const skill = fs.readFileSync(path.join(ROOT, 'skills/team-wiki-codebase/SKILL.md'), 'utf8'); + const command = [...skill.matchAll(/`(teamai codebase [^`]+)`/g)] + .map(match => match[1]) + .find(candidate => candidate.includes('--deep-enrich')); + expect(command).toBeDefined(); + + const fixture = fs.mkdtempSync(path.join(os.tmpdir(), 'teamai-deep-enrich-360-')); + try { + const missingRoot = path.join(fixture, 'missing'); + const enrichArgs = command!.split(/\s+/).slice(1) + .map(arg => arg === '' ? missingRoot : arg === '' ? 'slice360' : arg); + const missing = await runCLI(enrichArgs, fixture); + expect(missing.code, missing.output).toBe(1); + expect(missing.stdout).toContain('No teamwiki found'); + } finally { + fs.rmSync(fixture, { recursive: true, force: true }); + } + }); +}); + diff --git a/src/__tests__/index-codebase-help.test.ts b/src/__tests__/index-codebase-help.test.ts index acc03de2..21506d9b 100644 --- a/src/__tests__/index-codebase-help.test.ts +++ b/src/__tests__/index-codebase-help.test.ts @@ -8,19 +8,29 @@ afterEach(() => { vi.resetModules(); }); +async function readCodebaseHelp(): Promise { + let output = ''; + process.argv = ['node', 'teamai', 'codebase', '--help']; + vi.spyOn(process.stdout, 'write').mockImplementation((chunk) => { + output += String(chunk); + return true; + }); + vi.spyOn(process, 'exit').mockImplementation(((code?: string | number | null) => { + throw new Error(`process.exit(${code})`); + }) as never); + + await expect(import('../index.js')).rejects.toThrow('process.exit(0)'); + return output; +} + describe('codebase command registration', () => { it('lists the public reconcile option', async () => { - let output = ''; - process.argv = ['node', 'teamai', 'codebase', '--help']; - vi.spyOn(process.stdout, 'write').mockImplementation((chunk) => { - output += String(chunk); - return true; - }); - vi.spyOn(process, 'exit').mockImplementation(((code?: string | number | null) => { - throw new Error(`process.exit(${code})`); - }) as never); - - await expect(import('../index.js')).rejects.toThrow('process.exit(0)'); + const output = await readCodebaseHelp(); expect(output).toContain('--reconcile'); }); + + it('lists the public deep-enrich option', async () => { + const output = await readCodebaseHelp(); + expect(output).toContain('--deep-enrich'); + }); }); diff --git a/src/__tests__/team-wiki-codebase-skill.test.ts b/src/__tests__/team-wiki-codebase-skill.test.ts index 936e6222..df9184ca 100644 --- a/src/__tests__/team-wiki-codebase-skill.test.ts +++ b/src/__tests__/team-wiki-codebase-skill.test.ts @@ -34,6 +34,11 @@ describe('team-wiki-codebase builtin skill (issue #360 slice 1)', () => { } }); + it('tells agents to run teamai codebase --deep-enrich', () => { + const skill = fs.readFileSync(path.join(SKILL_DIR, 'SKILL.md'), 'utf8'); + expect(skill).toMatch(/`teamai codebase --deep-enrich --project --output `/); + }); + it('does not point at GRAPH-CAPABILITIES.md', () => { for (const file of SKILL_FILES) { const text = fs.readFileSync(file, 'utf8'); diff --git a/src/codebase-cmd.ts b/src/codebase-cmd.ts index 9920f8c6..84be8a27 100644 --- a/src/codebase-cmd.ts +++ b/src/codebase-cmd.ts @@ -21,6 +21,7 @@ export interface CodebaseCmdOptions extends GlobalOptions { maxFiles?: string; status?: boolean; reconcile?: boolean; + deepEnrich?: boolean; } // ─── Command handler ───────────────────────────────────────────────────────── @@ -56,7 +57,7 @@ export async function codebaseCmd(opts: CodebaseCmdOptions): Promise { return; } - if (!opts.lint && !opts.reconcile) { + if (!opts.lint && !opts.reconcile && !opts.deepEnrich) { console.log('teamai codebase — team codebase knowledge management'); console.log(''); console.log('Usage:'); @@ -66,6 +67,7 @@ export async function codebaseCmd(opts: CodebaseCmdOptions): Promise { console.log(' teamai codebase --lint --json Output JSON report (for CI)'); console.log(' teamai codebase --lint --severity high Only report high-severity issues'); console.log(' teamai codebase --reconcile Reconcile product and code knowledge'); + console.log(' teamai codebase --deep-enrich Generate deep knowledge from extracted evidence'); console.log(' teamai codebase --status Show knowledge-base git baseline'); return; } @@ -87,7 +89,61 @@ export async function codebaseCmd(opts: CodebaseCmdOptions): Promise { if (!(await pathExists(teamwikiDir))) { console.log('No teamwiki found. Run `teamai import` first.'); - if (opts.reconcile) process.exitCode = 1; + if (opts.reconcile || opts.deepEnrich) process.exitCode = 1; + return; + } + + if (opts.deepEnrich) { + const project = opts.project?.trim() ?? ''; + if (!project) { + console.log('Deep enrichment requires --project .'); + process.exitCode = 1; + return; + } + const { assertWithinRoot } = await import('./utils/path-safety.js'); + const codeRoot = path.join(teamwikiDir, 'evidence', 'code'); + const evidenceDir = path.join(codeRoot, project); + try { + assertWithinRoot(codeRoot, evidenceDir); + } catch (e) { + console.log((e as Error).message); + process.exitCode = 1; + return; + } + if (!(await pathExists(evidenceDir))) { + console.log(`No extracted evidence found for project "${project}". Run \`teamai codebase --extract\` first.`); + process.exitCode = 1; + return; + } + let componentCount = 0; + try { + const manifest = JSON.parse(await readFile(path.join(evidenceDir, '_manifest.json'), 'utf-8')) as { + components?: unknown; + }; + componentCount = Array.isArray(manifest.components) ? manifest.components.length : 0; + } catch { + componentCount = 0; + } + if (componentCount === 0) { + console.log(`No components in evidence for project "${project}". Run \`teamai codebase --extract\` first.`); + process.exitCode = 1; + return; + } + if (opts.dryRun) { + if (opts.json) { + console.log(JSON.stringify({ project, evidenceDir, dryRun: true }, null, 2)); + } else { + console.log(`Deep enrichment preview: project=${project} (no writes)`); + } + return; + } + const { deepEnrich } = await import('./deep-enrich.js'); + await deepEnrich({ project, evidenceDir, wikiRoot: teamwikiDir }); + if (opts.json) { + console.log(JSON.stringify({ project, evidenceDir, complete: true }, null, 2)); + } else { + console.log(`Deep enrichment complete: project=${project}`); + } return; } diff --git a/src/index.ts b/src/index.ts index 11a4fdf1..3ae58d82 100644 --- a/src/index.ts +++ b/src/index.ts @@ -915,11 +915,12 @@ program .description('Inspect and maintain team-codebase outputs') .option('--extract [path]', 'Extract code knowledge and build graph from source') .addOption(new Option('--incremental', 'Only re-extract changed files (requires prior manifest)').hideHelp()) - .addOption(new Option('--project ', 'Project slug for extract output (default: directory name)').hideHelp()) + .addOption(new Option('--project ', 'Project slug for --extract (defaults to directory name) and required for --deep-enrich').hideHelp()) .addOption(new Option('--max-files ', 'Max source files to scan (default: 200)').hideHelp()) .addOption(new Option('--upgrade-wiki', 'Migrate docs/team-codebase/ to teamwiki/ graph format').hideHelp()) .option('--lint', 'Run global consistency lint over the teamwiki knowledge graph') .option('--reconcile', 'Reconcile product and code knowledge in teamwiki') + .option('--deep-enrich', 'Generate deep knowledge docs from extracted evidence') .addOption(new Option('--fix', 'Deprecated: teamwiki lint has no autofix; runs lint in report-only mode').hideHelp()) .option('--status', 'Show knowledge-base git baseline (headSha / repoUrl / branch)') .addOption(new Option('--severity ', 'Minimum severity to report: high|medium|low|info').default('info').hideHelp())