feat: assemblePlanPrompt 改为按单文档组装,增加依赖说明

This commit is contained in:
2026-06-09 10:49:02 +08:00
parent 1c7a8b3322
commit ee01bd87ab
4 changed files with 104 additions and 28 deletions

View File

@@ -74,8 +74,12 @@ cli.command("plan <change-name>", "规划阶段").action(
const root = requireProjectRoot(); const root = requireProjectRoot();
await mkdir(getChangeDir(root, changeName), { recursive: true }); await mkdir(getChangeDir(root, changeName), { recursive: true });
const config = await loadConfig(root); const config = await loadConfig(root);
const prompt = await assemblePlanPrompt(config, root, changeName); const plan = config.stages.plan;
console.log(prompt); if (!plan) throw new Error("plan 阶段未配置");
for (const doc of plan.documents) {
const prompt = await assemblePlanPrompt(config, root, changeName, doc.name);
console.log(prompt);
}
}, },
); );

View File

@@ -15,35 +15,51 @@ export async function assemblePlanPrompt(
config: RuneConfig, config: RuneConfig,
projectRoot: string, projectRoot: string,
changeName: string, changeName: string,
documentName: string,
): Promise<string> { ): Promise<string> {
const plan = config.stages.plan; const plan = config.stages.plan;
if (!plan) throw new Error("plan 阶段未配置"); if (!plan) throw new Error("plan 阶段未配置");
const doc = plan.documents.find((d) => d.name === documentName);
if (!doc) {
throw new Error(`文档 "${documentName}" 不在配置的 plan.documents 中`);
}
const changeDir = getChangeDir(projectRoot, changeName); const changeDir = getChangeDir(projectRoot, changeName);
const parts: string[] = []; const parts: string[] = [];
parts.push(`# 规划阶段:${changeName}\n`); parts.push(`# 规划阶段:${changeName}`);
parts.push("请为当前变更生成以下文档:\n"); parts.push("");
parts.push(`## 文档:${doc.name}.md`);
parts.push(doc.prompt);
for (const doc of plan.documents) { const docPath = join(changeDir, `${doc.name}.md`);
parts.push(`## 文档:${doc.name}.md`); if (existsSync(docPath)) {
parts.push(doc.prompt); const existing = await readFile(docPath, "utf-8");
parts.push(`\n### 已有内容(请在此基础上修订):\n${existing}`);
const docPath = join(changeDir, `${doc.name}.md`);
if (existsSync(docPath)) {
const existing = await readFile(docPath, "utf-8");
parts.push(`\n### 已有内容(请在此基础上修订):\n${existing}`);
}
if (doc.template) {
const rendered = doc.template.replace(/\{\{change-name\}\}/g, changeName);
parts.push(`\n### 格式模板:\n${rendered}`);
}
parts.push("");
} }
parts.push(`请将文档写入目录:${changeDir}`); if (doc.template) {
const rendered = doc.template.replace(/\{\{change-name\}\}/g, changeName);
parts.push(`\n### 格式模板:\n${rendered}`);
}
if (doc.depend && doc.depend.length > 0) {
parts.push("\n---\n");
parts.push("## 依赖说明\n");
parts.push("本文档依赖以下前置文档:");
for (const dep of doc.depend) {
const depPath = join(changeDir, `${dep}.md`);
const depCompleted = existsSync(depPath);
const status = depCompleted ? "已完成" : "未完成";
parts.push(`- ${dep}.md${status}`);
}
parts.push("\n请先阅读已完成的前置文档确保内容一致。");
}
parts.push(`\n请将文档写入目录${changeDir}`);
return parts.join("\n"); return parts.join("\n");
} }

View File

@@ -37,16 +37,16 @@ describe("assembleDiscussPrompt", () => {
}); });
describe("assemblePlanPrompt", () => { describe("assemblePlanPrompt", () => {
it("包含变更名称和文档指引", async () => { it("包含指定文档名称和提示词", async () => {
const prompt = await assemblePlanPrompt( const prompt = await assemblePlanPrompt(
defaultConfig, defaultConfig,
TMP_DIR, TMP_DIR,
"user-auth", "user-auth",
"design",
); );
expect(prompt).toContain("user-auth"); expect(prompt).toContain("user-auth");
expect(prompt).toContain("design"); expect(prompt).toContain("design");
expect(prompt).toContain("task"); expect(prompt).not.toContain("task");
expect(prompt).toContain("格式模板");
}); });
it("包含已有文档内容(重复 plan 场景)", async () => { it("包含已有文档内容(重复 plan 场景)", async () => {
@@ -57,6 +57,7 @@ describe("assemblePlanPrompt", () => {
defaultConfig, defaultConfig,
TMP_DIR, TMP_DIR,
"user-auth", "user-auth",
"design",
); );
expect(prompt).toContain("已有设计"); expect(prompt).toContain("已有设计");
expect(prompt).toContain("在此基础上修订"); expect(prompt).toContain("在此基础上修订");
@@ -67,12 +68,67 @@ describe("assemblePlanPrompt", () => {
defaultConfig, defaultConfig,
TMP_DIR, TMP_DIR,
"user-auth", "user-auth",
"design",
); );
expect(prompt).toContain("user-auth 设计文档"); expect(prompt).toContain("user-auth 设计文档");
expect(prompt).toContain("user-auth 任务列表");
expect(prompt).not.toContain("{{change-name}}"); expect(prompt).not.toContain("{{change-name}}");
}); });
it("包含依赖说明(有依赖时)", async () => {
const config: RuneConfig = {
stages: {
plan: {
documents: [
{ name: "design", prompt: "生成设计" },
{ name: "task", prompt: "生成任务", depend: ["design"] },
],
},
},
};
const prompt = await assemblePlanPrompt(
config,
TMP_DIR,
"user-auth",
"task",
);
expect(prompt).toContain("依赖说明");
expect(prompt).toContain("design.md");
});
it("无依赖时不包含依赖说明", async () => {
const prompt = await assemblePlanPrompt(
defaultConfig,
TMP_DIR,
"user-auth",
"design",
);
expect(prompt).not.toContain("依赖说明");
});
it("依赖说明标注完成状态", async () => {
const changeDir = join(TMP_DIR, ".rune", "changes", "user-auth");
await mkdir(changeDir, { recursive: true });
await writeFile(join(changeDir, "design.md"), "# 设计");
const config: RuneConfig = {
stages: {
plan: {
documents: [
{ name: "design", prompt: "生成设计" },
{ name: "task", prompt: "生成任务", depend: ["design"] },
],
},
},
};
const prompt = await assemblePlanPrompt(
config,
TMP_DIR,
"user-auth",
"task",
);
expect(prompt).toContain("已完成");
});
it("使用自定义 plan 配置", async () => { it("使用自定义 plan 配置", async () => {
const config: RuneConfig = { const config: RuneConfig = {
stages: { stages: {
@@ -87,7 +143,7 @@ describe("assemblePlanPrompt", () => {
}, },
}, },
}; };
const prompt = await assemblePlanPrompt(config, TMP_DIR, "my-feature"); const prompt = await assemblePlanPrompt(config, TMP_DIR, "my-feature", "spec");
expect(prompt).toContain("spec"); expect(prompt).toContain("spec");
expect(prompt).toContain("my-feature 规格"); expect(prompt).toContain("my-feature 规格");
expect(prompt).not.toContain("design"); expect(prompt).not.toContain("design");

View File

@@ -34,7 +34,7 @@ describe("完整 SDD 流程", () => {
const changeName = "user-auth"; const changeName = "user-auth";
await mkdir(getChangeDir(TMP_DIR, changeName), { recursive: true }); await mkdir(getChangeDir(TMP_DIR, changeName), { recursive: true });
const planPrompt = await assemblePlanPrompt(config, TMP_DIR, changeName); const planPrompt = await assemblePlanPrompt(config, TMP_DIR, changeName, "design");
expect(planPrompt).toContain("user-auth"); expect(planPrompt).toContain("user-auth");
const changeDir = getChangeDir(TMP_DIR, changeName); const changeDir = getChangeDir(TMP_DIR, changeName);
@@ -118,7 +118,7 @@ describe("完整 SDD 流程", () => {
expect(discussPrompt).toBe("自定义讨论"); expect(discussPrompt).toBe("自定义讨论");
await mkdir(getChangeDir(TMP_DIR, "test"), { recursive: true }); await mkdir(getChangeDir(TMP_DIR, "test"), { recursive: true });
const planPrompt = await assemblePlanPrompt(config, TMP_DIR, "test"); const planPrompt = await assemblePlanPrompt(config, TMP_DIR, "test", "spec");
expect(planPrompt).toContain("spec"); expect(planPrompt).toContain("spec");
expect(planPrompt).toContain("test 规格"); expect(planPrompt).toContain("test 规格");
expect(planPrompt).not.toContain("design"); expect(planPrompt).not.toContain("design");