Files
writer-work-flow/apps/web/lib/review/history.ts
Yaojia Wang 72b3c81146 feat(review): 人物塑造 advisory 审(第五审,仅建议不阻断验收)
写→审链新增第五审「人物塑造」(灵感③/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 作客观锚点)。
2026-07-06 17:08:38 +02:00

102 lines
3.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 审稿历史归一:把后端 ReviewHistoryItem 的松散 conflictsdict[])收紧成 ReviewConflict[]。
// 纯逻辑可单测schema 把 conflicts 标成 {[k]:unknown}[],这里安全收窄。
import type { ReviewHistoryItem } from "@/lib/api/types";
import { asCharacterizationIssues } from "./sse";
import type {
CharacterizationReport,
ForeshadowKind,
ForeshadowSuggestion,
PaceIssue,
PaceReport,
ReviewConflict,
} from "./sse";
function asString(v: unknown, fallback = ""): string {
return typeof v === "string" ? v : fallback;
}
function asOptionalString(v: unknown): string | null {
return typeof v === "string" ? v : null;
}
function asIntArray(v: unknown): number[] {
if (!Array.isArray(v)) return [];
return v.filter((x): x is number => typeof x === "number" && Number.isFinite(x));
}
// 把一条留痕的 conflictsReviewConflictView[],已强类型)映射成 ReviewConflict。
// 保持顺序=下标身份(对齐冲突 gatetype 空串回退「未分类」refs 容忍缺省。
export function normalizeConflicts(
item: ReviewHistoryItem | undefined,
): ReviewConflict[] {
const raw = item?.conflicts ?? [];
return raw.map((c) => ({
type: c.type || "未分类",
where: c.where,
refs: c.refs ?? [],
suggestion: c.suggestion,
// 一键采纳补丁对留痕里带则透传供「采纳改法」find-replace 进终稿(缺省 null
original: c.original ?? null,
replacement: c.replacement ?? null,
}));
}
// 把一条留痕的 foreshadow_sug扁平 list每条带 kind收紧成建议缺字段给安全默认
// 对齐 collect 列映射C4 扩 T3.3planted/resolved 扁平成单 list、每条加 kind。
export function normalizeForeshadowSug(
item: ReviewHistoryItem | undefined,
): ForeshadowSuggestion[] {
const raw = item?.foreshadow_sug ?? [];
return raw.map((s) => ({
kind: asKind(s["kind"]),
code: asOptionalString(s["code"]),
title: asString(s["title"], "(未命名线索)"),
where: asOptionalString(s["where"]),
note: asOptionalString(s["note"]),
}));
}
function asKind(v: unknown): ForeshadowKind {
return v === "resolved" ? "resolved" : "planted";
}
// 把一条留痕的 pacedict收紧成节奏报告缺失/非 dict → null不渲染节拍图
export function normalizePace(
item: ReviewHistoryItem | undefined,
): PaceReport | null {
const raw = item?.pace;
if (typeof raw !== "object" || raw === null) return null;
const dict = raw as Record<string, unknown>;
const waterRaw = Array.isArray(dict["water"]) ? dict["water"] : [];
const water: PaceIssue[] = waterRaw
.filter((w): w is Record<string, unknown> => typeof w === "object" && w !== null)
.map((w) => ({
where: asString(w["where"]),
reason: asString(w["reason"]),
}));
return {
water,
hook: dict["hook"] === true,
beat_map: asIntArray(dict["beat_map"]),
};
}
// 把一条留痕的 characterizationdict收紧成人物塑造报告缺失/非 dict → null不渲染
// advisory只展示、无裁决 UI灵感 D2
export function normalizeCharacterization(
item: ReviewHistoryItem | undefined,
): CharacterizationReport | null {
const raw = item?.characterization;
if (typeof raw !== "object" || raw === null) return null;
const dict = raw as Record<string, unknown>;
return { issues: asCharacterizationIssues(dict["issues"]) };
}
// 最近一条留痕GET .../reviews 已按新→旧排序)。
export function latestReview(
items: readonly ReviewHistoryItem[] | undefined,
): ReviewHistoryItem | undefined {
return items?.[0];
}