Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/__tests__/auto-recall.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -615,14 +615,18 @@ describe('autoRecallFromInput', () => {
let tmpHome: string;
const originalHome = process.env.HOME;
const originalDisabled = process.env.TEAMAI_RECALL_DISABLED;
let cwdSpy: ReturnType<typeof vi.spyOn>;

beforeEach(() => {
tmpHome = makeTmpDir();
process.env.HOME = tmpHome;
// Mock cwd so autoDetectInit won't find the real project config
cwdSpy = vi.spyOn(process, 'cwd').mockReturnValue(tmpHome);
delete process.env.TEAMAI_RECALL_DISABLED;
});

afterEach(() => {
cwdSpy.mockRestore();
process.env.HOME = originalHome;
if (originalDisabled === undefined) {
delete process.env.TEAMAI_RECALL_DISABLED;
Expand Down
6 changes: 3 additions & 3 deletions src/__tests__/import-repo-merge.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ describe('importFromRepo — section merge', () => {
interactive: false,
});

const repoMdPath = path.join(workdir, 'docs', 'team-codebase', 'repos', 'github__owner__mergetest.md');
const repoMdPath = path.join(workdir, '.teamai', 'team-repo', 'docs', 'team-codebase', 'repos', 'github__owner__mergetest.md');
const exists = await fs.pathExists(repoMdPath);
expect(exists).toBe(true);

Expand All @@ -105,7 +105,7 @@ describe('importFromRepo — section merge', () => {
interactive: false,
});

const repoMdPath = path.join(workdir, 'docs', 'team-codebase', 'repos', 'github__owner__mergetest.md');
const repoMdPath = path.join(workdir, '.teamai', 'team-repo', 'docs', 'team-codebase', 'repos', 'github__owner__mergetest.md');
const stat1 = await fs.stat(repoMdPath);
const mtime1 = stat1.mtimeMs;

Expand Down Expand Up @@ -140,7 +140,7 @@ describe('importFromRepo — section merge', () => {
});

it('旧文件含未闭合锚点 → fallback 时备份旧文件、产物使用新 codebase', async () => {
const repoMdPath = path.join(workdir, 'docs', 'team-codebase', 'repos', 'github__owner__mergetest.md');
const repoMdPath = path.join(workdir, '.teamai', 'team-repo', 'docs', 'team-codebase', 'repos', 'github__owner__mergetest.md');
await fs.ensureDir(path.dirname(repoMdPath));

// 准备含未闭合锚点的旧文件
Expand Down
13 changes: 10 additions & 3 deletions src/codebase-cmd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,12 +97,19 @@ export async function codebaseCmd(opts: CodebaseCmdOptions): Promise<void> {
return;
}

// 若 teamwiki/ 存在,优先使用图谱 lint
// 若 teamwiki/ 存在(team-repo 内),优先使用图谱 lint
const { pathExists } = await import('./utils/fs.js');
const teamwikiDir = path.join(cwd, 'teamwiki');
let teamwikiDir: string;
try {
const { autoDetectInit } = await import('./config.js');
const { localConfig: lc } = await autoDetectInit();
teamwikiDir = path.join(lc.repo.localPath, 'teamwiki');
} catch {
teamwikiDir = path.join(cwd, '.teamai', 'team-repo', 'teamwiki');
}
if (await pathExists(teamwikiDir)) {
const { lintTeamwiki, formatWikiLintReport } = await import('./codebase-wiki-lint.js');
const report = await lintTeamwiki({ cwd, severity: opts.severity as 'high' | 'medium' | 'low' | 'info' });
const report = await lintTeamwiki({ wikiRoot: teamwikiDir, severity: opts.severity as 'high' | 'medium' | 'low' | 'info' });
if (opts.json) {
console.log(JSON.stringify(report, null, 2));
} else {
Expand Down
5 changes: 3 additions & 2 deletions src/codebase-wiki-lint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,11 @@ export interface WikiLintReport {
}

export async function lintTeamwiki(opts: {
cwd: string;
cwd?: string;
wikiRoot?: string;
severity?: WikiLintSeverity;
}): Promise<WikiLintReport> {
const wikiRoot = path.join(opts.cwd, 'teamwiki');
const wikiRoot = opts.wikiRoot ?? path.join(opts.cwd ?? process.cwd(), 'teamwiki');
const issues: WikiLintIssue[] = [];
const minSeverity = opts.severity ?? 'info';
const severityOrder: WikiLintSeverity[] = ['info', 'low', 'medium', 'high'];
Expand Down
24 changes: 13 additions & 11 deletions src/import-repo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export interface ImportFromRepoOptions {
explicitDomain?: string;
/** Dry-run 模式:跳过写盘但执行 clone+扫描 */
dryRun?: boolean;
/** 自定义产物根目录;默认 cwd/docs/team-codebase */
/** 自定义产物根目录;默认 .teamai/team-repo/docs/team-codebase */
output?: string;
/**
* 是否启用交互式确认。
Expand Down Expand Up @@ -527,7 +527,7 @@ export async function detectDomainDrift(args: {
* 1. 解析 url → provider + RepoInfo(owner/repo)
* 2. shallow clone(或增量 fetch+reset)到 ~/.teamai/cache/repos/<provider>/<owner>/<repo>
* 3. generateCodebaseMd({ repoPath: cacheDir })
* 4. 写出到 <outputRoot>/repos/<slug>.md(默认 outputRoot=cwd/docs/team-codebase)
* 4. 写出到 <outputRoot>/repos/<slug>.md(默认 outputRoot=.teamai/team-repo/docs/team-codebase)
* 5. 推荐业务域(或使用 --domain 显式指定)
* 6. 写入 .teamai/domains.yaml + appendHistory
* 7. 写 LAST_SYNC
Expand Down Expand Up @@ -642,8 +642,18 @@ export async function importFromRepo(opts: ImportFromRepoOptions): Promise<void>
}
}

// Resolve team-repo directory (needed for both docs/team-codebase and teamwiki)
let teamRepoDir: string;
try {
const { autoDetectInit } = await import('./config.js');
const { localConfig: lc } = await autoDetectInit();
teamRepoDir = lc.repo.localPath;
} catch {
teamRepoDir = path.join(process.cwd(), '.teamai', 'team-repo');
}

// 4. 写入 docs/team-codebase 叙事文档(AI 扫描成功时)
const outputRoot = output ?? path.join(process.cwd(), 'docs', 'team-codebase');
const outputRoot = output ?? path.join(teamRepoDir, 'docs', 'team-codebase');
let repoMdPath = path.join(outputRoot, 'repos', `${slug}.md`);

if (codebaseMd) {
Expand Down Expand Up @@ -720,14 +730,6 @@ export async function importFromRepo(opts: ImportFromRepoOptions): Promise<void>
} // end if (codebaseMd)

// 4b. 生成 teamwiki/ 知识图谱产物(写入 team-repo 以便自动 push)
let teamRepoDir: string;
try {
const { autoDetectInit } = await import('./config.js');
const { localConfig: lc } = await autoDetectInit();
teamRepoDir = lc.repo.localPath;
} catch {
teamRepoDir = path.join(process.cwd(), '.teamai', 'team-repo');
}
const teamwikiRoot = output
? path.resolve(output, '..', 'teamwiki')
: path.join(teamRepoDir, 'teamwiki');
Expand Down
27 changes: 2 additions & 25 deletions src/pull.ts
Original file line number Diff line number Diff line change
Expand Up @@ -618,28 +618,7 @@ async function pullForScope(
}
}

// Sync teamwiki/ directory (codebase knowledge graph)
const teamwikiRepoDir = path.join(localConfig.repo.localPath, 'teamwiki');
if (await pathExists(teamwikiRepoDir)) {
const syncTarget = localConfig.projectRoot ?? process.cwd();
const localTeamwikiDir = path.join(syncTarget, 'teamwiki');
// 检查本地 graph-index 是否比远端更新(避免覆盖未推送的本地产物)
const localGraph = path.join(localTeamwikiDir, '.indices', 'graph-index.json');
const remoteGraph = path.join(teamwikiRepoDir, '.indices', 'graph-index.json');
let shouldSync = true;
if (await pathExists(localGraph) && await pathExists(remoteGraph)) {
const localStat = await fse.stat(localGraph);
const remoteStat = await fse.stat(remoteGraph);
if (localStat.mtimeMs > remoteStat.mtimeMs) {
log.warn(`[${scopeLabel}] 本地 teamwiki/ 比远端更新,跳过覆盖(请先 teamai push)`);
shouldSync = false;
}
}
if (shouldSync) {
await fse.copy(teamwikiRepoDir, localTeamwikiDir, { overwrite: true });
log.debug(`[${scopeLabel}] Synced teamwiki/ knowledge graph`);
}
}
// teamwiki/ stays inside .teamai/team-repo/ — no copy to project root

// Build the index when ANY of the four categories has content.
const hasAnySource =
Expand All @@ -649,10 +628,8 @@ async function pullForScope(
await pathExists(skillsRepoDir);

// Resolve codebase directory (project cwd or team repo)
const cwdCodebaseDir = path.join(process.cwd(), 'docs', 'team-codebase');
const repoCodebaseDir = path.join(localConfig.repo.localPath, 'docs', 'team-codebase');
const effectiveCodebaseDir = await pathExists(cwdCodebaseDir) ? cwdCodebaseDir
: await pathExists(repoCodebaseDir) ? repoCodebaseDir : undefined;
const effectiveCodebaseDir = await pathExists(repoCodebaseDir) ? repoCodebaseDir : undefined;

if (hasAnySource || effectiveCodebaseDir) {
const votesExist = await pathExists(votesDir);
Expand Down
12 changes: 7 additions & 5 deletions src/recall.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,10 +188,8 @@ async function loadOrBuildScopeIndex(
const docsDir = path.join(localConfig.repo.localPath, 'docs');
const rulesDir = path.join(localConfig.repo.localPath, 'rules');
const skillsDir = path.join(localConfig.repo.localPath, 'skills');
const cwdCodebaseDir = path.join(process.cwd(), 'docs', 'team-codebase');
const repoCodebaseDir = path.join(localConfig.repo.localPath, 'docs', 'team-codebase');
const codebaseDir = await pathExists(cwdCodebaseDir) ? cwdCodebaseDir
: await pathExists(repoCodebaseDir) ? repoCodebaseDir : undefined;
const codebaseDir = await pathExists(repoCodebaseDir) ? repoCodebaseDir : undefined;
try {
await buildIndex({
learningsDir: effectiveLearningsDir ?? undefined,
Expand Down Expand Up @@ -259,7 +257,12 @@ export async function recall(
log.debug('recall: user scope not available');
}

const hasWiki = await pathExists(path.join(process.cwd(), 'teamwiki'));
// Resolve teamwiki path from team-repo (prefer project scope, fallback to user scope)
const wikiConfig = scopeIndexes[0]?.config;
const wikiRoot = wikiConfig
? path.join(wikiConfig.repo.localPath, 'teamwiki')
: path.join(process.cwd(), '.teamai', 'team-repo', 'teamwiki');
const hasWiki = await pathExists(wikiRoot);
if (scopeIndexes.length === 0 && !hasWiki) {
log.info('No learnings available. Run `teamai pull` first to sync team knowledge.');
return;
Expand All @@ -281,7 +284,6 @@ export async function recall(
}

// ── Codebase knowledge graph recall ──────────────────────
const wikiRoot = path.join(process.cwd(), 'teamwiki');
try {
const codeResults = await queryCodeKnowledge(query, { wikiRoot, limit: 3, depth: options.depth });
// B11: Normalize BM25 scores to 0-10 range before merging with learnings scores
Expand Down
2 changes: 1 addition & 1 deletion src/utils/team-codebase-paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export const TEAM_CODEBASE_DIR = 'team-codebase';

/** 团队 codebase 各层路径集合。 */
export interface TeamCodebasePaths {
/** <cwd>/docs/team-codebase */
/** .teamai/team-repo/docs/team-codebase (or custom --output path) */
root: string;
/** <root>/index.md */
index: string;
Expand Down
Loading