zizi b0bc7a8745 框架: 技能按动作-对象重组 + 先审后入创作闭环
一、技能重组(动作-对象命名)
- 旧目录 clean/confirm/continuation/db/detect/embed/… 重组为
  clean-book-text/decide-candidate/write-next-chapter/access-database/
  check-content-consistency/embed-knowledge/…(git 识别为 rename,内容保持)
- agents/*.md、AGENTS.md/CLAUDE.md 收编、example_skill 登记表同步新名

二、先审后入创作闭环(本次核心)
正文接受从"机械门一过就写正典"改为"机械门+语义审查双通过+用户批准+单事务原子提交",
DB 级兜底,编排层跳步即被硬拒。
- candidate_cas.py + example_candidate_cas(109):持久化 CAS 状态链
- fact_delta.py + example_fact_delta/example_fact_ledger(106):结构化事实增量,
  模型只提六型闭集增量+正文证据引文,仅用户批准的增量随正文同事务入账本
- projection_registry.py + example_projection_run(107):投影登记与恢复
- acceptance_state.py:接受前置实时状态重读
- lesson_registry.py + example_lesson(108):经验升格链,禁止自动升格
- DDL 105:example_candidate 增 semantic_status/semantic_report_sha256
- write_canonical.accept:语义兜底+同事务合并增量+登记投影;
  run_writer_pipeline/persist_writer_run/run_writer_semantic_detector/step2 接入全链
- claude_runtime:兼容新 CLI modelUsage 信息字段

三、审查修复(独立子代理四维审查后)
- 事实增量 propose→approve 翻态正道,不撞唯一键
- 冻结配置探针重刷(CLI 2.1.211→2.1.231 漂移),profileSha256/adapterVersion 再登记
- 可视化合同悬空路径/五六空间矛盾、 SoT 旧技能名漂移、行尾空白清理

测试:离线 65 套 + 真实库集成 5 套(CAS/接受故障注入/事实增量/投影/经验升格)+ 回放 79 项全绿。
创作内容(docs/design、生成正文 artifacts)按"框架与创作分开"未入本提交。
2026-08-14 10:24:08 +08:00

94 lines
3.2 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 argparse
from pathlib import Path
import tempfile
import unittest
from serial_merge import SerialMergeError, build_packet, parse_raw, validate_prefix
def candidate(name: str) -> str:
return f"""# {name}
## 目录
- 第一章
- 第二章
## 一、定位
<!-- S001-S050 -->
- **S001|{name}一:** 第一章来源内容足够长,用来验证前文章节不会混入当前来源区。
- **S002|{name}二:** 第一章第二条来源内容,同样只应该通过已审定统合前文出现。
## 二、世界
<!-- S051-S100 -->
- **S051|{name}三:** 第二章当前来源内容,应该进入发给新代理的比较输入包。
- **S052|{name}四:** 第二章另一条来源内容,用于确认五案当前章可以并列比较。
"""
class SerialMergeTest(unittest.TestCase):
def setUp(self) -> None:
self.temp = tempfile.TemporaryDirectory()
self.root = Path(self.temp.name)
def tearDown(self) -> None:
self.temp.cleanup()
def write(self, name: str, text: str) -> Path:
path = self.root / name
path.write_text(text, encoding="utf-8")
return path
def test_packet_contains_prior_and_only_current_source_sections(self) -> None:
root_doc = self.write(
"root.md",
"# 设计\n\n## 一、根设定\n\n- **前提:**这是一条足够长的根设定,用来验证输入包带着最高权威进入每个节点。\n\n## 二、其他\n",
)
sources = [self.write(f"c{index}.md", candidate(f"候选{index}")) for index in range(1, 3)]
integrated = self.write(
"integrated.md",
"# 统合\n\n## 目录\n\n- 第一章\n- 第二章\n\n## 一、定位\n\n<!-- S001-S050 -->\n\n- **S001|已定:** 这是主会话审定后的第一章,不是任何来源草案。\n",
)
output = self.root / "packet.md"
build_packet(
argparse.Namespace(
root_doc=root_doc,
integrated_doc=integrated,
heading="二、世界",
output=output,
candidates=sources,
)
)
text = output.read_text(encoding="utf-8")
self.assertIn("这是主会话审定后的第一章", text)
self.assertIn("第二章当前来源内容", text)
self.assertNotIn("第一章来源内容足够长", text)
def test_prefix_drift_is_rejected(self) -> None:
source = candidate("来源")
integrated = "# 统合\n\n## 目录\n\n## 二、世界\n"
with self.assertRaises(SerialMergeError):
validate_prefix(integrated, source, "二、世界")
def test_raw_output_contract(self) -> None:
raw = """<decision>保留能够产生剧情的规则。</decision>
<chapter>
## 二、世界
<!-- S051-S100 -->
- **S051|规则一:** 规则有明确条件、事件用途、失败代价以及后续阶段接口,不靠旁白成立。
- **S052|规则二:** 另一条规则与前文兼容,并通过人物行动和资源损失进入故事。
</chapter>"""
_, _, ids = parse_raw(raw, "二、世界", 51, 100, 2, 3, 20)
self.assertEqual([51, 52], ids)
if __name__ == "__main__":
unittest.main()