mirror of
https://github.com/Narcooo/inkos.git
synced 2026-05-07 05:51:26 +08:00
fix: replan when persisted runtime intent is invalid
This commit is contained in:
@@ -991,6 +991,88 @@ describe("PipelineRunner", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("re-plans instead of reusing a persisted invalid intent artifact in v2 mode", async () => {
|
||||
const { root, runner, state, bookId } = await createRunnerFixture({
|
||||
inputGovernanceMode: "v2",
|
||||
});
|
||||
const storyDir = join(state.bookDir(bookId), "story");
|
||||
const runtimeDir = join(storyDir, "runtime");
|
||||
await mkdir(runtimeDir, { recursive: true });
|
||||
|
||||
await Promise.all([
|
||||
writeFile(join(storyDir, "current_focus.md"), "# Current Focus\n\nBring focus back to the mentor conflict.\n", "utf-8"),
|
||||
writeFile(
|
||||
join(storyDir, "volume_outline.md"),
|
||||
[
|
||||
"# Volume Outline",
|
||||
"",
|
||||
"### Golden First Three Chapters Rule",
|
||||
"",
|
||||
"**Chapter 1:**",
|
||||
"Track the merchant guild trail.",
|
||||
"",
|
||||
].join("\n"),
|
||||
"utf-8",
|
||||
),
|
||||
writeFile(join(storyDir, "current_state.md"), "# Current State\n\n- Lin Yue still hides the broken oath token.\n", "utf-8"),
|
||||
writeFile(join(storyDir, "story_bible.md"), "# Story Bible\n\n- The jade seal cannot be destroyed.\n", "utf-8"),
|
||||
writeFile(join(storyDir, "pending_hooks.md"), "# Pending Hooks\n\n- Why the mentor vanished after the trial.\n", "utf-8"),
|
||||
writeFile(
|
||||
join(runtimeDir, "chapter-0001.intent.md"),
|
||||
[
|
||||
"# Chapter Intent",
|
||||
"",
|
||||
"## Goal",
|
||||
"**",
|
||||
"",
|
||||
"## Outline Node",
|
||||
"**",
|
||||
"",
|
||||
"## Must Keep",
|
||||
"- none",
|
||||
"",
|
||||
"## Must Avoid",
|
||||
"- none",
|
||||
"",
|
||||
"## Style Emphasis",
|
||||
"- none",
|
||||
"",
|
||||
"## Conflicts",
|
||||
"- none",
|
||||
"",
|
||||
].join("\n"),
|
||||
"utf-8",
|
||||
),
|
||||
]);
|
||||
|
||||
const planChapter = vi.spyOn(PlannerAgent.prototype, "planChapter");
|
||||
const writeChapter = vi.spyOn(WriterAgent.prototype, "writeChapter").mockResolvedValue(
|
||||
createWriterOutput({
|
||||
chapterNumber: 1,
|
||||
content: "Governed pipeline draft.",
|
||||
wordCount: "Governed pipeline draft.".length,
|
||||
}),
|
||||
);
|
||||
vi.spyOn(ContinuityAuditor.prototype, "auditChapter").mockResolvedValue(
|
||||
createAuditResult({
|
||||
passed: true,
|
||||
issues: [],
|
||||
summary: "clean",
|
||||
}),
|
||||
);
|
||||
|
||||
try {
|
||||
await runner.writeNextChapter(bookId, 220);
|
||||
|
||||
expect(planChapter).toHaveBeenCalledTimes(1);
|
||||
const writeInput = writeChapter.mock.calls[0]?.[0];
|
||||
expect(writeInput?.chapterIntent).toContain("Track the merchant guild trail.");
|
||||
expect(writeInput?.chapterIntent).not.toContain("\n**\n");
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("logs explicit stage messages during writeNextChapter", async () => {
|
||||
const { logger, infos } = createCaptureLogger();
|
||||
const { root, runner, state, bookId } = await createRunnerFixture({
|
||||
|
||||
@@ -2289,9 +2289,12 @@ ${matrix}`,
|
||||
const intentMarkdown = await readFile(runtimePath, "utf-8");
|
||||
const sections = this.parseIntentSections(intentMarkdown);
|
||||
const goal = this.readIntentScalar(sections, "Goal");
|
||||
if (!goal) return null;
|
||||
if (!goal || this.isInvalidPersistedIntentScalar(goal)) return null;
|
||||
|
||||
const outlineNode = this.readIntentScalar(sections, "Outline Node");
|
||||
if (outlineNode && outlineNode !== "(not found)" && this.isInvalidPersistedIntentScalar(outlineNode)) {
|
||||
return null;
|
||||
}
|
||||
const conflicts = this.readIntentList(sections, "Conflicts")
|
||||
.map((line) => {
|
||||
const separator = line.indexOf(":");
|
||||
@@ -2354,6 +2357,16 @@ ${matrix}`,
|
||||
.map((line) => line.replace(/^-\s*/, ""));
|
||||
}
|
||||
|
||||
private isInvalidPersistedIntentScalar(value: string): boolean {
|
||||
const normalized = value.trim();
|
||||
if (!normalized) return true;
|
||||
if (/^[*_`~::|.-]+$/.test(normalized)) return true;
|
||||
return (
|
||||
/^\((describe|briefly describe|write)\b[\s\S]*\)$/i.test(normalized)
|
||||
|| /^((?:在这里描述|描述|填写|写下)[\s\S]*)$/u.test(normalized)
|
||||
);
|
||||
}
|
||||
|
||||
private relativeToBookDir(bookDir: string, absolutePath: string): string {
|
||||
const prefix = `${bookDir}/`;
|
||||
return absolutePath.startsWith(prefix) ? absolutePath.slice(prefix.length) : absolutePath;
|
||||
|
||||
Reference in New Issue
Block a user