feat: M2 — 写→审(一致性)→裁决→验收(事务);未决冲突禁验收

- 续审 Agent 声明(AgentSpec) + 结构化输出契约(ContinuityReview/Conflict 五类)
- LangGraph 并行审子图(可扩四审) + collect 落 chapter_reviews 留痕 + review SSE(section/conflict)
- 验收-side Repository:章节 accepted 版本晋升 + digest append-only + 审稿留痕/裁决
- API:review(SSE) + reviews 历史 + accept(单原子事务:晋升 version + 终稿 digest + 裁决留痕)
- 冲突 gate:未决裁决拦截(CONFLICT_UNRESOLVED);digest 从终稿提炼(不变量#4)
- 前端:审稿报告页 + 冲突就地标注 + 裁决(采纳/忽略/手改) + 未决禁验收 + 「本次将更新」清单
- M2 E2E:真实 DB + 多档位 mock 网关零 token 走通 写→审→裁决→验收→摘要入库
- 多 agent 协同台账(PROGRESS.md) + 共享记忆(memory/contracts·decisions·gotchas)
This commit is contained in:
Yaojia Wang
2026-06-18 11:38:28 +02:00
parent b523b4fd21
commit 68f194a043
36 changed files with 3881 additions and 0 deletions

View File

@@ -0,0 +1,34 @@
// 审稿历史归一:把后端 ReviewHistoryItem 的松散 conflictsdict[])收紧成 ReviewConflict[]。
// 纯逻辑可单测schema 把 conflicts 标成 {[k]:unknown}[],这里安全收窄。
import type { ReviewHistoryItem } from "@/lib/api/types";
import type { ReviewConflict } from "./sse";
function asString(v: unknown, fallback = ""): string {
return typeof v === "string" ? v : fallback;
}
function asStringArray(v: unknown): string[] {
if (!Array.isArray(v)) return [];
return v.filter((x): x is string => typeof x === "string");
}
// 把一条留痕的 conflicts 收紧(缺字段给安全默认;保持顺序=下标身份,对齐冲突 gate
export function normalizeConflicts(
item: ReviewHistoryItem | undefined,
): ReviewConflict[] {
const raw = item?.conflicts ?? [];
return raw.map((c) => ({
type: asString(c["type"], "未分类"),
where: asString(c["where"]),
refs: asStringArray(c["refs"]),
suggestion: asString(c["suggestion"]),
}));
}
// 最近一条留痕GET .../reviews 已按新→旧排序)。
export function latestReview(
items: readonly ReviewHistoryItem[] | undefined,
): ReviewHistoryItem | undefined {
return items?.[0];
}