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:
74
apps/web/lib/review/decisions.ts
Normal file
74
apps/web/lib/review/decisions.ts
Normal 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"));
|
||||
}
|
||||
Reference in New Issue
Block a user