feat(ux): R3 验收前清单 — AcceptPanel「本次将更新」预览
新 lib/review/accept-preview.ts (buildAcceptPreview):据现有 client state 列验收前预期项——晋升版次/提炼摘要/写回裁决/伏笔到期扫描/伏笔建议待登记。仅列后端 accept 事务确实做的事(核对 projects.py accept 端点),不声称改人物 latest_state,带「以验收回执为准」免责。 AcceptPanel 加 foreshadowCount prop + 未阻断时渲染清单;ReviewReport 传 foreshadow.length。 TDD: 4 测。前端门禁绿: lint/tsc/vitest 205/build。
This commit is contained in:
@@ -1,10 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import type { AcceptResponse } from "@/lib/api/types";
|
||||
import { buildAcceptPreview } from "@/lib/review/accept-preview";
|
||||
|
||||
interface AcceptPanelProps {
|
||||
conflictCount: number;
|
||||
unresolvedCount: number;
|
||||
// R3:当前伏笔建议数(验收前清单提醒,只读不自动落库)。
|
||||
foreshadowCount: number;
|
||||
accepting: boolean;
|
||||
result: AcceptResponse | null;
|
||||
onAccept: () => void;
|
||||
@@ -14,11 +17,16 @@ interface AcceptPanelProps {
|
||||
export function AcceptPanel({
|
||||
conflictCount,
|
||||
unresolvedCount,
|
||||
foreshadowCount,
|
||||
accepting,
|
||||
result,
|
||||
onAccept,
|
||||
}: AcceptPanelProps) {
|
||||
const blocked = unresolvedCount > 0;
|
||||
const preview = buildAcceptPreview({
|
||||
conflictCount,
|
||||
foreshadowSuggestions: foreshadowCount,
|
||||
});
|
||||
|
||||
if (result) {
|
||||
return (
|
||||
@@ -56,11 +64,35 @@ export function AcceptPanel({
|
||||
尚有 {unresolvedCount} 项冲突未裁决,处理完才能验收。
|
||||
</p>
|
||||
) : (
|
||||
<p className="mb-2 text-xs text-ink-soft">
|
||||
{conflictCount === 0
|
||||
? "无冲突,可直接验收。"
|
||||
: "全部冲突已裁决,可验收本章。"}
|
||||
</p>
|
||||
<>
|
||||
<p className="mb-2 text-xs text-ink-soft">
|
||||
{conflictCount === 0
|
||||
? "无冲突,可直接验收。"
|
||||
: "全部冲突已裁决,可验收本章。"}
|
||||
</p>
|
||||
{/* R3:验收前「本次将更新」清单(预期,落库口径以验收回执为准)。 */}
|
||||
<div className="mb-3 rounded border border-line/70 bg-bg/50 p-3">
|
||||
<h4 className="mb-1.5 text-xs font-semibold text-ink">
|
||||
本次验收将更新
|
||||
</h4>
|
||||
<ul className="space-y-1.5">
|
||||
{preview.map((item) => (
|
||||
<li key={item.label} className="flex gap-2 text-xs text-ink-soft">
|
||||
<span className="text-cinnabar" aria-hidden="true">
|
||||
◆
|
||||
</span>
|
||||
<span>
|
||||
<span className="text-ink">{item.label}</span>
|
||||
<span className="text-ink-soft">:{item.detail}</span>
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<p className="mt-2 text-[11px] text-ink-soft/70">
|
||||
以上为预期,最终以验收回执为准。
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -376,6 +376,7 @@ export function ReviewReport({
|
||||
<AcceptPanel
|
||||
conflictCount={conflictCount}
|
||||
unresolvedCount={resolved ? 0 : unresolved}
|
||||
foreshadowCount={foreshadow.length}
|
||||
accepting={accept.status === "accepting"}
|
||||
result={accept.result}
|
||||
onAccept={() => void onAccept()}
|
||||
|
||||
48
apps/web/lib/review/accept-preview.test.ts
Normal file
48
apps/web/lib/review/accept-preview.test.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { buildAcceptPreview } from "./accept-preview";
|
||||
|
||||
describe("buildAcceptPreview", () => {
|
||||
it("always previews version promotion, digest, and overdue scan", () => {
|
||||
const items = buildAcceptPreview({
|
||||
conflictCount: 0,
|
||||
foreshadowSuggestions: 0,
|
||||
});
|
||||
const labels = items.map((it) => it.label);
|
||||
expect(labels).toContain("晋升章节版次");
|
||||
expect(labels).toContain("提炼章节摘要");
|
||||
expect(labels).toContain("扫描伏笔到期");
|
||||
});
|
||||
|
||||
it("omits the decisions line when there are no conflicts", () => {
|
||||
const items = buildAcceptPreview({
|
||||
conflictCount: 0,
|
||||
foreshadowSuggestions: 0,
|
||||
});
|
||||
expect(items.some((it) => it.label === "写回冲突裁决")).toBe(false);
|
||||
});
|
||||
|
||||
it("includes the decisions line with the conflict count", () => {
|
||||
const items = buildAcceptPreview({
|
||||
conflictCount: 3,
|
||||
foreshadowSuggestions: 0,
|
||||
});
|
||||
const decisions = items.find((it) => it.label === "写回冲突裁决");
|
||||
expect(decisions?.detail).toContain("3");
|
||||
});
|
||||
|
||||
it("reminds about pending foreshadow suggestions only when present", () => {
|
||||
expect(
|
||||
buildAcceptPreview({
|
||||
conflictCount: 0,
|
||||
foreshadowSuggestions: 0,
|
||||
}).some((it) => it.label === "伏笔建议待登记"),
|
||||
).toBe(false);
|
||||
const items = buildAcceptPreview({
|
||||
conflictCount: 0,
|
||||
foreshadowSuggestions: 2,
|
||||
});
|
||||
const foreshadow = items.find((it) => it.label === "伏笔建议待登记");
|
||||
expect(foreshadow?.detail).toContain("2");
|
||||
});
|
||||
});
|
||||
49
apps/web/lib/review/accept-preview.ts
Normal file
49
apps/web/lib/review/accept-preview.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
// R3 · 验收前「本次将更新」清单(HITL gate,UX §7.4)。
|
||||
// 只列后端 accept 事务确实会做的事(projects.py accept 端点):晋升版次 + 提炼章节摘要
|
||||
// + 写回裁决 + 验收后伏笔到期扫描。**不**声称改人物 latest_state / 自动落库伏笔建议
|
||||
// (那些不在 accept 事务里)。最终口径以后端 AcceptResponse 回执为准。
|
||||
// 纯逻辑,便于 node 环境单测。
|
||||
|
||||
export interface AcceptPreviewInput {
|
||||
conflictCount: number;
|
||||
foreshadowSuggestions: number;
|
||||
}
|
||||
|
||||
export interface AcceptPreviewItem {
|
||||
label: string;
|
||||
detail: string;
|
||||
}
|
||||
|
||||
// 据现有客户端 state 生成验收前预览项(非落库口径,仅给作者一个「将要发生什么」的预期)。
|
||||
export function buildAcceptPreview({
|
||||
conflictCount,
|
||||
foreshadowSuggestions,
|
||||
}: AcceptPreviewInput): AcceptPreviewItem[] {
|
||||
const items: AcceptPreviewItem[] = [
|
||||
{
|
||||
label: "晋升章节版次",
|
||||
detail: "终稿晋升为新的接受版本(旧草稿保留可追溯)",
|
||||
},
|
||||
{
|
||||
label: "提炼章节摘要",
|
||||
detail: "从终稿提炼一行摘要,追加到记忆真源",
|
||||
},
|
||||
];
|
||||
if (conflictCount > 0) {
|
||||
items.push({
|
||||
label: "写回冲突裁决",
|
||||
detail: `${conflictCount} 条裁决落库留痕`,
|
||||
});
|
||||
}
|
||||
items.push({
|
||||
label: "扫描伏笔到期",
|
||||
detail: "验收后台扫描,可能将逾期伏笔置为 OVERDUE",
|
||||
});
|
||||
if (foreshadowSuggestions > 0) {
|
||||
items.push({
|
||||
label: "伏笔建议待登记",
|
||||
detail: `${foreshadowSuggestions} 条只读建议,验收不自动落库,需你另行登记`,
|
||||
});
|
||||
}
|
||||
return items;
|
||||
}
|
||||
Reference in New Issue
Block a user