feat(web): 立项新增 基调/结局/视角 三字段(书级常量入 stable_core 缓存前缀)

灵感计划 §3④ + §4 D 枚举决策:
- projects 表加 tone/ending_type/narrative_pov(Text nullable、无 CHECK,同 genre/structure;
  迁移 e5f6a7b8c9d0 nullable 免 backfill)。
- ProjectCreateRequest/ProjectResponse + domain ProjectCreate/ProjectView 全链路透传(snake_case)。
- 完整 stable_core 链路:ProjectSpecView + SqlProjectSpecRepo.spec + _build_spec_section 渲染进
  「作品蓝本」块——书级常量随书稳定,字节稳定守缓存前缀不变量 #9,绝不入 volatile。
- 前端 wizard.ts 三字段 + 预设数组 TONES/ENDING_TYPES/NARRATIVE_POVS + ProjectWizard 控件与确认页回显。
- 契约变更已 pnpm gen:api + memory/contracts.md 登记。
This commit is contained in:
Yaojia Wang
2026-07-06 15:59:49 +02:00
parent 491d9342ef
commit 821ace5989
17 changed files with 247 additions and 1 deletions

View File

@@ -418,6 +418,25 @@ async def test_no_genre_injects_no_fragment() -> None:
assert "本作题材写法" not in ctx.stable_core
# ---- 灵感④:立项基调/结局/视角是书级常量 → 进 stable_core缓存前缀不变量 #9 ----
async def test_build_stable_includes_tone_ending_pov() -> None:
spec = ProjectSpecView(title="", tone="热血", ending_type="HE", narrative_pov="第一人称")
ctx = await assemble(build_repos(spec=spec), PROJECT, 5)
assert "热血" in ctx.stable_core
assert "HE" in ctx.stable_core
assert "第一人称" in ctx.stable_core
async def test_tone_ending_pov_never_enter_volatile() -> None:
# 书级常量随书稳定 → 只进缓存前缀,绝不进 volatile章号易变区
spec = ProjectSpecView(title="", tone="热血", ending_type="HE", narrative_pov="第一人称")
ctx = await assemble(build_repos(spec=spec), PROJECT, 5)
assert "热血" not in ctx.volatile
assert "第一人称" not in ctx.volatile
# ---- 灵感①/F1开篇黄金三章特判volatile仅前 3 章) ----

View File

@@ -1,6 +1,16 @@
from datetime import UTC, datetime
from ww_core.domain.project_repo import max_time
from ww_core.domain.project_repo import _to_view, max_time
from ww_db.models import Project
def test_to_view_carries_tone_ending_pov() -> None:
# ④ 立项三字段(基调/结局/视角)经 ORM 行 → ProjectView 快照逐字段透传snake_case
row = Project(title="", tone="热血", ending_type="BE", narrative_pov="多视角")
view = _to_view(row)
assert view.tone == "热血"
assert view.ending_type == "BE"
assert view.narrative_pov == "多视角"
def test_max_time_handles_missing_values() -> None: