Переглянути джерело

refactor(write): slim webnovel-write to dispatch contract; add git-diff change-surface check; fix reviewer write-ownership

- Agent calls -> §8.4 natural-language form (registered names, no pseudo-function)
- reviewer returns JSON only; main flow writes review_results.json (ownership)
- remove inline data-agent schema (single source = data-agent.md, 判据一)
- add read-only git diff --name-status/--check before chapter-commit (B-class contract)
- tests: migrate invocation-template test, retire schema-anchor test (§12.2), flip git-diff xfail->pass
lingfengQAQ 2 тижнів тому
батько
коміт
293b819ed7

+ 11 - 19
webnovel-writer/scripts/data_modules/tests/test_prompt_integrity.py

@@ -272,14 +272,14 @@ def test_active_skills_use_agent_tool_name_not_legacy_task():
 
 
 def test_webnovel_write_skill_uses_explicit_agent_invocation_templates():
-    """webnovel-write 的关键 subagent 必须用显式 Agent(subagent_type=...) 调用模板。"""
+    """关键 subagent 必须经 Agent 工具按注册名 webnovel-writer:X 显式调用;不再用伪函数 subagent_type 块(plan §4.4.2/§8.4)。"""
     text = _read_text(SKILLS_DIR / "webnovel-write" / "SKILL.md")
     fm = _extract_frontmatter(text)
 
     assert "Agent" in fm.get("allowed-tools", "")
     for subagent in ("context-agent", "reviewer", "data-agent"):
-        assert f'subagent_type: "webnovel-writer:{subagent}"' in text
-        assert f'subagent_type: "{subagent}"' not in text
+        assert f"webnovel-writer:{subagent}" in text, f"缺少 {subagent} 的注册名显式调用"
+    assert "subagent_type:" not in text, "不应再使用伪函数 subagent_type 调用块"
     assert "不得用主流程口头代替 subagent 输出" in text
 
 
@@ -347,16 +347,10 @@ def test_data_agent_is_described_as_extraction_only_not_direct_write_mainline():
     assert "直接写入 index.db 和 state.json" not in text
 
 
-def test_webnovel_write_data_agent_prompt_requires_extraction_schema():
-    text = (SKILLS_DIR / "webnovel-write" / "SKILL.md").read_text(encoding="utf-8")
-    assert "webnovel-writer:data-agent" in text
-    assert "fulfillment_result.json 必须顶层" in text
-    assert "planned_nodes/covered_nodes/missed_nodes/extra_nodes" in text
-    assert "disambiguation_result.json 必须顶层包含 pending" in text
-    assert "extraction_result.json 必须严格" in text
-    assert "accepted_events/state_deltas/entity_deltas" in text
-    assert "禁止包在 chapter/fulfillment/disambiguation/extraction" in text
-    assert "event_id/chapter/event_type/subject/payload" in text
+# (已按 plan §12.2 退役) test_webnovel_write_data_agent_prompt_requires_extraction_schema:
+# 该测试逐字要求主 Skill 写出 data artifact 的 schema 字段名,与判据一冲突。schema 字段保障已迁到
+# data-agent.md 生产方(test_data_agent_is_described_as_extraction_only_not_direct_write_mainline)
+# + precommit 负向用例(Task 7)。主 Skill 不再内联长 schema。
 
 
 def test_dashboard_and_plan_skills_surface_story_runtime_mainline():
@@ -642,16 +636,14 @@ def test_agent_write_ownership_matches_tools_frontmatter():
 
 # B 类红线(提交前变更面校验):write SKILL 在 chapter-commit 前必须执行只读 git diff 变更面校验。
 # 现状 write SKILL 尚无此步 → 标 xfail;Task 5(Phase 1)实现后移除本标记,转为硬守护。
-@pytest.mark.xfail(
-    reason="B 类新契约:提交前只读 git diff 变更面校验由 Phase 1 (Task 5) 落地,落地后移除本标记",
-    strict=False,
-)
+# B 类红线(提交前变更面校验):write SKILL 在 chapter-commit 前必须执行只读 git diff 变更面校验。
+# Phase 1 (Task 5) 已落地 → 转为硬守护(移除 xfail 标记)。
 def test_write_skill_has_readonly_git_diff_change_surface_check():
     """红线(提交前变更面校验):write SKILL 在 chapter-commit 前执行只读 git diff 校验。"""
     text = _read_text(SKILLS_DIR / "webnovel-write" / "SKILL.md")
-    assert "git diff --name-status" in text, (
+    assert "diff --name-status" in text, (
         "write SKILL 缺少提交前只读 git diff --name-status 变更面校验"
     )
-    assert "git diff --check" in text, (
+    assert "diff --check" in text, (
         "write SKILL 缺少 git diff --check 空白/冲突标记校验"
     )

+ 50 - 20
webnovel-writer/skills/webnovel-write/SKILL.md

@@ -83,12 +83,16 @@ python -X utf8 "${SCRIPTS_DIR}/webnovel.py" --project-root "${PROJECT_ROOT}" \
 
 必须使用 `Agent` 工具调用 `context-agent`,不得由主流程自行整理任务书。
 
-```text
-Agent(
-  subagent_type: "webnovel-writer:context-agent",
-  prompt: "chapter={chapter_num}; project_root=${PROJECT_ROOT}; scripts_dir=${SCRIPTS_DIR}; storage_path=${PROJECT_ROOT}/.webnovel; state_file=${PROJECT_ROOT}/.webnovel/state.json(projection/read-model,仅兼容读取)。先 research,再按 本章硬性约束→CBN/CPNs/CEN→本章禁区→风格指引→dynamic_context补充参考 的顺序输出五段写作任务书。"
-)
-```
+Use the Agent tool to run `webnovel-writer:context-agent`.
+
+Task:
+- chapter={chapter_num}
+- project_root=${PROJECT_ROOT}
+- scripts_dir=${SCRIPTS_DIR}
+- storage_path=${PROJECT_ROOT}/.webnovel
+- state_file=${PROJECT_ROOT}/.webnovel/state.json(projection/read-model,仅兼容读取)
+- 先 research,再按 本章硬性约束 → CBN/CPNs/CEN → 本章禁区 → 风格指引 → dynamic_context 补充参考 的顺序输出五段写作任务书。
+- 上下文不足时返回 blocker。
 
 产物:一份写作任务书,能独立支撑 Step 2 起草。
 
@@ -100,12 +104,17 @@ Agent(
 
 必须使用 `Agent` 工具调用 `reviewer`,不得由主流程伪造审查 JSON。
 
-```text
-Agent(
-  subagent_type: "webnovel-writer:reviewer",
-  prompt: "chapter={chapter_num}; chapter_file=${CHAPTER_FILE}; project_root=${PROJECT_ROOT}; scripts_dir=${SCRIPTS_DIR}。严格输出 reviewer schema JSON,并保存到 ${PROJECT_ROOT}/.webnovel/tmp/review_results.json。"
-)
-```
+Use the Agent tool to run `webnovel-writer:reviewer`.
+
+Task:
+- chapter={chapter_num}
+- chapter_file=${CHAPTER_FILE}
+- project_root=${PROJECT_ROOT}
+- scripts_dir=${SCRIPTS_DIR}
+- 只返回严格的 reviewer schema JSON,不写任何文件。
+- 不评分、不口头总结。
+
+reviewer 只返回 JSON;主流程负责用 `Write` 把返回的 JSON 写入 `${PROJECT_ROOT}/.webnovel/tmp/review_results.json`(reviewer 不持 Write,是这份 artifact 的非写入方),随后运行 review-pipeline。
 
 ```bash
 python -X utf8 "${SCRIPTS_DIR}/webnovel.py" --project-root "${PROJECT_ROOT}" review-pipeline \
@@ -132,21 +141,42 @@ python -X utf8 "${SCRIPTS_DIR}/webnovel.py" --project-root "${PROJECT_ROOT}" rev
 
 必须使用 `Agent` 工具调用 `data-agent`,产出 fulfillment_result / disambiguation_result / extraction_result 三份 JSON,并复用 Step 3 的 review_results。
 
-```text
-Agent(
-  subagent_type: "webnovel-writer:data-agent",
-  prompt: "chapter={chapter_num}; chapter_file=${CHAPTER_FILE}; project_root=${PROJECT_ROOT}; scripts_dir=${SCRIPTS_DIR}。从正文提取事实,生成 .webnovel/tmp/ 下的 fulfillment_result.json、disambiguation_result.json、extraction_result.json;fulfillment_result.json 必须顶层包含 planned_nodes/covered_nodes/missed_nodes/extra_nodes;disambiguation_result.json 必须顶层包含 pending;extraction_result.json 必须严格按你的第7节格式输出顶层字段 accepted_events/state_deltas/entity_deltas/entities_appeared/scenes/summary_text,禁止包在 chapter/fulfillment/disambiguation/extraction 等外层对象里;accepted_events 子项必须包含 event_id/chapter/event_type/subject/payload;不直接写 state/index/summaries/memory。"
-)
-```
+Use the Agent tool to run `webnovel-writer:data-agent`.
+
+Task:
+- chapter={chapter_num}
+- chapter_file=${CHAPTER_FILE}
+- project_root=${PROJECT_ROOT}
+- scripts_dir=${SCRIPTS_DIR}
+- output_dir=${PROJECT_ROOT}/.webnovel/tmp
+- 按你自己的 schema(见 data-agent 输出格式段)生成 fulfillment_result.json、disambiguation_result.json、extraction_result.json 三份 artifact。
+- 你是这三份 artifact 的唯一写入者;不直接写 state/index/summaries/memory/vectors/projection。
+
+artifact 字段 schema 由 data-agent 自身定义、runtime validator 校验;主流程只检查文件存在与 schema,不重写、不补写、不口头替代。
 
-Data Agent 只提取事实+生成 artifacts,不直接写 state/index/summaries/memory。
+#### 5.2 提交前校验与 CHAPTER_COMMIT
 
-#### 5.2 CHAPTER_COMMIT
+先跑 precommit gate:
 
 ```bash
 python -X utf8 "${SCRIPTS_DIR}/webnovel.py" --project-root "${PROJECT_ROOT}" \
   write-gate --chapter {chapter_num} --stage precommit --format json
+```
+
+precommit 通过后,运行提交前只读 `git diff` 变更面校验(写入所有权 sanity check,只读、不 stage、不提交):
+
+```bash
+if git -C "${PROJECT_ROOT}" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
+  git -C "${PROJECT_ROOT}" diff --name-status -- .
+  git -C "${PROJECT_ROOT}" diff --check -- .
+fi
+```
+
+变更面不得出现插件目录、其他书项目、其他章节正文或不属于本章流程的手写状态文件;`git diff` 只覆盖 git 可见文件,SQLite / `.webnovel/` 内部语义由 5.3 postcommit 与 runtime 只读查询验证。若项目根不是 git worktree,记录“跳过 git diff 校验”,不得因此跳过 precommit gate。本步只读,禁止在此执行 `git add`/`git commit`。
 
+校验通过后运行 chapter-commit:
+
+```bash
 python -X utf8 "${SCRIPTS_DIR}/webnovel.py" --project-root "${PROJECT_ROOT}" chapter-commit \
   --chapter {chapter_num} \
   --review-result "${PROJECT_ROOT}/.webnovel/tmp/review_results.json" \