写→审链新增第五审「人物塑造」(灵感③/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 作客观锚点)。
324 lines
14 KiB
Python
324 lines
14 KiB
Python
"""ORM 模型——全部 MVP 表(ARCH §3.1)。
|
||
|
||
snake_case;无向量列;P2 表 timeline/decisions 不建(见 DEV_PLAN T0.2)。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import uuid
|
||
from datetime import datetime
|
||
from typing import Any
|
||
|
||
from sqlalchemy import (
|
||
BigInteger,
|
||
CheckConstraint,
|
||
DateTime,
|
||
ForeignKey,
|
||
Index,
|
||
Integer,
|
||
LargeBinary,
|
||
Text,
|
||
UniqueConstraint,
|
||
text,
|
||
)
|
||
from sqlalchemy.dialects.postgresql import JSONB
|
||
from sqlalchemy.orm import Mapped, mapped_column
|
||
|
||
from ww_db.base import Base, CreatedAt, TimestampedMixin, UuidPk
|
||
|
||
# ---- 运营/系统表 ----
|
||
|
||
|
||
class User(UuidPk, CreatedAt, Base):
|
||
__tablename__ = "users"
|
||
email: Mapped[str] = mapped_column(Text, unique=True, nullable=False)
|
||
display_name: Mapped[str | None] = mapped_column(Text)
|
||
|
||
|
||
# ---- 创作数据表 ----
|
||
|
||
|
||
class Project(UuidPk, TimestampedMixin, Base):
|
||
__tablename__ = "projects"
|
||
owner_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("users.id"), nullable=False, index=True)
|
||
title: Mapped[str] = mapped_column(Text, nullable=False)
|
||
genre: Mapped[str | None] = mapped_column(Text)
|
||
logline: Mapped[str | None] = mapped_column(Text)
|
||
premise: Mapped[str | None] = mapped_column(Text)
|
||
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):
|
||
__tablename__ = "characters"
|
||
project_id: Mapped[uuid.UUID] = mapped_column(
|
||
ForeignKey("projects.id", ondelete="CASCADE"), nullable=False, index=True
|
||
)
|
||
name: Mapped[str] = mapped_column(Text, nullable=False)
|
||
role: Mapped[str | None] = mapped_column(Text)
|
||
traits: Mapped[dict[str, Any] | None] = mapped_column(JSONB)
|
||
appearance: Mapped[str | None] = mapped_column(Text)
|
||
motive: Mapped[str | None] = mapped_column(Text)
|
||
backstory: Mapped[str | None] = mapped_column(Text)
|
||
arc: Mapped[dict[str, Any] | None] = mapped_column(JSONB)
|
||
speech_tics: Mapped[dict[str, Any] | None] = mapped_column(JSONB)
|
||
tags: Mapped[list[Any]] = mapped_column(JSONB, server_default=text("'[]'"))
|
||
relations: Mapped[list[Any]] = mapped_column(JSONB, server_default=text("'[]'"))
|
||
first_chapter: Mapped[int | None] = mapped_column(Integer)
|
||
latest_state: Mapped[str | None] = mapped_column(Text)
|
||
|
||
|
||
class WorldEntity(UuidPk, CreatedAt, Base):
|
||
__tablename__ = "world_entities"
|
||
project_id: Mapped[uuid.UUID] = mapped_column(
|
||
ForeignKey("projects.id", ondelete="CASCADE"), nullable=False, index=True
|
||
)
|
||
type: Mapped[str] = mapped_column(Text, nullable=False)
|
||
name: Mapped[str] = mapped_column(Text, nullable=False)
|
||
rules: Mapped[dict[str, Any] | None] = mapped_column(JSONB)
|
||
first_chapter: Mapped[int | None] = mapped_column(Integer)
|
||
latest_state: Mapped[str | None] = mapped_column(Text)
|
||
|
||
|
||
class Outline(UuidPk, Base):
|
||
__tablename__ = "outline"
|
||
__table_args__ = (UniqueConstraint("project_id", "chapter_no"),)
|
||
project_id: Mapped[uuid.UUID] = mapped_column(
|
||
ForeignKey("projects.id", ondelete="CASCADE"), nullable=False, index=True
|
||
)
|
||
volume: Mapped[int] = mapped_column(Integer, nullable=False)
|
||
chapter_no: Mapped[int] = mapped_column(Integer, nullable=False)
|
||
beats: Mapped[dict[str, Any] | None] = mapped_column(JSONB)
|
||
foreshadow_windows: Mapped[list[Any]] = mapped_column(JSONB, server_default=text("'[]'"))
|
||
|
||
|
||
class ChapterDigest(UuidPk, CreatedAt, Base):
|
||
__tablename__ = "chapter_digests"
|
||
__table_args__ = (
|
||
Index(
|
||
"ix_chapter_digests_project_chapter",
|
||
"project_id",
|
||
text("chapter_no DESC"),
|
||
),
|
||
)
|
||
project_id: Mapped[uuid.UUID] = mapped_column(
|
||
ForeignKey("projects.id", ondelete="CASCADE"), nullable=False, index=True
|
||
)
|
||
chapter_no: Mapped[int] = mapped_column(Integer, nullable=False)
|
||
facts: Mapped[dict[str, Any]] = mapped_column(JSONB, nullable=False)
|
||
|
||
|
||
class Foreshadow(UuidPk, Base):
|
||
__tablename__ = "foreshadow"
|
||
__table_args__ = (
|
||
UniqueConstraint("project_id", "code"),
|
||
CheckConstraint(
|
||
"status IN ('OPEN', 'PARTIAL', 'CLOSED', 'OVERDUE')",
|
||
name="ck_foreshadow_status",
|
||
),
|
||
)
|
||
project_id: Mapped[uuid.UUID] = mapped_column(
|
||
ForeignKey("projects.id", ondelete="CASCADE"), nullable=False, index=True
|
||
)
|
||
code: Mapped[str] = mapped_column(Text, nullable=False)
|
||
title: Mapped[str] = mapped_column(Text, nullable=False)
|
||
status: Mapped[str] = mapped_column(Text, nullable=False, server_default=text("'OPEN'"))
|
||
planted_at: Mapped[int | None] = mapped_column(Integer)
|
||
content: Mapped[str | None] = mapped_column(Text)
|
||
expected_close_from: Mapped[int | None] = mapped_column(Integer)
|
||
expected_close_to: Mapped[int | None] = mapped_column(Integer)
|
||
importance: Mapped[str | None] = mapped_column(Text)
|
||
links: Mapped[list[Any]] = mapped_column(JSONB, server_default=text("'[]'"))
|
||
progress: Mapped[list[Any]] = mapped_column(JSONB, server_default=text("'[]'"))
|
||
updated_at: Mapped[datetime] = mapped_column(
|
||
DateTime(timezone=True),
|
||
nullable=False,
|
||
server_default=text("now()"),
|
||
onupdate=text("now()"),
|
||
)
|
||
|
||
|
||
class StyleFingerprint(UuidPk, CreatedAt, Base):
|
||
__tablename__ = "style_fingerprint"
|
||
project_id: Mapped[uuid.UUID] = mapped_column(
|
||
ForeignKey("projects.id", ondelete="CASCADE"), nullable=False, index=True
|
||
)
|
||
dimensions_json: Mapped[dict[str, Any]] = mapped_column(JSONB, nullable=False)
|
||
evidence_json: Mapped[dict[str, Any]] = mapped_column(JSONB, nullable=False)
|
||
version: Mapped[int] = mapped_column(Integer, nullable=False, server_default=text("1"))
|
||
|
||
|
||
class Rule(UuidPk, CreatedAt, Base):
|
||
__tablename__ = "rules"
|
||
__table_args__ = (
|
||
Index(
|
||
"ix_rules_global",
|
||
"id",
|
||
postgresql_where=text("project_id IS NULL"),
|
||
),
|
||
)
|
||
project_id: Mapped[uuid.UUID | None] = mapped_column(
|
||
ForeignKey("projects.id", ondelete="CASCADE"), index=True
|
||
)
|
||
level: Mapped[str] = mapped_column(Text, nullable=False)
|
||
content: Mapped[str] = mapped_column(Text, nullable=False)
|
||
|
||
|
||
class Chapter(UuidPk, TimestampedMixin, Base):
|
||
__tablename__ = "chapters"
|
||
__table_args__ = (
|
||
UniqueConstraint("project_id", "chapter_no", "version"),
|
||
CheckConstraint(
|
||
"status IN ('draft', 'accepted')",
|
||
name="ck_chapters_status",
|
||
),
|
||
)
|
||
project_id: Mapped[uuid.UUID] = mapped_column(
|
||
ForeignKey("projects.id", ondelete="CASCADE"), nullable=False, index=True
|
||
)
|
||
volume: Mapped[int] = mapped_column(Integer, nullable=False)
|
||
chapter_no: Mapped[int] = mapped_column(Integer, nullable=False)
|
||
content: Mapped[str | None] = mapped_column(Text)
|
||
status: Mapped[str] = mapped_column(Text, nullable=False, server_default=text("'draft'"))
|
||
version: Mapped[int] = mapped_column(Integer, nullable=False, server_default=text("1"))
|
||
|
||
|
||
class ChapterInjection(UuidPk, TimestampedMixin, Base):
|
||
"""本章注入覆盖(B0 可控版):作者对确定性自动选择的微调,每章一行。
|
||
|
||
与 `outline.beats` 分表存放(复用 outline 行会污染 beats/prompt,已否决)。
|
||
pinned/excluded 为 JSONB list[{kind,name}];recent_n 覆盖近况回看章数(NULL=用默认)。
|
||
"""
|
||
|
||
__tablename__ = "chapter_injection"
|
||
__table_args__ = (UniqueConstraint("project_id", "chapter_no"),)
|
||
project_id: Mapped[uuid.UUID] = mapped_column(
|
||
ForeignKey("projects.id", ondelete="CASCADE"), nullable=False, index=True
|
||
)
|
||
chapter_no: Mapped[int] = mapped_column(Integer, nullable=False)
|
||
pinned: Mapped[list[Any]] = mapped_column(JSONB, server_default=text("'[]'"))
|
||
excluded: Mapped[list[Any]] = mapped_column(JSONB, server_default=text("'[]'"))
|
||
recent_n: Mapped[int | None] = mapped_column(Integer)
|
||
|
||
|
||
class ChapterReview(UuidPk, CreatedAt, Base):
|
||
__tablename__ = "chapter_reviews"
|
||
__table_args__ = (
|
||
Index(
|
||
"ix_chapter_reviews_project_chapter",
|
||
"project_id",
|
||
"chapter_no",
|
||
text("created_at DESC"),
|
||
),
|
||
)
|
||
project_id: Mapped[uuid.UUID] = mapped_column(
|
||
ForeignKey("projects.id", ondelete="CASCADE"), nullable=False, index=True
|
||
)
|
||
chapter_no: Mapped[int] = mapped_column(Integer, nullable=False)
|
||
chapter_version: Mapped[int | None] = mapped_column(Integer)
|
||
conflicts: Mapped[list[Any]] = mapped_column(JSONB, server_default=text("'[]'"))
|
||
foreshadow_sug: Mapped[list[Any]] = mapped_column(JSONB, server_default=text("'[]'"))
|
||
style: Mapped[dict[str, Any] | None] = mapped_column(JSONB)
|
||
pace: Mapped[dict[str, Any] | None] = mapped_column(JSONB)
|
||
# 灵感③ advisory 人物塑造审留痕(JSONB nullable,独立列不复用恒 None 的 health_score)。
|
||
characterization: Mapped[dict[str, Any] | None] = mapped_column(JSONB)
|
||
health_score: Mapped[int | None] = mapped_column(Integer)
|
||
decisions: Mapped[dict[str, Any] | None] = mapped_column(JSONB)
|
||
|
||
|
||
# ---- 运营/系统表(续)----
|
||
|
||
|
||
class ProviderCredential(UuidPk, CreatedAt, Base):
|
||
__tablename__ = "provider_credentials"
|
||
__table_args__ = (UniqueConstraint("owner_id", "project_id", "provider"),)
|
||
owner_id: Mapped[uuid.UUID] = mapped_column(
|
||
ForeignKey("users.id", ondelete="CASCADE"), nullable=False
|
||
)
|
||
project_id: Mapped[uuid.UUID | None] = mapped_column(
|
||
ForeignKey("projects.id", ondelete="CASCADE")
|
||
)
|
||
provider: Mapped[str] = mapped_column(Text, nullable=False)
|
||
auth_type: Mapped[str] = mapped_column(Text, nullable=False, server_default=text("'api_key'"))
|
||
api_key_enc: Mapped[bytes | None] = mapped_column(LargeBinary, nullable=True)
|
||
oauth_enc: Mapped[bytes | None] = mapped_column(LargeBinary, nullable=True)
|
||
|
||
|
||
class TierRouting(UuidPk, Base):
|
||
__tablename__ = "tier_routing"
|
||
__table_args__ = (UniqueConstraint("project_id", "tier"),)
|
||
project_id: Mapped[uuid.UUID | None] = mapped_column(
|
||
ForeignKey("projects.id", ondelete="CASCADE")
|
||
)
|
||
tier: Mapped[str] = mapped_column(Text, nullable=False)
|
||
provider: Mapped[str] = mapped_column(Text, nullable=False)
|
||
model: Mapped[str] = mapped_column(Text, nullable=False)
|
||
fallback: Mapped[list[Any]] = mapped_column(JSONB, server_default=text("'[]'"))
|
||
|
||
|
||
class UsageLedger(UuidPk, CreatedAt, Base):
|
||
__tablename__ = "usage_ledger"
|
||
owner_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("users.id"), nullable=False, index=True)
|
||
project_id: Mapped[uuid.UUID | None] = mapped_column(ForeignKey("projects.id"), index=True)
|
||
provider: Mapped[str] = mapped_column(Text, nullable=False)
|
||
model: Mapped[str] = mapped_column(Text, nullable=False)
|
||
input_tokens: Mapped[int] = mapped_column(Integer, nullable=False)
|
||
output_tokens: Mapped[int] = mapped_column(Integer, nullable=False)
|
||
cache_read: Mapped[int] = mapped_column(Integer, server_default=text("0"))
|
||
cost_minor: Mapped[int] = mapped_column(BigInteger, nullable=False)
|
||
currency: Mapped[str] = mapped_column(Text, nullable=False)
|
||
|
||
|
||
class Skill(UuidPk, CreatedAt, Base):
|
||
__tablename__ = "skills"
|
||
scope: Mapped[str] = mapped_column(Text, nullable=False)
|
||
owner_id: Mapped[uuid.UUID | None] = mapped_column(ForeignKey("users.id"))
|
||
name: Mapped[str] = mapped_column(Text, nullable=False)
|
||
description: Mapped[str | None] = mapped_column(Text)
|
||
tier: Mapped[str] = mapped_column(Text, nullable=False)
|
||
system_prompt: Mapped[str] = mapped_column(Text, nullable=False)
|
||
input_schema: Mapped[dict[str, Any] | None] = mapped_column(JSONB)
|
||
output_schema: Mapped[dict[str, Any] | None] = mapped_column(JSONB)
|
||
reads: Mapped[list[Any]] = mapped_column(JSONB, server_default=text("'[]'"))
|
||
writes: Mapped[list[Any]] = mapped_column(JSONB, server_default=text("'[]'"))
|
||
genre: Mapped[str | None] = mapped_column(Text)
|
||
examples: Mapped[list[Any]] = mapped_column(JSONB, server_default=text("'[]'"))
|
||
|
||
|
||
class PromptTemplate(UuidPk, CreatedAt, Base):
|
||
"""作者保存/复用的提示词模板(单用户本地版;无分享/市场)。
|
||
|
||
owner_id 为单用户原型 stub;tool_key 可选关联某生成器(NULL=通用)。
|
||
"""
|
||
|
||
__tablename__ = "prompt_templates"
|
||
owner_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("users.id"), nullable=False, index=True)
|
||
title: Mapped[str] = mapped_column(Text, nullable=False)
|
||
body: Mapped[str] = mapped_column(Text, nullable=False)
|
||
category: Mapped[str | None] = mapped_column(Text)
|
||
tool_key: Mapped[str | None] = mapped_column(Text)
|
||
|
||
|
||
class Job(UuidPk, TimestampedMixin, Base):
|
||
__tablename__ = "jobs"
|
||
__table_args__ = (
|
||
Index(
|
||
"ix_jobs_status_running",
|
||
"status",
|
||
postgresql_where=text("status = 'running'"),
|
||
),
|
||
)
|
||
project_id: Mapped[uuid.UUID | None] = mapped_column(
|
||
ForeignKey("projects.id", ondelete="CASCADE")
|
||
)
|
||
kind: Mapped[str] = mapped_column(Text, nullable=False)
|
||
status: Mapped[str] = mapped_column(Text, nullable=False, server_default=text("'queued'"))
|
||
progress: Mapped[int] = mapped_column(Integer, server_default=text("0"))
|
||
result: Mapped[dict[str, Any] | None] = mapped_column(JSONB)
|
||
error: Mapped[str | None] = mapped_column(Text)
|