For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: 建立 CHAPTER_COMMIT 主链、accepted / rejected 语义与四类 projection writers,让章节事实写后回写统一经过提交对象,而不再散写到 state / index / summary / memory。
Architecture: 在 Phase 2 的合同优先运行时之上,引入 CHAPTER_COMMIT.json 作为写后唯一事实入口。提交阶段先汇总 review_result / fulfillment_result / disambiguation_result / accepted_events / deltas,只有 commit accepted 才允许投影器分发到下游存储。state_manager / memory writer / index_manager 在本阶段重定位为投影写入器底座,而不是章节事实真源。
Tech Stack: Python 3.13, Pydantic, argparse, pytest, SQLite (index.db), JSON commit artifacts under .story-system/commits
Spec: docs/superpowers/specs/2026-04-12-story-system-evolution-spec.md
Companion Plans: docs/superpowers/plans/2026-04-12-story-system-phase1-contract-seed.md, docs/superpowers/plans/2026-04-12-story-system-phase2-contract-first-runtime.md
本计划只覆盖 Phase 3:
CHAPTER_COMMIT明确不做:
退出标准:
PROJECT_ROOT/.story-system/commits/chapter_XXX.commit.json 成为写后事实入口state / index / summary / memory 投影,其中 StateProjectionWriter 必须真实更新 state.jsonprojection_status 可追踪每个 writer 的完成情况;写入失败只记录到对应 writer 状态,不回滚 commit accepted/rejected 判定文档更新继续追加到已有 Story System 段落,不重写 README 总体结构。
webnovel-writer/scripts/chapter_commit.pywebnovel-writer/scripts/data_modules/story_commit_schema.pywebnovel-writer/scripts/data_modules/chapter_commit_service.pywebnovel-writer/scripts/data_modules/state_projection_writer.pywebnovel-writer/scripts/data_modules/index_projection_writer.pywebnovel-writer/scripts/data_modules/summary_projection_writer.pywebnovel-writer/scripts/data_modules/memory_projection_writer.pywebnovel-writer/scripts/data_modules/tests/test_story_commit_schema.pywebnovel-writer/scripts/data_modules/tests/test_chapter_commit_service.pywebnovel-writer/scripts/data_modules/tests/test_projection_writers.pydocs/architecture/story-system-phase3.mdwebnovel-writer/scripts/data_modules/story_contracts.pywebnovel-writer/scripts/data_modules/webnovel.pywebnovel-writer/scripts/data_modules/tests/test_webnovel_unified_cli.pywebnovel-writer/scripts/review_pipeline.pywebnovel-writer/scripts/data_modules/state_manager.pywebnovel-writer/scripts/data_modules/memory/writer.pywebnovel-writer/skills/webnovel-write/SKILL.mdwebnovel-writer/scripts/data_modules/tests/test_prompt_integrity.pyREADME.mddocs/architecture/overview.mddocs/guides/commands.mddocs/superpowers/README.mdCHAPTER_COMMIT schema 与落盘路径Files:
webnovel-writer/scripts/data_modules/story_commit_schema.pywebnovel-writer/scripts/data_modules/tests/test_story_commit_schema.pyModify: webnovel-writer/scripts/data_modules/story_contracts.py
[ ] Step 1: 先写 schema 测试
# webnovel-writer/scripts/data_modules/tests/test_story_commit_schema.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from data_modules.story_commit_schema import ChapterCommit
def test_chapter_commit_accepts_required_sections():
payload = {
"meta": {"schema_version": "story-system/v1", "chapter": 3, "status": "accepted"},
"contract_refs": {"master": "MASTER_SETTING.json", "chapter": "chapter_003.json"},
"outline_snapshot": {"planned_nodes": ["发现陷阱"]},
"review_result": {"blocking_count": 0},
"fulfillment_result": {"missed_nodes": []},
"disambiguation_result": {"pending": []},
"accepted_events": [],
"state_deltas": [],
"entity_deltas": [],
"projection_status": {"state": "pending", "index": "pending", "summary": "pending", "memory": "pending"},
}
model = ChapterCommit.model_validate(payload)
assert model.meta["status"] == "accepted"
[ ] Step 2: 跑红灯
Run: python -m pytest webnovel-writer/scripts/data_modules/tests/test_story_commit_schema.py -q --no-cov
Expected: ModuleNotFoundError: No module named 'data_modules.story_commit_schema'
[ ] Step 3: 实现 schema 与 commit 路径
# webnovel-writer/scripts/data_modules/story_commit_schema.py
from __future__ import annotations
from typing import Any, Dict, List
from pydantic import BaseModel, Field
class ChapterCommit(BaseModel):
meta: Dict[str, Any]
contract_refs: Dict[str, str]
outline_snapshot: Dict[str, Any]
review_result: Dict[str, Any]
fulfillment_result: Dict[str, Any]
disambiguation_result: Dict[str, Any]
accepted_events: List[Dict[str, Any]] = Field(default_factory=list)
state_deltas: List[Dict[str, Any]] = Field(default_factory=list)
entity_deltas: List[Dict[str, Any]] = Field(default_factory=list)
projection_status: Dict[str, str]
# webnovel-writer/scripts/data_modules/story_contracts.py
@property
def commits_dir(self) -> Path:
return self.root / "commits"
def commit_json(self, chapter: int) -> Path:
return self.commits_dir / f"chapter_{chapter:03d}.commit.json"
[ ] Step 4: 回跑测试
Run: python -m pytest webnovel-writer/scripts/data_modules/tests/test_story_commit_schema.py -q --no-cov
Expected: 通过
[ ] Step 5: 提交
git add webnovel-writer/scripts/data_modules/story_commit_schema.py \
webnovel-writer/scripts/data_modules/story_contracts.py \
webnovel-writer/scripts/data_modules/tests/test_story_commit_schema.py
git commit -m "feat: add chapter commit schema and paths"
chapter_commit_service 与提交校验Files:
webnovel-writer/scripts/data_modules/chapter_commit_service.pywebnovel-writer/scripts/data_modules/tests/test_chapter_commit_service.pyModify: webnovel-writer/scripts/review_pipeline.py
[ ] Step 1: 先写提交通过/阻断测试
# webnovel-writer/scripts/data_modules/tests/test_chapter_commit_service.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from data_modules.chapter_commit_service import ChapterCommitService
def test_commit_service_rejects_when_missed_nodes_exist(tmp_path):
service = ChapterCommitService(tmp_path)
payload = service.build_commit(
chapter=3,
review_result={"blocking_count": 0},
fulfillment_result={"planned_nodes": ["发现陷阱"], "missed_nodes": ["发现陷阱"]},
disambiguation_result={"pending": []},
extraction_result={"state_deltas": [], "entity_deltas": [], "accepted_events": []},
)
assert payload["meta"]["status"] == "rejected"
def test_commit_service_accepts_when_all_checks_pass(tmp_path):
service = ChapterCommitService(tmp_path)
payload = service.build_commit(
chapter=3,
review_result={"blocking_count": 0},
fulfillment_result={"planned_nodes": ["发现陷阱"], "covered_nodes": ["发现陷阱"], "missed_nodes": [], "extra_nodes": []},
disambiguation_result={"pending": []},
extraction_result={"state_deltas": [], "entity_deltas": [], "accepted_events": []},
)
assert payload["meta"]["status"] == "accepted"
assert payload["contract_refs"]["master"] == "MASTER_SETTING.json"
assert payload["contract_refs"]["chapter"] == "chapter_003.json"
assert payload["outline_snapshot"]["covered_nodes"] == ["发现陷阱"]
[ ] Step 2: 跑红灯
Run: python -m pytest webnovel-writer/scripts/data_modules/tests/test_chapter_commit_service.py -q --no-cov
Expected: ModuleNotFoundError: No module named 'data_modules.chapter_commit_service'
[ ] Step 3: 实现提交服务
# webnovel-writer/scripts/data_modules/chapter_commit_service.py
from __future__ import annotations
import json
from pathlib import Path
from typing import Any, Dict
from data_modules.index_projection_writer import IndexProjectionWriter
from data_modules.memory_projection_writer import MemoryProjectionWriter
from data_modules.state_projection_writer import StateProjectionWriter
from data_modules.summary_projection_writer import SummaryProjectionWriter
class ChapterCommitService:
def __init__(self, project_root: Path):
self.project_root = Path(project_root)
def build_commit(
self,
chapter: int,
review_result: Dict[str, Any],
fulfillment_result: Dict[str, Any],
disambiguation_result: Dict[str, Any],
extraction_result: Dict[str, Any],
) -> Dict[str, Any]:
rejected = bool(review_result.get("blocking_count")) or bool(fulfillment_result.get("missed_nodes")) or bool(disambiguation_result.get("pending"))
status = "rejected" if rejected else "accepted"
return {
"meta": {"schema_version": "story-system/v1", "chapter": chapter, "status": status},
"contract_refs": {
"master": "MASTER_SETTING.json",
"chapter": f"chapter_{chapter:03d}.json",
"review": f"chapter_{chapter:03d}.review.json",
},
"outline_snapshot": {
"planned_nodes": fulfillment_result.get("planned_nodes", []),
"covered_nodes": fulfillment_result.get("covered_nodes", []),
"missed_nodes": fulfillment_result.get("missed_nodes", []),
"extra_nodes": fulfillment_result.get("extra_nodes", []),
},
"review_result": review_result,
"fulfillment_result": fulfillment_result,
"disambiguation_result": disambiguation_result,
"accepted_events": extraction_result.get("accepted_events", []),
"state_deltas": extraction_result.get("state_deltas", []),
"entity_deltas": extraction_result.get("entity_deltas", []),
"projection_status": {"state": "pending", "index": "pending", "summary": "pending", "memory": "pending"},
}
def persist_commit(self, payload: Dict[str, Any]) -> Path:
target = self.project_root / ".story-system" / "commits"
target.mkdir(parents=True, exist_ok=True)
path = target / f"chapter_{int(payload['meta']['chapter']):03d}.commit.json"
path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
return path
def apply_projections(self, payload: Dict[str, Any]) -> Dict[str, Any]:
if payload["meta"]["status"] != "accepted":
return payload
writers = {
"state": StateProjectionWriter(self.project_root),
"index": IndexProjectionWriter(self.project_root),
"summary": SummaryProjectionWriter(self.project_root),
"memory": MemoryProjectionWriter(self.project_root),
}
for name, writer in writers.items():
try:
result = writer.apply(payload)
payload["projection_status"][name] = "done" if result.get("applied") else "skipped"
except Exception as exc:
payload["projection_status"][name] = f"failed:{exc}"
self.persist_commit(payload)
return payload
这里补一条 Phase 3 / Phase 4 的职责协议,后续实现必须遵守:
ChapterCommitService.apply_projections() 始终是唯一调度入口EventProjectionRouter 只负责判定“哪些 writer 应被激活”EventProjectionRouter 不单独再跑一轮投影,避免 state_deltas 与 accepted_events 双重落库review_pipeline.py 在本 Task 必须补一条明确接线:
review_result / fulfillment_result / disambiguation_result / extraction_resultChapterCommitService.build_commit()persist_commit(),再依据 payload["meta"]["status"] 决定是否进入投影阶段也就是说,review_pipeline.py 不再只是“被列进修改文件”,而是 Phase 3 写后主链真正的调用入口。
Run: python -m pytest webnovel-writer/scripts/data_modules/tests/test_chapter_commit_service.py -q --no-cov
Expected: 通过
[ ] Step 5: 提交
git add webnovel-writer/scripts/data_modules/chapter_commit_service.py \
webnovel-writer/scripts/data_modules/tests/test_chapter_commit_service.py \
webnovel-writer/scripts/review_pipeline.py
git commit -m "feat: add chapter commit service and status semantics"
Files:
webnovel-writer/scripts/data_modules/state_projection_writer.pywebnovel-writer/scripts/data_modules/index_projection_writer.pywebnovel-writer/scripts/data_modules/summary_projection_writer.pywebnovel-writer/scripts/data_modules/memory_projection_writer.pywebnovel-writer/scripts/data_modules/tests/test_projection_writers.pywebnovel-writer/scripts/data_modules/index_manager.pywebnovel-writer/scripts/data_modules/state_manager.pyModify: webnovel-writer/scripts/data_modules/memory/writer.py
[ ] Step 1: 先写 accepted / rejected 投影测试
# webnovel-writer/scripts/data_modules/tests/test_projection_writers.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import json
from data_modules.chapter_commit_service import ChapterCommitService
from data_modules.state_projection_writer import StateProjectionWriter
def test_state_projection_writer_skips_rejected_commit(tmp_path):
writer = StateProjectionWriter(tmp_path)
result = writer.apply({"meta": {"status": "rejected"}, "state_deltas": []})
assert result["applied"] is False
def test_state_projection_writer_applies_accepted_commit(tmp_path):
(tmp_path / ".webnovel").mkdir(parents=True, exist_ok=True)
(tmp_path / ".webnovel" / "state.json").write_text("{}", encoding="utf-8")
writer = StateProjectionWriter(tmp_path)
result = writer.apply({"meta": {"status": "accepted"}, "state_deltas": [{"entity_id": "x", "field": "realm", "new": "斗者"}]})
assert result["applied"] is True
payload = json.loads((tmp_path / ".webnovel" / "state.json").read_text(encoding="utf-8"))
assert payload["entity_state"]["x"]["realm"] == "斗者"
def test_accepted_commit_updates_state_json_end_to_end(tmp_path):
(tmp_path / ".webnovel").mkdir(parents=True, exist_ok=True)
(tmp_path / ".webnovel" / "state.json").write_text("{}", encoding="utf-8")
service = ChapterCommitService(tmp_path)
commit_payload = service.build_commit(
chapter=3,
review_result={"blocking_count": 0},
fulfillment_result={"planned_nodes": ["发现陷阱"], "covered_nodes": ["发现陷阱"], "missed_nodes": [], "extra_nodes": []},
disambiguation_result={"pending": []},
extraction_result={"state_deltas": [{"entity_id": "x", "field": "realm", "new": "斗者"}], "entity_deltas": [], "accepted_events": []},
)
StateProjectionWriter(tmp_path).apply(commit_payload)
payload = json.loads((tmp_path / ".webnovel" / "state.json").read_text(encoding="utf-8"))
assert payload["entity_state"]["x"]["realm"] == "斗者"
[ ] Step 2: 跑红灯
Run: python -m pytest webnovel-writer/scripts/data_modules/tests/test_projection_writers.py -q --no-cov
Expected: ModuleNotFoundError for projection writer modules
[ ] Step 3: 实现四类 writer
# webnovel-writer/scripts/data_modules/state_projection_writer.py
from data_modules.story_contracts import read_json_if_exists
class StateProjectionWriter:
def __init__(self, project_root: Path):
self.project_root = Path(project_root)
def apply(self, commit_payload: dict) -> dict:
if commit_payload["meta"]["status"] != "accepted":
return {"applied": False, "writer": "state", "reason": "commit_rejected"}
state_path = self.project_root / ".webnovel" / "state.json"
state = read_json_if_exists(state_path) or {}
entity_state = state.setdefault("entity_state", {})
applied_count = 0
for delta in commit_payload.get("state_deltas", []):
entity_id = str(delta.get("entity_id") or "").strip()
field = str(delta.get("field") or "").strip()
if not entity_id or not field:
continue
entity_state.setdefault(entity_id, {})[field] = delta.get("new")
applied_count += 1
state_path.write_text(json.dumps(state, ensure_ascii=False, indent=2), encoding="utf-8")
return {"applied": applied_count > 0, "writer": "state", "applied_count": applied_count}
其他三个 writer 在 Phase 3 可以先保持“最小投影”,但不能是 no-op stub,至少要薄适配到现有底座:
class IndexProjectionWriter:
def apply(self, commit_payload: dict) -> dict:
if commit_payload["meta"]["status"] != "accepted":
return {"applied": False, "writer": "index", "reason": "commit_rejected"}
manager = IndexManager(self.project_root)
for delta in commit_payload.get("entity_deltas", []):
manager.apply_entity_delta(delta)
return {"applied": True, "writer": "index", "applied_count": len(commit_payload.get("entity_deltas", []))}
class SummaryProjectionWriter:
def apply(self, commit_payload: dict) -> dict:
if commit_payload["meta"]["status"] != "accepted":
return {"applied": False, "writer": "summary", "reason": "commit_rejected"}
return append_summary_projection(self.project_root, commit_payload)
class MemoryProjectionWriter:
def apply(self, commit_payload: dict) -> dict:
if commit_payload["meta"]["status"] != "accepted":
return {"applied": False, "writer": "memory", "reason": "commit_rejected"}
return MemoryWriter(self.project_root).apply_commit_projection(commit_payload)
这里的交付要求写死:
StateProjectionWriter 必须真实落地Index / Summary / Memory 允许是薄适配,但必须调用真实底座或真实文件写入IndexManager.apply_entity_delta()、append_summary_projection()、MemoryWriter.apply_commit_projection(),就在本 Task 一并补最小适配器骨架;函数名可调整,但 writer 层对外协议不变projection_status 记录 "done" / "skipped" / "failed:...",不能一律回 "pending"
[ ] Step 4: 回跑测试
Run: python -m pytest webnovel-writer/scripts/data_modules/tests/test_projection_writers.py -q --no-cov
Expected: 通过
[ ] Step 5: 提交
git add webnovel-writer/scripts/data_modules/state_projection_writer.py \
webnovel-writer/scripts/data_modules/index_projection_writer.py \
webnovel-writer/scripts/data_modules/summary_projection_writer.py \
webnovel-writer/scripts/data_modules/memory_projection_writer.py \
webnovel-writer/scripts/data_modules/tests/test_projection_writers.py \
webnovel-writer/scripts/data_modules/state_manager.py \
webnovel-writer/scripts/data_modules/memory/writer.py
git commit -m "feat: add commit-driven projection writers"
Files:
webnovel-writer/scripts/chapter_commit.pywebnovel-writer/scripts/data_modules/webnovel.pywebnovel-writer/scripts/data_modules/tests/test_webnovel_unified_cli.pywebnovel-writer/scripts/data_modules/tests/test_chapter_commit_service.pywebnovel-writer/skills/webnovel-write/SKILL.mdwebnovel-writer/scripts/data_modules/tests/test_prompt_integrity.pydocs/architecture/story-system-phase3.mdREADME.mddocs/architecture/overview.mddocs/guides/commands.mdModify: docs/superpowers/README.md
[ ] Step 1: 增加统一 CLI 转发测试
def test_webnovel_commit_forwards(monkeypatch, tmp_path):
from data_modules import webnovel as cli
project_root = tmp_path / "book"
(project_root / ".webnovel").mkdir(parents=True, exist_ok=True)
(project_root / ".webnovel" / "state.json").write_text("{}", encoding="utf-8")
called = {}
def _fake_run_script(script_name, argv):
called["script_name"] = script_name
called["argv"] = argv
return 0
monkeypatch.setattr(cli, "_run_script", _fake_run_script)
monkeypatch.setattr(sys, "argv", ["webnovel", "--project-root", str(project_root), "chapter-commit", "--chapter", "3"])
cli.main()
assert called["script_name"] == "chapter_commit.py"
def test_chapter_commit_cli_builds_and_persists_commit(tmp_path, monkeypatch):
review_path = tmp_path / "review.json"
fulfillment_path = tmp_path / "fulfillment.json"
disambiguation_path = tmp_path / "disambiguation.json"
extraction_path = tmp_path / "extraction.json"
review_path.write_text('{"blocking_count": 0}', encoding="utf-8")
fulfillment_path.write_text('{"planned_nodes": ["发现陷阱"], "covered_nodes": ["发现陷阱"], "missed_nodes": [], "extra_nodes": []}', encoding="utf-8")
disambiguation_path.write_text('{"pending": []}', encoding="utf-8")
extraction_path.write_text('{"state_deltas": [], "entity_deltas": [], "accepted_events": []}', encoding="utf-8")
from chapter_commit import main
monkeypatch.setattr(
sys,
"argv",
[
"chapter_commit",
"--project-root",
str(tmp_path),
"--chapter",
"3",
"--review-result",
str(review_path),
"--fulfillment-result",
str(fulfillment_path),
"--disambiguation-result",
str(disambiguation_path),
"--extraction-result",
str(extraction_path),
],
)
main()
assert (tmp_path / ".story-system" / "commits" / "chapter_003.commit.json").is_file()
[ ] Step 2: 接入 CLI 与技能
在 webnovel.py 增加:
# webnovel-writer/scripts/chapter_commit.py
def _read_json(path: str) -> dict:
return json.loads(Path(path).read_text(encoding="utf-8"))
def main() -> None:
parser = argparse.ArgumentParser(description="Chapter commit CLI")
parser.add_argument("--project-root", required=True)
parser.add_argument("--chapter", type=int, required=True)
parser.add_argument("--review-result", required=True)
parser.add_argument("--fulfillment-result", required=True)
parser.add_argument("--disambiguation-result", required=True)
parser.add_argument("--extraction-result", required=True)
args = parser.parse_args()
service = ChapterCommitService(Path(args.project_root))
payload = service.build_commit(
chapter=args.chapter,
review_result=_read_json(args.review_result),
fulfillment_result=_read_json(args.fulfillment_result),
disambiguation_result=_read_json(args.disambiguation_result),
extraction_result=_read_json(args.extraction_result),
)
service.persist_commit(payload)
if payload["meta"]["status"] == "accepted":
payload = service.apply_projections(payload)
print(json.dumps(payload, ensure_ascii=False))
# webnovel-writer/scripts/data_modules/webnovel.py
p_commit = sub.add_parser("chapter-commit", help="转发到 chapter_commit.py")
p_commit.add_argument("args", nargs=argparse.REMAINDER)
在 skills/webnovel-write/SKILL.md 将原先“写完直接 state / index / summaries / memory 回写”替换为:
python -X utf8 "${SCRIPTS_DIR}/webnovel.py" --project-root "${PROJECT_ROOT}" \
chapter-commit --chapter {chapter_num} \
--review-result "{review_json}" \
--fulfillment-result "{fulfillment_json}" \
--disambiguation-result "{disambiguation_json}" \
--extraction-result "{extraction_json}"
同时在文档里明确一个运行约束:
chapter_commit.py 是独立人工/CLI 入口review_pipeline.py 是 skill 主流程中的集成入口同一次写后流程只能走其中一个入口,禁止 review_pipeline.py 已提交后再补跑 chapter_commit.py
[ ] Step 3: 新建 Phase 3 文档并跑回归
Run:
python -m pytest \
webnovel-writer/scripts/data_modules/tests/test_story_commit_schema.py \
webnovel-writer/scripts/data_modules/tests/test_chapter_commit_service.py \
webnovel-writer/scripts/data_modules/tests/test_projection_writers.py \
webnovel-writer/scripts/data_modules/tests/test_webnovel_unified_cli.py \
webnovel-writer/scripts/data_modules/tests/test_prompt_integrity.py \
-q --no-cov
Expected: 全部通过
[ ] Step 4: 最终提交
git add webnovel-writer/scripts/chapter_commit.py \
webnovel-writer/scripts/data_modules/webnovel.py \
webnovel-writer/scripts/data_modules/tests/test_webnovel_unified_cli.py \
webnovel-writer/scripts/data_modules/tests/test_chapter_commit_service.py \
webnovel-writer/skills/webnovel-write/SKILL.md \
webnovel-writer/scripts/data_modules/tests/test_prompt_integrity.py \
README.md \
docs/architecture/story-system-phase3.md \
docs/architecture/overview.md \
docs/guides/commands.md \
docs/superpowers/README.md
git commit -m "docs: document story system phase3 chapter commit chain"
13.4 Phase 3:章节提交主链
CHAPTER_COMMIT:Task 1 / Task 29.2 / 9.3 / 9.5
11.2 / 11.3
TODO / TBDPhase 3 完成后进入:
Phase 4 Event Log And Override Ledger