mirror of
https://github.com/Tencent/teamai-cli.git
synced 2026-10-02 03:14:40 +08:00
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).
This commit is contained in:
@@ -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 <url>` | CI: extract knowledge from MR, post comments, write after merge |
|
||||
|
||||
@@ -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 <url>` | CI:从 MR 提取知识、发评论、合并后写入 |
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
- 每次生成后有准确性统计,超标自动警告
|
||||
- AI 读知识库而非读源码,**约 1/50 的 token 消耗**获得全局架构认知
|
||||
- Phase 0 可用 `teamai codebase --extract` 生成可证据化的结构边(TS/JS/Python/Go AST + 多语言 heuristic)
|
||||
- 提取后可用 `teamai codebase --deep-enrich --project <slug> --output <repo>` 生成确定性图谱文档(G1/G2/G3)与深度知识;无需单独的 team-wiki CLI
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -889,6 +889,7 @@ _review/ ← 过程文件(不入知识库)
|
||||
| 阶段 | 命令 / 路径 |
|
||||
|------|-------------|
|
||||
| Phase 0 结构基线 | `teamai codebase --extract <repo> --project <slug>`(writes `<repo>/teamwiki/`) |
|
||||
| Deep knowledge | Use `teamai codebase --deep-enrich --project <slug> --output <repo>` after extract has written `teamwiki/evidence/code/<slug>/`. `--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 <repo>` after product pages and extracted code pages are under `<repo>/teamwiki/`. Prefix with `teamai --dry-run` to preview without updating the graph. |
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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 === '<repo>' ? missingRoot : arg === '<slug>' ? '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 });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -8,19 +8,29 @@ afterEach(() => {
|
||||
vi.resetModules();
|
||||
});
|
||||
|
||||
async function readCodebaseHelp(): Promise<string> {
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 <slug> --output <repo>`/);
|
||||
});
|
||||
|
||||
it('does not point at GRAPH-CAPABILITIES.md', () => {
|
||||
for (const file of SKILL_FILES) {
|
||||
const text = fs.readFileSync(file, 'utf8');
|
||||
|
||||
+58
-2
@@ -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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
|
||||
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 <slug>.');
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -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 <name>', 'Project slug for extract output (default: directory name)').hideHelp())
|
||||
.addOption(new Option('--project <name>', 'Project slug for --extract (defaults to directory name) and required for --deep-enrich').hideHelp())
|
||||
.addOption(new Option('--max-files <n>', '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 <level>', 'Minimum severity to report: high|medium|low|info').default('info').hideHelp())
|
||||
|
||||
Reference in New Issue
Block a user