Files
writer-work-flow/apps/web/lib/review/decisions.ts
Yaojia Wang 68f194a043 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)
2026-06-18 11:38:28 +02:00

75 lines
2.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 裁决纯逻辑完整性判定未决禁验收、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"));
}