feat(core): continuity 冲突裁决反哺——上一已验收章保留裁决入本章 volatile 前瞻约束
灵感②/D3 窄版 v1:assemble 读上一已验收章的 continuity 冲突作者裁决
(decisions.items 按 conflict_index join 回 conflicts 取正文),仅保留
verdict∈{accept,manual}、去重、按 conflict_index 排序、有界 top-3,
用确定性字符串模板改写成前瞻正向约束,注入本章 volatile「上一章冲突提醒」。
- 绝不调 LLM、绝不反哺 advisory(style/pace/characterization 无作者裁决 oracle)——守不变量 #3/#4。
- 只入 volatile、永不进 stable_core 缓存前缀(依赖上一章裁决、每章易变)——守 #9。
- assemble 仍是确定性纯函数、字节稳定、可缓存——守 #6/#7。
- decisions 非空=已验收(只在验收事务写入);未验收上一章不反哺——守风险#5。
- MemoryRepos 末位加 review: ReviewRepo | None = None(默认 None→优雅降级不反哺),
docstring 8→9;sql_memory_repos 工厂补 review=SqlReviewRepo(既有构造点靠默认无需感知)——守风险#6。
This commit is contained in:
@@ -15,6 +15,8 @@ from typing import Any, Protocol
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from ww_core.domain.review_repo import ReviewRepo
|
||||
|
||||
# ---- 只读视图(snake_case,frozen 防止意外突变)----
|
||||
|
||||
|
||||
@@ -148,7 +150,11 @@ class ProjectSpecRepo(Protocol):
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class MemoryRepos:
|
||||
"""记忆服务所需的 8 个 repo 的依赖捆绑(注入点)。"""
|
||||
"""记忆服务所需的 9 个 repo 的依赖捆绑(注入点)。
|
||||
|
||||
`review` 末位默认 None:只有冲突裁决反哺(灵感②)需要它;不注入时 assemble
|
||||
优雅降级(不反哺),故既有构造点无需感知(守风险#6)。
|
||||
"""
|
||||
|
||||
outline: OutlineRepo
|
||||
character: CharacterRepo
|
||||
@@ -158,3 +164,4 @@ class MemoryRepos:
|
||||
style: StyleRepo
|
||||
rules: RulesRepo
|
||||
project: ProjectSpecRepo
|
||||
review: ReviewRepo | None = None
|
||||
|
||||
@@ -25,6 +25,7 @@ from ww_core.domain.repositories import (
|
||||
StyleView,
|
||||
WorldEntityView,
|
||||
)
|
||||
from ww_core.domain.review_repo import ReviewView
|
||||
|
||||
from .render import render_cards
|
||||
from .selection import MAIN_ROLES, RECENT_DIGEST_COUNT, EntityKey, select_relevant_entities
|
||||
@@ -36,6 +37,13 @@ _LEVEL_RANK: dict[str, int] = {"global": 0, "genre": 1, "style": 2, "project": 3
|
||||
# 开篇黄金三章特判上限:前 N 章在 volatile 追加开篇提示(灵感①)。
|
||||
OPENING_CHAPTER_LIMIT = 3
|
||||
|
||||
# 冲突裁决反哺(灵感②/D3):只反哺"保留项"裁决——作者采纳(accept)或手改(manual);
|
||||
# ignore(作者判为非冲突)不反哺。这三个控制值对齐 schemas.projects.Verdict。
|
||||
_RETAINED_VERDICTS: frozenset[str] = frozenset({"accept", "manual"})
|
||||
|
||||
# 上一章冲突提醒有界上限(top-N)——控作者/模型每章注意力预算(灵感② 校验 M)。
|
||||
PRIOR_CONFLICT_NOTE_LIMIT = 3
|
||||
|
||||
|
||||
def _ser(value: Any) -> str:
|
||||
return json.dumps(value, ensure_ascii=False, sort_keys=True)
|
||||
@@ -137,6 +145,57 @@ def _opening_marker(chapter_no: int) -> str | None:
|
||||
)
|
||||
|
||||
|
||||
def _conflict_constraint(conflict: dict[str, Any], note: str | None) -> str | None:
|
||||
"""把一条已裁决 continuity 冲突改写成本章的前瞻正向约束(确定性模板,绝不调 LLM)。
|
||||
|
||||
优先用作者手改备注(`note`,作者自己的话);否则用审稿改法建议(`suggestion`——
|
||||
作者点「采纳」即背书之,故仍是外部 oracle 驱动,不违 #3/#4)。只做正向「延续设定」
|
||||
措辞,绝无道歉/自指(守 #3/#4:不自评自改、不磨平文风)。两者皆空则返回 None。
|
||||
"""
|
||||
detail = (note or "").strip() or str(conflict.get("suggestion") or "").strip()
|
||||
if not detail:
|
||||
return None
|
||||
ctype = str(conflict.get("type") or "").strip()
|
||||
prefix = f"【{ctype}】" if ctype else ""
|
||||
return f"{prefix}延续上一章已确认的设定:{detail}"
|
||||
|
||||
|
||||
def _prior_conflict_notes(
|
||||
latest_review: ReviewView | None,
|
||||
*,
|
||||
limit: int = PRIOR_CONFLICT_NOTE_LIMIT,
|
||||
) -> str | None:
|
||||
"""上一已验收章的 continuity 冲突作者裁决保留项 → 本章前瞻约束(灵感②/D3)。
|
||||
|
||||
- 仅当上一章已验收(`decisions` 非空——只在验收事务写入)才反哺(守风险#5:不读未定稿)。
|
||||
- 按 `decisions["items"][].conflict_index` join 回 `conflicts[index]` 取正文,
|
||||
过滤 `verdict∈{accept,manual}`(保留项)、去重、按 conflict_index 排序、有界 top-N。
|
||||
- 确定性纯函数、字节稳定、无 LLM 调用(守 #6/#7:assemble 可缓存的纯函数)。
|
||||
- 软批评类(style/pace/characterization)无作者裁决 oracle → 永不经此反哺(守 #3/#4)。
|
||||
"""
|
||||
if latest_review is None or latest_review.decisions is None:
|
||||
return None
|
||||
items = latest_review.decisions.get("items") or []
|
||||
conflicts = latest_review.conflicts
|
||||
constraints: list[str] = []
|
||||
seen: set[str] = set()
|
||||
for item in sorted(items, key=lambda d: d.get("conflict_index", 0)):
|
||||
if item.get("verdict") not in _RETAINED_VERDICTS:
|
||||
continue
|
||||
idx = item.get("conflict_index")
|
||||
if not isinstance(idx, int) or not (0 <= idx < len(conflicts)):
|
||||
continue
|
||||
line = _conflict_constraint(conflicts[idx], item.get("note"))
|
||||
if line is None or line in seen:
|
||||
continue
|
||||
seen.add(line)
|
||||
constraints.append(line)
|
||||
bounded = constraints[:limit]
|
||||
if not bounded:
|
||||
return None
|
||||
return "\n".join(f"- {c}" for c in bounded)
|
||||
|
||||
|
||||
def _build_volatile(
|
||||
chapter_no: int,
|
||||
cards: str,
|
||||
@@ -144,6 +203,7 @@ def _build_volatile(
|
||||
digests: list[DigestView],
|
||||
beats: dict[str, Any],
|
||||
directive: str | None = None,
|
||||
prior_conflict_notes: str | None = None,
|
||||
) -> str:
|
||||
fore_lines = [
|
||||
f"【伏笔】{f.code} {f.title} [{f.status}]"
|
||||
@@ -159,6 +219,8 @@ def _build_volatile(
|
||||
_section("写作指令", f"请创作第 {chapter_no} 章的正文。"),
|
||||
# 开篇黄金三章标记(仅前 N 章)——每章易变,只入 volatile。
|
||||
_opening_marker(chapter_no),
|
||||
# 上一章冲突裁决反哺(灵感②)——依赖上一章裁决,每章易变,绝不进缓存前缀(#9)。
|
||||
_section("上一章冲突提醒", prior_conflict_notes) if prior_conflict_notes else None,
|
||||
_section("本章注入卡片", cards),
|
||||
_section("相关伏笔窗口", "\n".join(fore_lines)),
|
||||
_section("近况摘要", "\n".join(digest_lines)),
|
||||
@@ -217,10 +279,24 @@ async def assemble(
|
||||
|
||||
main_characters = [c for c in characters if (c.role or "") in MAIN_ROLES]
|
||||
|
||||
# 冲突裁决反哺(灵感②/D3):读上一已验收章的 continuity 冲突作者裁决保留项 →
|
||||
# 本章 volatile 前瞻约束。review 未注入(默认 None)或本章为第 1 章 → 优雅降级不反哺。
|
||||
prior_conflict_notes: str | None = None
|
||||
if repos.review is not None and chapter_no > 1:
|
||||
prior_reviews = await repos.review.list_for_chapter(project_id, chapter_no - 1)
|
||||
latest_review = prior_reviews[0] if prior_reviews else None # list_for_chapter 新→旧
|
||||
prior_conflict_notes = _prior_conflict_notes(latest_review)
|
||||
|
||||
stable_core = _build_stable(spec, world_entities, main_characters, style, rules)
|
||||
cards = render_cards(selection, characters, world_entities)
|
||||
volatile = _build_volatile(
|
||||
chapter_no, cards, foreshadows, recent_digests, outline.beats, directive
|
||||
chapter_no,
|
||||
cards,
|
||||
foreshadows,
|
||||
recent_digests,
|
||||
outline.beats,
|
||||
directive,
|
||||
prior_conflict_notes,
|
||||
)
|
||||
|
||||
return AssembledContext(stable_core=stable_core, volatile=volatile, selection=selection)
|
||||
|
||||
@@ -32,6 +32,7 @@ from ww_core.domain.repositories import (
|
||||
StyleView,
|
||||
WorldEntityView,
|
||||
)
|
||||
from ww_core.domain.review_repo import SqlReviewRepo
|
||||
|
||||
|
||||
class SqlOutlineRepo:
|
||||
@@ -223,7 +224,10 @@ class SqlProjectSpecRepo:
|
||||
|
||||
|
||||
def sql_memory_repos(session: AsyncSession) -> MemoryRepos:
|
||||
"""用一个 AsyncSession 装配全部 8 个 SQLAlchemy repo(T1.4 注入点)。"""
|
||||
"""用一个 AsyncSession 装配全部 9 个 SQLAlchemy repo(T1.4 注入点)。
|
||||
|
||||
`review=SqlReviewRepo`:供 assemble 反读上一已验收章的 continuity 冲突裁决(灵感②)。
|
||||
"""
|
||||
return MemoryRepos(
|
||||
outline=SqlOutlineRepo(session),
|
||||
character=SqlCharacterRepo(session),
|
||||
@@ -233,4 +237,5 @@ def sql_memory_repos(session: AsyncSession) -> MemoryRepos:
|
||||
style=SqlStyleRepo(session),
|
||||
rules=SqlRulesRepo(session),
|
||||
project=SqlProjectSpecRepo(session),
|
||||
review=SqlReviewRepo(session),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user