feat(character): appearance/motive 全栈穿线 + motive/traits 口径重切

打通 appearance/motive 从生成→schema→契约→落库→前端整条穿线
(DB 列早在初始迁移、此前一直 NULL,无新迁移),并原子重切人物
内驱口径。

- @llm: CharacterCard/character-gen.md 加 appearance/motive(给默认值守
  解析韧性),重切——动机唯一落 motive、traits 只留核心/表层/阴影三层、
  arc 引用 motive;regen prompt_hashes.json 金标准。
- @backend: CharacterCardView + CharacterWriteRepo/SqlCharacterWriteRepo
  (create + update backfill 分支)+ CharacterWriteFields + 路由双向形变
  与 _existing_characters 带上两字段。
- @frontend: pnpm gen:api 重生成 TS 客户端;CharacterCardItem 展示/编辑
  两字段。
- 测试: schema 默认值/解析、动机不双写重切回归、card_to_view 双向、
  ingest 透传、list 暴露、SqlCharacterWriteRepo create/backfill 真 pg 集成。
This commit is contained in:
Yaojia Wang
2026-07-06 14:36:11 +02:00
parent 6854dac98f
commit acde715515
13 changed files with 297 additions and 15 deletions

View File

@@ -922,9 +922,21 @@ export interface components {
role: string;
/**
* Traits
* @description 性格特质清单
* @description 性格特质清单(性格三层)
*/
traits?: string[];
/**
* Appearance
* @description 外貌形象(缺则空串)
* @default
*/
appearance: string;
/**
* Motive
* @description 核心动机/欲望/恐惧(缺则空串)
* @default
*/
motive: string;
/**
* Backstory
* @description 背景故事

View File

@@ -23,6 +23,8 @@ import {
const card = (over: Partial<CharacterCardView> = {}): CharacterCardView => ({
name: "甲",
role: "主角",
appearance: "白衣如雪",
motive: "为家族正名",
backstory: "出身寒门",
arc: "由怯转勇",
...over,
@@ -86,6 +88,18 @@ describe("updateCard", () => {
expect(original.name).toBe("甲");
expect(next).not.toBe(original);
});
it("patches appearance/motive without dropping the other fields", () => {
const original = card();
const next = updateCard(original, {
appearance: "玄色劲装",
motive: "复仇",
});
expect(next.appearance).toBe("玄色劲装");
expect(next.motive).toBe("复仇");
expect(next.backstory).toBe(original.backstory);
expect(original.appearance).toBe("白衣如雪"); // no mutation
});
});
describe("extractIngestConflicts", () => {