feat: 新增 assembleCreatePrompt,plan/build/archive 不再内嵌文件内容
This commit is contained in:
@@ -16,6 +16,15 @@ export function assembleDiscussPrompt(config: RuneConfig): string {
|
||||
return applyCommandPrefix(discuss.prompt, config);
|
||||
}
|
||||
|
||||
export function assembleCreatePrompt(config: RuneConfig): string {
|
||||
const create = config.stages.create;
|
||||
if (!create)
|
||||
throw new CommandError("创建阶段未配置", {
|
||||
hint: "请在 .rune/config.yaml 中配置 stages.create",
|
||||
});
|
||||
return applyCommandPrefix(create.prompt, config);
|
||||
}
|
||||
|
||||
export async function assemblePlanPrompt(
|
||||
config: RuneConfig,
|
||||
projectRoot: string,
|
||||
@@ -45,8 +54,7 @@ export async function assemblePlanPrompt(
|
||||
|
||||
const docPath = join(changeDir, `${doc.name}.md`);
|
||||
if (existsSync(docPath)) {
|
||||
const existing = await readFile(docPath, "utf-8");
|
||||
parts.push(`\n### 已有内容(请在此基础上修订):\n${existing}`);
|
||||
parts.push(`\n文档已存在,请先读取 ${docPath} 查看已有内容,在此基础上修订。`);
|
||||
}
|
||||
|
||||
if (doc.template) {
|
||||
@@ -114,13 +122,9 @@ export async function assembleBuildPrompt(
|
||||
const parts: string[] = [];
|
||||
parts.push(`# 构建阶段:${changeName}\n`);
|
||||
parts.push(build.prompt);
|
||||
parts.push(`\n## 任务列表\n`);
|
||||
parts.push(taskContent);
|
||||
parts.push(`\n## 待执行任务(共 ${pendingTasks.length} 项)`);
|
||||
for (const task of pendingTasks) {
|
||||
parts.push(`- [ ] ${task.text}`);
|
||||
}
|
||||
parts.push(`\n请从第一个待执行任务开始。完成后更新 ${taskPath} 中的 checkbox。`);
|
||||
parts.push(`\n请先读取 ${taskPath} 查看任务列表。`);
|
||||
parts.push(`当前有 ${pendingTasks.length} 个待执行任务,从第一个未完成的任务开始。`);
|
||||
parts.push(`完成后更新 ${taskPath} 中的 checkbox。`);
|
||||
|
||||
return applyCommandPrefix(parts.join("\n"), config);
|
||||
}
|
||||
@@ -142,23 +146,21 @@ export async function assembleArchivePrompt(
|
||||
if (config.metadata?.tracked) {
|
||||
const changeDir = getChangeDir(projectRoot, changeName);
|
||||
const taskPath = join(changeDir, "task.md");
|
||||
try {
|
||||
const taskContent = await readFile(taskPath, "utf-8");
|
||||
const tasks = parseTasks(taskContent);
|
||||
const incompleteTasks = tasks.filter((t) => !t.checked);
|
||||
if (incompleteTasks.length > 0) {
|
||||
parts.push("## ⚠️ 警告:存在未完成的任务\n");
|
||||
parts.push(`以下 ${incompleteTasks.length} 个任务尚未完成:`);
|
||||
for (const t of incompleteTasks) {
|
||||
parts.push(`- [ ] ${t.text}`);
|
||||
if (existsSync(taskPath)) {
|
||||
try {
|
||||
const taskContent = await readFile(taskPath, "utf-8");
|
||||
const tasks = parseTasks(taskContent);
|
||||
const incompleteTasks = tasks.filter((t) => !t.checked);
|
||||
if (incompleteTasks.length > 0) {
|
||||
parts.push("## ⚠️ 警告:存在未完成的任务\n");
|
||||
parts.push(`请先读取 ${taskPath} 检查是否有未完成的任务。`);
|
||||
parts.push("如有未完成任务,询问用户是否确认在任务未全部完成的情况下归档。");
|
||||
parts.push("如用户确认,则继续归档;否则中止并返回构建阶段。");
|
||||
parts.push("");
|
||||
}
|
||||
parts.push("");
|
||||
parts.push("请询问用户是否确认在任务未全部完成的情况下归档。");
|
||||
parts.push("如用户确认,则继续归档;否则中止并返回构建阶段。");
|
||||
parts.push("");
|
||||
} catch {
|
||||
// task.md 读取失败时不追加警告
|
||||
}
|
||||
} catch {
|
||||
// task.md 不存在时不追加警告
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,12 +3,14 @@ import { mkdir, writeFile, rm } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import {
|
||||
assembleDiscussPrompt,
|
||||
assembleCreatePrompt,
|
||||
assemblePlanPrompt,
|
||||
assembleBuildPrompt,
|
||||
assembleArchivePrompt,
|
||||
} from "../../src/core/assembler.ts";
|
||||
import type { RuneConfig } from "../../src/types.ts";
|
||||
import { defaultConfig } from "../../src/defaults/config.ts";
|
||||
import { CommandError } from "../../src/cli/errors.ts";
|
||||
|
||||
const TMP_DIR = join(import.meta.dir, "__tmp_assembler_test__");
|
||||
|
||||
@@ -38,6 +40,35 @@ describe("assembleDiscussPrompt", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("assembleCreatePrompt", () => {
|
||||
it("返回默认 create 提示词", () => {
|
||||
const prompt = assembleCreatePrompt(defaultConfig);
|
||||
expect(prompt).toBeTruthy();
|
||||
expect(prompt).toContain("变更名称");
|
||||
expect(prompt).toContain("/rune-plan");
|
||||
});
|
||||
|
||||
it("返回自定义 create 提示词", () => {
|
||||
const config: RuneConfig = {
|
||||
stages: { create: { prompt: "自定义创建" } },
|
||||
};
|
||||
const prompt = assembleCreatePrompt(config);
|
||||
expect(prompt).toBe("自定义创建");
|
||||
});
|
||||
|
||||
it("create 阶段未配置时抛出 CommandError", () => {
|
||||
const config: RuneConfig = {
|
||||
stages: { build: { prompt: "构建" } },
|
||||
};
|
||||
try {
|
||||
assembleCreatePrompt(config);
|
||||
expect.unreachable();
|
||||
} catch (e) {
|
||||
expect(e).toBeInstanceOf(CommandError);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("assemblePlanPrompt", () => {
|
||||
it("包含指定文档名称和提示词", async () => {
|
||||
const prompt = await assemblePlanPrompt(defaultConfig, TMP_DIR, "user-auth", "design");
|
||||
@@ -46,13 +77,14 @@ describe("assemblePlanPrompt", () => {
|
||||
expect(prompt).not.toContain("task");
|
||||
});
|
||||
|
||||
it("包含已有文档内容(重复 plan 场景)", async () => {
|
||||
it("已有文档时引导 AI 读取而非内嵌内容", async () => {
|
||||
const changeDir = join(TMP_DIR, ".rune", "changes", "user-auth");
|
||||
await mkdir(changeDir, { recursive: true });
|
||||
await writeFile(join(changeDir, "design.md"), "# 已有设计");
|
||||
const prompt = await assemblePlanPrompt(defaultConfig, TMP_DIR, "user-auth", "design");
|
||||
expect(prompt).toContain("已有设计");
|
||||
expect(prompt).toContain("在此基础上修订");
|
||||
expect(prompt).toContain("已有内容");
|
||||
expect(prompt).toContain("design.md");
|
||||
expect(prompt).not.toContain("# 已有设计");
|
||||
});
|
||||
|
||||
it("替换模板中的 {{change-name}}", async () => {
|
||||
@@ -123,14 +155,15 @@ describe("assemblePlanPrompt", () => {
|
||||
});
|
||||
|
||||
describe("assembleBuildPrompt", () => {
|
||||
it("包含待执行任务列表", async () => {
|
||||
it("引导 AI 读取 task.md 而非内嵌任务内容", async () => {
|
||||
const changeDir = join(TMP_DIR, ".rune", "changes", "user-auth");
|
||||
await mkdir(changeDir, { recursive: true });
|
||||
await writeFile(join(changeDir, "task.md"), `- [x] 任务一\n- [ ] 任务二\n- [ ] 任务三`);
|
||||
const prompt = await assembleBuildPrompt(defaultConfig, TMP_DIR, "user-auth");
|
||||
expect(prompt).toContain("任务二");
|
||||
expect(prompt).toContain("待执行任务");
|
||||
expect(prompt).toContain("共 2 项");
|
||||
expect(prompt).toContain("task.md");
|
||||
expect(prompt).toContain("2");
|
||||
expect(prompt).not.toContain("任务二");
|
||||
expect(prompt).not.toContain("任务三");
|
||||
});
|
||||
|
||||
it("所有任务完成时提示可归档", async () => {
|
||||
@@ -218,7 +251,7 @@ describe("assembleArchivePrompt", () => {
|
||||
expect(prompt).not.toContain("未完成");
|
||||
});
|
||||
|
||||
it("tracked=true 时读取 task.md 并注入未完成任务警告", async () => {
|
||||
it("tracked=true 时引导 AI 检查 task.md 而非内嵌任务内容", async () => {
|
||||
const changeDir = join(TMP_DIR, ".rune", "changes", "user-auth");
|
||||
await mkdir(changeDir, { recursive: true });
|
||||
await writeFile(join(changeDir, "task.md"), "- [ ] 未完成任务");
|
||||
@@ -227,8 +260,9 @@ describe("assembleArchivePrompt", () => {
|
||||
metadata: { tracked: true },
|
||||
};
|
||||
const prompt = await assembleArchivePrompt(config, TMP_DIR, "user-auth");
|
||||
expect(prompt).toContain("task.md");
|
||||
expect(prompt).toContain("未完成");
|
||||
expect(prompt).toContain("未完成任务");
|
||||
expect(prompt).not.toContain("- [ ] 未完成任务");
|
||||
});
|
||||
|
||||
it("tracked=true 且所有任务完成时不注入警告", async () => {
|
||||
|
||||
@@ -50,8 +50,8 @@ describe("完整 SDD 流程", () => {
|
||||
expect(changes[0].documents.length).toBeGreaterThan(0);
|
||||
|
||||
const buildPrompt = await assembleBuildPrompt(config, TMP_DIR, changeName);
|
||||
expect(buildPrompt).toContain("实现登录 API");
|
||||
expect(buildPrompt).toContain("共 2 项");
|
||||
expect(buildPrompt).toContain("task.md");
|
||||
expect(buildPrompt).toContain("2");
|
||||
|
||||
await writeFile(join(changeDir, "task.md"), "- [x] 实现登录 API\n- [x] 编写登录测试");
|
||||
|
||||
@@ -95,10 +95,10 @@ describe("完整 SDD 流程", () => {
|
||||
expect(changes).toHaveLength(2);
|
||||
|
||||
const authPrompt = await assembleBuildPrompt(config, TMP_DIR, "auth");
|
||||
expect(authPrompt).toContain("auth 任务");
|
||||
expect(authPrompt).toContain("task.md");
|
||||
|
||||
const paymentPrompt = await assembleBuildPrompt(config, TMP_DIR, "payment");
|
||||
expect(paymentPrompt).toContain("payment 任务");
|
||||
expect(paymentPrompt).toContain("task.md");
|
||||
});
|
||||
|
||||
it("自定义配置覆盖默认配置", async () => {
|
||||
@@ -189,7 +189,9 @@ describe("archive 校验", () => {
|
||||
|
||||
const prompt = await assembleArchivePrompt(config, TMP_DIR, "incomplete-task");
|
||||
expect(prompt).toContain("警告");
|
||||
expect(prompt).toContain("未完成任务");
|
||||
expect(prompt).toContain("task.md");
|
||||
expect(prompt).toContain("未完成");
|
||||
expect(prompt).not.toContain("- [ ] 未完成任务");
|
||||
expect(prompt).toContain("是否确认");
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user