#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。
27 lines
1.5 KiB
TypeScript
27 lines
1.5 KiB
TypeScript
// 文风漂移段的「内容锚」定位(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;
|
||
}
|