zizi 091b66a9bb 重构: 收敛 Agent/Skill 运行时与创作质量闭环
将角色与 Skill 从 .claude 迁入 .agent,移除 Claude CLI 运行时并接入固定 Opus 角色 profile、完整 schema、预算 deadline、raw 与回执证据链。

同步拆分 Skill 职责、复利 lesson、Gate 回放、Dashboard 人审入口、数据库登记和机械门禁;候选设计正文不包含在本提交中。
2026-08-22 02:12:32 +08:00

125 lines
4.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""前期设计候选校验器的离线测试:只覆盖机械门禁自身的正确性。"""
import json
import sys
from pathlib import Path
import tempfile
import unittest
PROJECT_ROOT = Path(__file__).resolve().parents[3]
SCRIPT_DIR = PROJECT_ROOT / ".agent" / "skills" / "design-story-foundation" / "scripts"
sys.path.insert(0, str(SCRIPT_DIR))
from validate_candidates import ValidationConfig, validate_candidates, validate_root # noqa: E402
TREE = json.loads((SCRIPT_DIR / "candidate_tree.json").read_text(encoding="utf-8"))
BLOCK = "> 回答:本节回答什么。\n> 应写:本节写什么。\n> 不写:本节不写什么。\n> 关联:其他节。\n"
ITEM = "- **S{sid:03d}|条目:** 这是带条件、代价和剧情用途的完整设定内容。"
def chapter_bands() -> list[tuple[int, int]]:
"""按每章实际条目数切分号段,保证全局连续编号都落在本章号段内。"""
bands = []
cursor = 1
for chapter in TREE["chapters"]:
count = sum(len(section["units"]) for section in chapter["sections"])
bands.append((cursor, cursor + count - 1))
cursor += count
return bands
BANDS = chapter_bands()
def make_candidate() -> str:
parts = ["# 候选\n\n## 目录\n\n- 章节列表\n"]
counter = 1
for index, chapter in enumerate(TREE["chapters"]):
start, end = BANDS[index]
parts.append(f"## {chapter['heading']}\n\n<!-- S{start:03d}-S{end:03d} -->\n")
for section in chapter["sections"]:
parts.append(f"### {section['heading']}\n\n{BLOCK}")
for unit in section["units"]:
parts.append(f"### {unit}\n\n{BLOCK}")
parts.append(ITEM.format(sid=counter))
counter += 1
return "\n".join(parts)
class CandidateValidatorTest(unittest.TestCase):
def setUp(self) -> None:
self.temp_dir = tempfile.TemporaryDirectory()
self.root = Path(self.temp_dir.name)
self.config = ValidationConfig(min_candidates=2, min_item_chars=10)
def tearDown(self) -> None:
self.temp_dir.cleanup()
def write(self, name: str, content: str) -> Path:
path = self.root / name
path.write_text(content, encoding="utf-8")
return path
def codes(self, *contents: str) -> set[str]:
paths = [self.write(f"c{index}.md", content) for index, content in enumerate(contents)]
_, problems = validate_candidates(paths, self.config)
return {problem.code for problem in problems}
def test_two_same_shape_candidates_pass(self) -> None:
self.assertEqual(set(), self.codes(make_candidate(), make_candidate()))
def test_missing_unit_heading_fails(self) -> None:
broken = make_candidate().replace("### 1.1.3 同类作品中的差异\n", "")
self.assertIn("TREE_MISMATCH", self.codes(make_candidate(), broken))
def test_deep_heading_fails(self) -> None:
broken = make_candidate().replace(
"### 1.1.1 题材、载体与目标读者\n", "### 1.1.1 题材、载体与目标读者\n\n#### 细分\n"
)
self.assertIn("CANDIDATE_STRUCTURE", self.codes(make_candidate(), broken))
def test_duplicate_and_out_of_range_ids_fail(self) -> None:
broken = make_candidate().replace("S002|条目:", "S001|条目:").replace("S003|条目:", "S002|条目:")
codes = self.codes(make_candidate(), broken)
self.assertIn("SETTING_ID_DUPLICATE", codes)
self.assertIn("SETTING_ORDER", codes)
def test_placeholder_fails(self) -> None:
broken = make_candidate().replace("完整设定内容", "TODO")
self.assertIn("CANDIDATE_PLACEHOLDER", self.codes(make_candidate(), broken))
def test_root_contract_accepts_clean_items(self) -> None:
root_doc = self.write(
"root.md",
"""# 前期设计
## 一、根设定
- **作品前提:**主角刚结束高考,随后进入拥有星际机甲的陌生时代并寻找立足之地。
- **披露要求:**前三章只呈现当前危机和能力征兆,终局答案留到后续阶段逐步揭开。
## 二、定盘
""",
)
self.assertEqual([], validate_root(root_doc, self.config))
def test_root_rejects_process_language(self) -> None:
root_doc = self.write(
"root.md",
"""# 前期设计
## 一、根设定
- **代理要求:**Claude 子代理分别生成候选文档,再由主会话比较和选择最终方向。
## 二、定盘
""",
)
self.assertIn("ROOT_PROCESS_LEAK", {problem.code for problem in validate_root(root_doc, self.config)})
if __name__ == "__main__":
unittest.main()