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:
@@ -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 章) ----
|
||||
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -29,6 +29,9 @@ class ProjectView:
|
||||
theme: str | None = None
|
||||
selling_points: list[Any] = field(default_factory=list)
|
||||
structure: str | None = None
|
||||
tone: str | None = None
|
||||
ending_type: str | None = None
|
||||
narrative_pov: str | None = None
|
||||
updated_at: datetime | None = None
|
||||
pending_review_count: int = 0
|
||||
|
||||
@@ -44,6 +47,9 @@ class ProjectCreate:
|
||||
theme: str | None = None
|
||||
selling_points: list[Any] = field(default_factory=list)
|
||||
structure: str | None = None
|
||||
tone: str | None = None
|
||||
ending_type: str | None = None
|
||||
narrative_pov: str | None = None
|
||||
|
||||
|
||||
class ProjectRepo(Protocol):
|
||||
@@ -71,6 +77,9 @@ def _to_view(
|
||||
theme=row.theme,
|
||||
selling_points=list(row.selling_points or []),
|
||||
structure=row.structure,
|
||||
tone=row.tone,
|
||||
ending_type=row.ending_type,
|
||||
narrative_pov=row.narrative_pov,
|
||||
updated_at=updated_at or row.updated_at,
|
||||
pending_review_count=pending_review_count,
|
||||
)
|
||||
@@ -92,6 +101,9 @@ class SqlProjectRepo:
|
||||
theme=data.theme,
|
||||
selling_points=list(data.selling_points),
|
||||
structure=data.structure,
|
||||
tone=data.tone,
|
||||
ending_type=data.ending_type,
|
||||
narrative_pov=data.narrative_pov,
|
||||
)
|
||||
self._s.add(row)
|
||||
await self._s.commit()
|
||||
|
||||
@@ -101,6 +101,10 @@ class ProjectSpecView(BaseModel):
|
||||
theme: str | None = None
|
||||
# 题材(每书稳定项)——供 assemble 注入 genre-aware craft 片段(灵感 F1)。
|
||||
genre: str | None = None
|
||||
# 立项书级常量(灵感④):基调/结局取向/叙事视角——每书稳定 → 入 stable_core 缓存前缀。
|
||||
tone: str | None = None
|
||||
ending_type: str | None = None
|
||||
narrative_pov: str | None = None
|
||||
|
||||
|
||||
# ---- Repository 协议(async;统一按 project_id 过滤)----
|
||||
|
||||
@@ -69,6 +69,13 @@ def _build_spec_section(spec: ProjectSpecView | None) -> str | None:
|
||||
lines.append(f"【前提】{spec.premise}")
|
||||
if spec.theme:
|
||||
lines.append(f"【主题】{spec.theme}")
|
||||
# 立项书级常量(灵感④):每书稳定 → 随蓝本进缓存前缀,跨章字节不变(不变量 #9)。
|
||||
if spec.tone:
|
||||
lines.append(f"【基调】{spec.tone}")
|
||||
if spec.narrative_pov:
|
||||
lines.append(f"【叙事视角】{spec.narrative_pov}")
|
||||
if spec.ending_type:
|
||||
lines.append(f"【结局取向】{spec.ending_type}")
|
||||
return _section("作品蓝本", "\n".join(lines))
|
||||
|
||||
|
||||
|
||||
@@ -216,6 +216,9 @@ class SqlProjectSpecRepo:
|
||||
premise=row.premise,
|
||||
theme=row.theme,
|
||||
genre=row.genre,
|
||||
tone=row.tone,
|
||||
ending_type=row.ending_type,
|
||||
narrative_pov=row.narrative_pov,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
"""Add tone/ending_type/narrative_pov to projects (灵感④ 立项书级常量).
|
||||
|
||||
三列均 Text nullable(与既有 genre/structure 一致,无 CHECK;枚举约束留前端预设),
|
||||
nullable 免 backfill(旧项目留 NULL,assemble/前端优雅降级)。
|
||||
|
||||
Revision ID: e5f6a7b8c9d0
|
||||
Revises: 8c1d2e3f4a5b
|
||||
Create Date: 2026-07-06 00:00:00.000000
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision: str = "e5f6a7b8c9d0"
|
||||
down_revision: str | None = "8c1d2e3f4a5b"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.add_column("projects", sa.Column("tone", sa.Text(), nullable=True))
|
||||
op.add_column("projects", sa.Column("ending_type", sa.Text(), nullable=True))
|
||||
op.add_column("projects", sa.Column("narrative_pov", sa.Text(), nullable=True))
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column("projects", "narrative_pov")
|
||||
op.drop_column("projects", "ending_type")
|
||||
op.drop_column("projects", "tone")
|
||||
@@ -48,6 +48,10 @@ class Project(UuidPk, TimestampedMixin, Base):
|
||||
theme: Mapped[str | None] = mapped_column(Text)
|
||||
selling_points: Mapped[list[Any]] = mapped_column(JSONB, server_default=text("'[]'"))
|
||||
structure: Mapped[str | None] = mapped_column(Text)
|
||||
# 立项书级常量(灵感④)——同 genre/structure:Text nullable、无 CHECK(枚举约束留前端预设)。
|
||||
tone: Mapped[str | None] = mapped_column(Text)
|
||||
ending_type: Mapped[str | None] = mapped_column(Text)
|
||||
narrative_pov: Mapped[str | None] = mapped_column(Text)
|
||||
|
||||
|
||||
class Character(UuidPk, CreatedAt, Base):
|
||||
|
||||
Reference in New Issue
Block a user