fix(qa): 实施 4 组设计型 QA 项——规则删除/codex 角色/大纲卷过滤/文风回炉锚点

#5 规则 DELETE + id:暴露 RuleView.id(PK);新增 DELETE /projects/{id}/rules/{rule_id}
  (项目/规则不存在→404,成功→204,按 (id,project_id) 限定);rule_repo 加
  list_for_project/delete;RulesPage 每条加删除(乐观删+回滚+toast)。assemble 侧
  RuleView(缓存前缀)不动,列表另立 RuleListItemView。
#7 codex 角色 relations:写侧本已持久化、读端点 _existing_characters 硬编码 []。
  加 _relations_from_jsonb 解析 {name,kind,note},CodexPage 渲染关系 chip。
#8 角色入库幂等:SqlCharacterWriteRepo.create 改 (project_id,name) app 层 upsert——
  重复入库改更新而非插入;不加 UNIQUE/迁移(线上已有重复行会让约束迁移失败)。
#1 大纲卷过滤:GET /outline 支持可选 ?volume(无参=全部,向后兼容);OutlineEditor
  加「查看:全部/卷N」筛选,与生成目标卷解耦。
H3/#9 文风回炉锚点:StyleDriftSegment 加 text(逐字命中段),style.md 指示审稿输出;
  前端按内容锚点定位回炉目标(idx 仅排序),命中失败 → 提示「无法定位该段」而非
  静默 no-op。style golden fixture 已重生成。

契约变更已 pnpm gen:api(RuleView.id / DELETE rules / outline ?volume)。无迁移
(alembic 无漂移)。门禁绿:ruff/mypy(210)/alembic/pytest 760 · 前端 tsc/lint/vitest 329。
This commit is contained in:
Yaojia Wang
2026-06-25 12:53:03 +02:00
parent e60eff7aa1
commit bf39f50b2f
33 changed files with 907 additions and 99 deletions

View File

@@ -0,0 +1,26 @@
// 文风漂移段的「内容锚」定位QA H3 / #9用漂移段携带的原文 `text` 在终稿里做
// 内容匹配定位回炉目标,而不是用位置索引 idx——因为审稿端的分段方式与前端按空行切段
// 不一定一致,靠 idx 取段会命中错段、越界则静默落空。纯逻辑,便于 node 环境单测。
import type { StyleDriftSegment } from "@/lib/review/sse";
// 在终稿里定位漂移段原文:
// 1) 段自带 text 且能在终稿里逐字搜到 → 返回该原文(最可靠,内容锚)。
// 2) text 为空/搜不到(旧数据、或作者已手改导致原文不在)→ 回退到位置 idx 取段;
// idx 越界或取到空段 → 返回 null由调用方就 #9 给出「无法定位」提示,不静默 no-op
export function locateDriftSegment(
finalParas: readonly string[],
segment: Pick<StyleDriftSegment, "idx" | "text">,
): string | null {
const anchor = segment.text.trim();
if (anchor.length > 0) {
// 内容匹配:原文需在某一段里出现(终稿整体含该原文即视为可定位)。
const found = finalParas.some((p) => p.includes(anchor));
if (found) return anchor;
// 自带原文但终稿里搜不到(已被手改)→ 不回退到可能错位的 idx直接判定为定位失败。
return null;
}
// 无内容锚(旧数据)→ 退回位置 idx 取段(兼容旧留痕)。
const para = finalParas[segment.idx]?.trim() ?? "";
return para.length > 0 ? para : null;
}