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,74 @@
// 裁决纯逻辑完整性判定未决禁验收、accept 请求体组装、缺判高亮映射。
// 对齐 C3 accept 契约 + R5 冲突 gate裁决 conflict_index 须覆盖 range(len(conflicts)))。
import type { AcceptRequest, ConflictDecision } from "@/lib/api/types";
export type Verdict = "accept" | "ignore" | "manual";
// 单个冲突的本地裁决草稿(按下标定位)。
export interface DecisionDraft {
verdict: Verdict | null;
note: string;
}
// 初始化 N 个冲突的裁决草稿(全部未决)。
export function emptyDecisions(conflictCount: number): DecisionDraft[] {
return Array.from({ length: conflictCount }, () => ({
verdict: null,
note: "",
}));
}
// 不可变更新:设置某个冲突的 verdict。
export function setVerdict(
drafts: readonly DecisionDraft[],
index: number,
verdict: Verdict,
): DecisionDraft[] {
return drafts.map((d, i) => (i === index ? { ...d, verdict } : d));
}
// 不可变更新:设置某个冲突的 note。
export function setNote(
drafts: readonly DecisionDraft[],
index: number,
note: string,
): DecisionDraft[] {
return drafts.map((d, i) => (i === index ? { ...d, note } : d));
}
// 是否全部已决:每个冲突都有 verdict覆盖 range(len(conflicts)))。零冲突 → 直通。
export function allResolved(drafts: readonly DecisionDraft[]): boolean {
return drafts.every((d) => d.verdict !== null);
}
// 本地仍未决的冲突下标(用于禁用态提示)。
export function unresolvedIndices(drafts: readonly DecisionDraft[]): number[] {
return drafts.flatMap((d, i) => (d.verdict === null ? [i] : []));
}
// 组装 accept 请求体仅含已决项note 去空白后非空才带)。
export function buildAcceptRequest(
finalText: string,
drafts: readonly DecisionDraft[],
): AcceptRequest {
const decisions: ConflictDecision[] = drafts.flatMap((d, i) => {
if (d.verdict === null) return [];
const trimmed = d.note.trim();
const decision: ConflictDecision = {
conflict_index: i,
verdict: d.verdict,
};
return [trimmed ? { ...decision, note: trimmed } : decision];
});
return { final_text: finalText, decisions };
}
// 从 409 CONFLICT_UNRESOLVED 的 details 提取缺判下标集合(用于报告卡高亮)。
export function missingConflictIndices(details: unknown): Set<number> {
if (typeof details !== "object" || details === null) return new Set();
const raw = (details as { missing_conflict_indices?: unknown })
.missing_conflict_indices;
if (!Array.isArray(raw)) return new Set();
return new Set(raw.filter((n): n is number => typeof n === "number"));
}