feat(review): 人物塑造 advisory 审(第五审,仅建议不阻断验收)
写→审链新增第五审「人物塑造」(灵感③/D2):advisory 单维——仅建议、不阻断
验收(不进 conflicts、不碰 assert_conflicts_resolved、不入 REVIEW_RESERVED_NAMES);
只做人物塑造、不做氛围。
- SPEC characterization_spec(analyst,reads=("characters",),writes=())+
prompts/characterization.md(只读、显式忽略文本长度与辞藻华丽度、引用逐字片段+置信度)
- schema CharacterizationReview{issues[...]}(全字段默认值守解析韧性)+ 注册 catalog
- 计数三处 22→23 + regen prompt_hashes.json(仅加一条)
- DB chapter_reviews 加 characterization JSONB nullable 列(迁移 f6a7b8c9d0e1)
- 接线:graph REVIEW_SPECS 追加 · chain ReviewRecordRepo · collect(extract/record) ·
review_repo(Protocol/Sql/_to_view/ReviewView) · sse(event/factory/section) · normalize 自动纳入
- 契约 ReviewHistoryItem 加 characterization(gen:api 已重生成)
- 前端 sse/history/useReviewStream + CharacterizationPanel(只展示无裁决 UI、置信度降序+低置信折叠)+ ReviewReport 挂面板
- 测试 test_characterization_does_not_block_accept + 计数/golden + collect/normalize + 前端 reducer/normalize/parse
守不变量 #2/#3/#9;依赖 ⑧(motive/appearance 作客观锚点)。
This commit is contained in:
@@ -151,6 +151,7 @@ class FakeReviewRepo:
|
||||
foreshadow_sug: list[dict[str, Any]] | None = None,
|
||||
style: dict[str, Any] | None = None,
|
||||
pace: dict[str, Any] | None = None,
|
||||
characterization: dict[str, Any] | None = None,
|
||||
health_score: int | None = None,
|
||||
) -> ReviewView:
|
||||
view = ReviewView(
|
||||
@@ -162,6 +163,7 @@ class FakeReviewRepo:
|
||||
foreshadow_sug=[dict(s) for s in (foreshadow_sug or [])],
|
||||
style=style,
|
||||
pace=pace,
|
||||
characterization=characterization,
|
||||
health_score=health_score,
|
||||
)
|
||||
# 新→旧:插到列表头部。
|
||||
|
||||
@@ -115,6 +115,7 @@ class _FakeReviewRepo:
|
||||
foreshadow_sug: list[dict[str, Any]] | None = None,
|
||||
style: dict[str, Any] | None = None,
|
||||
pace: dict[str, Any] | None = None,
|
||||
characterization: dict[str, Any] | None = None,
|
||||
health_score: int | None = None,
|
||||
) -> Any:
|
||||
rec = _FakeReviewRecord(chapter_no, list(conflicts))
|
||||
|
||||
@@ -209,6 +209,70 @@ async def test_accept_promotes_appends_digest_records_decisions() -> None:
|
||||
assert session.commits == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_characterization_does_not_block_accept() -> None:
|
||||
"""灵感③ advisory:人物塑造问题(零 continuity 冲突)不阻断验收(D2)。
|
||||
|
||||
留痕带 characterization issues 但 conflicts=[] → 验收 gate 只读 continuity conflicts,
|
||||
直通晋升;characterization 从不进 conflicts、从不产 decisions。
|
||||
"""
|
||||
from ww_api.services.digest_extraction import ChapterDigestFacts
|
||||
|
||||
review_repo = FakeReviewRepo()
|
||||
pid = uuid.uuid4()
|
||||
# 零 continuity 冲突,但有 advisory 人物塑造问题。
|
||||
await review_repo.record(
|
||||
pid,
|
||||
1,
|
||||
conflicts=[],
|
||||
characterization={
|
||||
"issues": [
|
||||
{
|
||||
"character": "沈砚",
|
||||
"aspect": "动机一致性",
|
||||
"where": "第3段",
|
||||
"quote": "他忽然转身就走。",
|
||||
"diagnosis": "与既定护妹动机断裂",
|
||||
"suggestion": "退缩扣回护妹动机",
|
||||
"confidence": 0.82,
|
||||
}
|
||||
]
|
||||
},
|
||||
)
|
||||
digest_gw = FakeReviewGateway(parsed=ChapterDigestFacts(summary="s"))
|
||||
client, chapter_repo, _, digest_repo, session = _make_client(
|
||||
review_repo=review_repo, digest_gateway=digest_gw
|
||||
)
|
||||
async with client:
|
||||
resp = await client.post(
|
||||
f"/projects/{pid}/chapters/1/accept",
|
||||
# 无需对人物塑造做任何裁决(advisory 无 conflict 通道)。
|
||||
json={"final_text": "作者终稿正文。", "decisions": []},
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["accepted_version"] == 1
|
||||
assert session.commits == 1
|
||||
# characterization 留痕不产 decisions(advisory);晋升照常。
|
||||
accepted = await chapter_repo.latest_accepted(pid, 1)
|
||||
assert accepted is not None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_characterization_surfaces_in_review_history() -> None:
|
||||
"""列 characterization 经 GET .../reviews 透传到历史项(前端面板 seed 用)。"""
|
||||
review_repo = FakeReviewRepo()
|
||||
pid = uuid.uuid4()
|
||||
await review_repo.record(
|
||||
pid, 1, conflicts=[], characterization={"issues": [{"character": "沈砚"}]}
|
||||
)
|
||||
client, _, _, _, _ = _make_client(review_repo=review_repo)
|
||||
async with client:
|
||||
resp = await client.get(f"/projects/{pid}/chapters/1/reviews")
|
||||
assert resp.status_code == 200
|
||||
row = resp.json()["reviews"][0]
|
||||
assert row["characterization"]["issues"][0]["character"] == "沈砚"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_accept_with_no_review_succeeds() -> None:
|
||||
"""无审稿留痕(零冲突)→ gate 直通,仍晋升 + 提炼 digest。"""
|
||||
|
||||
@@ -488,6 +488,7 @@ async def list_reviews(
|
||||
foreshadow_sug=v.foreshadow_sug,
|
||||
style=v.style,
|
||||
pace=v.pace,
|
||||
characterization=v.characterization,
|
||||
health_score=v.health_score,
|
||||
decisions=v.decisions,
|
||||
)
|
||||
|
||||
@@ -191,6 +191,8 @@ class ReviewHistoryItem(BaseModel):
|
||||
foreshadow_sug: list[dict[str, Any]] = Field(default_factory=list)
|
||||
style: dict[str, Any] | None = None
|
||||
pace: dict[str, Any] | None = None
|
||||
# 灵感③ advisory 人物塑造审留痕(issues 列表 dict);仅建议、不阻断验收。
|
||||
characterization: dict[str, Any] | None = None
|
||||
health_score: int | None = None
|
||||
decisions: dict[str, Any] | None = None
|
||||
|
||||
|
||||
Reference in New Issue
Block a user