feat: scanChanges 扩展返回 DocumentStatus、planCompleted、buildUnlocked
This commit is contained in:
@@ -1,14 +1,16 @@
|
|||||||
import { readdir, stat, readFile } from "node:fs/promises";
|
import { readdir, stat, readFile } from "node:fs/promises";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import type { ChangeStatus } from "../types.ts";
|
import type { ChangeStatus, DocumentStatus, RuneConfig } from "../types.ts";
|
||||||
import { getChangesDir, getArchiveDir } from "./config.ts";
|
import { getChangesDir, getArchiveDir } from "./config.ts";
|
||||||
import { parseTasks } from "./task-parser.ts";
|
import { parseTasks } from "./task-parser.ts";
|
||||||
|
|
||||||
export async function scanChanges(
|
export async function scanChanges(
|
||||||
projectRoot: string,
|
projectRoot: string,
|
||||||
|
config?: RuneConfig,
|
||||||
): Promise<ChangeStatus[]> {
|
): Promise<ChangeStatus[]> {
|
||||||
const changesDir = getChangesDir(projectRoot);
|
const changesDir = getChangesDir(projectRoot);
|
||||||
const results: ChangeStatus[] = [];
|
const results: ChangeStatus[] = [];
|
||||||
|
const planDocs = config?.stages.plan?.documents;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const entries = await readdir(changesDir);
|
const entries = await readdir(changesDir);
|
||||||
@@ -17,11 +19,34 @@ export async function scanChanges(
|
|||||||
const entryStat = await stat(entryPath);
|
const entryStat = await stat(entryPath);
|
||||||
if (!entryStat.isDirectory()) continue;
|
if (!entryStat.isDirectory()) continue;
|
||||||
|
|
||||||
const docs = await readdir(entryPath);
|
const files = await readdir(entryPath);
|
||||||
const documents = docs.filter((d) => d.endsWith(".md"));
|
const mdFiles = new Set(files.filter((f) => f.endsWith(".md")));
|
||||||
|
|
||||||
|
let documents: DocumentStatus[];
|
||||||
|
|
||||||
|
if (planDocs) {
|
||||||
|
documents = planDocs.map((docConfig) => {
|
||||||
|
const fileName = `${docConfig.name}.md`;
|
||||||
|
const completed = mdFiles.has(fileName);
|
||||||
|
const deps = docConfig.depend ?? [];
|
||||||
|
const dependMet =
|
||||||
|
deps.length === 0 ||
|
||||||
|
deps.every((dep) => mdFiles.has(`${dep}.md`));
|
||||||
|
return { name: docConfig.name, completed, dependMet };
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
documents = Array.from(mdFiles).map((fileName) => ({
|
||||||
|
name: fileName.replace(/\.md$/, ""),
|
||||||
|
completed: true,
|
||||||
|
dependMet: true,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
const planCompleted = planDocs ? documents.every((d) => d.completed) : false;
|
||||||
|
const buildUnlocked = planCompleted;
|
||||||
|
|
||||||
let taskProgress: { completed: number; total: number } | null = null;
|
let taskProgress: { completed: number; total: number } | null = null;
|
||||||
const taskFile = docs.find((d) => d === "task.md");
|
const taskFile = files.find((d) => d === "task.md");
|
||||||
if (taskFile) {
|
if (taskFile) {
|
||||||
const content = await readFile(join(entryPath, taskFile), "utf-8");
|
const content = await readFile(join(entryPath, taskFile), "utf-8");
|
||||||
const tasks = parseTasks(content);
|
const tasks = parseTasks(content);
|
||||||
@@ -31,7 +56,13 @@ export async function scanChanges(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
results.push({ name: entry, documents, taskProgress });
|
results.push({
|
||||||
|
name: entry,
|
||||||
|
documents,
|
||||||
|
planCompleted,
|
||||||
|
buildUnlocked,
|
||||||
|
taskProgress,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { describe, it, expect, beforeEach, afterEach } from "bun:test";
|
|||||||
import { mkdir, writeFile, rm } from "node:fs/promises";
|
import { mkdir, writeFile, rm } from "node:fs/promises";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { scanChanges, scanArchives } from "../../src/core/scanner.ts";
|
import { scanChanges, scanArchives } from "../../src/core/scanner.ts";
|
||||||
|
import type { RuneConfig } from "../../src/types.ts";
|
||||||
|
|
||||||
const TMP_DIR = join(import.meta.dir, "__tmp_scanner_test__");
|
const TMP_DIR = join(import.meta.dir, "__tmp_scanner_test__");
|
||||||
|
|
||||||
@@ -32,8 +33,11 @@ describe("scanChanges", () => {
|
|||||||
const changes = await scanChanges(TMP_DIR);
|
const changes = await scanChanges(TMP_DIR);
|
||||||
expect(changes).toHaveLength(1);
|
expect(changes).toHaveLength(1);
|
||||||
expect(changes[0].name).toBe("user-auth");
|
expect(changes[0].name).toBe("user-auth");
|
||||||
expect(changes[0].documents).toContain("design.md");
|
const docNames = changes[0].documents.map((d) => `${d.name}.md`);
|
||||||
expect(changes[0].documents).toContain("task.md");
|
expect(docNames).toContain("design.md");
|
||||||
|
expect(docNames).toContain("task.md");
|
||||||
|
expect(changes[0].planCompleted).toBe(false);
|
||||||
|
expect(changes[0].buildUnlocked).toBe(false);
|
||||||
expect(changes[0].taskProgress).toEqual({ completed: 1, total: 2 });
|
expect(changes[0].taskProgress).toEqual({ completed: 1, total: 2 });
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -57,6 +61,93 @@ describe("scanChanges", () => {
|
|||||||
const changes = await scanChanges(TMP_DIR);
|
const changes = await scanChanges(TMP_DIR);
|
||||||
expect(changes).toHaveLength(2);
|
expect(changes).toHaveLength(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("返回 DocumentStatus 含 completed 和 dependMet", async () => {
|
||||||
|
const changesDir = join(TMP_DIR, ".rune", "changes");
|
||||||
|
await mkdir(join(changesDir, "user-auth"), { recursive: true });
|
||||||
|
await writeFile(join(changesDir, "user-auth", "design.md"), "# 设计");
|
||||||
|
|
||||||
|
const config: RuneConfig = {
|
||||||
|
stages: {
|
||||||
|
plan: {
|
||||||
|
documents: [
|
||||||
|
{ name: "design", prompt: "生成设计" },
|
||||||
|
{ name: "task", prompt: "生成任务", depend: ["design"] },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const changes = await scanChanges(TMP_DIR, config);
|
||||||
|
expect(changes).toHaveLength(1);
|
||||||
|
expect(changes[0].documents).toHaveLength(2);
|
||||||
|
expect(changes[0].documents[0]).toEqual({
|
||||||
|
name: "design",
|
||||||
|
completed: true,
|
||||||
|
dependMet: true,
|
||||||
|
});
|
||||||
|
expect(changes[0].documents[1]).toEqual({
|
||||||
|
name: "task",
|
||||||
|
completed: false,
|
||||||
|
dependMet: true,
|
||||||
|
});
|
||||||
|
expect(changes[0].planCompleted).toBe(false);
|
||||||
|
expect(changes[0].buildUnlocked).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("depend 未满足时 dependMet 为 false", async () => {
|
||||||
|
const changesDir = join(TMP_DIR, ".rune", "changes");
|
||||||
|
await mkdir(join(changesDir, "user-auth"), { recursive: true });
|
||||||
|
|
||||||
|
const config: RuneConfig = {
|
||||||
|
stages: {
|
||||||
|
plan: {
|
||||||
|
documents: [
|
||||||
|
{ name: "design", prompt: "生成设计" },
|
||||||
|
{ name: "task", prompt: "生成任务", depend: ["design"] },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const changes = await scanChanges(TMP_DIR, config);
|
||||||
|
expect(changes[0].documents[1].dependMet).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("所有文档完成时 planCompleted 和 buildUnlocked 为 true", async () => {
|
||||||
|
const changesDir = join(TMP_DIR, ".rune", "changes");
|
||||||
|
await mkdir(join(changesDir, "user-auth"), { recursive: true });
|
||||||
|
await writeFile(join(changesDir, "user-auth", "design.md"), "# 设计");
|
||||||
|
await writeFile(join(changesDir, "user-auth", "task.md"), "- [ ] 任务");
|
||||||
|
|
||||||
|
const config: RuneConfig = {
|
||||||
|
stages: {
|
||||||
|
plan: {
|
||||||
|
documents: [
|
||||||
|
{ name: "design", prompt: "生成设计" },
|
||||||
|
{ name: "task", prompt: "生成任务" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const changes = await scanChanges(TMP_DIR, config);
|
||||||
|
expect(changes[0].planCompleted).toBe(true);
|
||||||
|
expect(changes[0].buildUnlocked).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("无 config 时使用文件扫描兼容模式", async () => {
|
||||||
|
const changesDir = join(TMP_DIR, ".rune", "changes");
|
||||||
|
await mkdir(join(changesDir, "feature-a"), { recursive: true });
|
||||||
|
await writeFile(join(changesDir, "feature-a", "design.md"), "# 设计");
|
||||||
|
|
||||||
|
const changes = await scanChanges(TMP_DIR);
|
||||||
|
expect(changes).toHaveLength(1);
|
||||||
|
expect(changes[0].documents[0].name).toBe("design");
|
||||||
|
expect(changes[0].documents[0].completed).toBe(true);
|
||||||
|
expect(changes[0].planCompleted).toBe(false);
|
||||||
|
expect(changes[0].buildUnlocked).toBe(false);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("scanArchives", () => {
|
describe("scanArchives", () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user