Files
mini-openclaw-v2/src/lib/prescreen-run.ts
T

110 lines
6.3 KiB
TypeScript
Raw Normal View History

/** T09 预审运行与沙盘的前端契约与呈现规则。
*
* 依据:docs/v2/04-ui-spec.md §5/§9、DESIGN.md §38、13-probability-prototype.md。
* 契约字段手工对齐 backend/contracts/{prescreen,simulation}.py;**不得**据此宣称
* 类型已自动生成(同 prescreen.ts 的既有说明)。
*
* 三条前端硬规则:
* 1. 数值只来自服务端:演示情景表在服务端,本文件不复制 42/58/51/64(AC-30/31)。
* 2. 真实概率恒为空:未校准时只显示建议、缺证与降级,不显示任何"通过率"(AC-12)。
* 3. 颜色不单独传意:来源、数据域、状态都同时给文字与图标(AC-42、DESIGN §32)。
*/
export const DEMO_LABEL = "人工设定,仅演示交互;未经历史数据校准";
export const STAGE_NAMES = ["未提供", "仅有说明", "资料待核", "交叉核验", "人工复核"];
export const STAGE_LEGEND = "0 未提供 · 1 仅有说明 · 2 资料待核 · 3 交叉核验 · 4 人工复核";
export const RADAR_MIN = 3;
export const RADAR_MAX = 8;
export const EMPTY_DIMENSIONS = "当前方案尚未配置核查维度";
export type DemoEstimate = {value: number; source: "synthetic_demo"; scenario_table_version: string; scenario_key: string; label: string};
export type ProbabilityView = {target_event: string; snapshot_id: string; as_of: string; real_estimate: null; reason: "not_calibrated"; demo_estimate: DemoEstimate | null};
export type DimensionResult = {id: string; stage: number | null; state: "unknown" | "checked" | "blocked"; basis_ids: string[]};
export type RunView = {
id: string; project_id: string; data_domain: "demo" | "production"; revision: number;
snapshot_id: string; plan_ref: {id: string; version: string}; status: string;
recommendation: "advance" | "supplement" | "stop" | "insufficient_information" | null;
findings: any[]; missing_inputs: string[]; applied_experiences: any[];
probability: null; probability_view?: ProbabilityView | null;
degradations: {code: string; message: string}[]; dimension_results: DimensionResult[];
// 本次核查实际引用的证据:界面"查看依据"的实体来源(纠偏第一批)
evidence: {id?: string; evidence_id?: string; source?: string; locator?: string;
value?: string; version?: string}[];
versions: Record<string, any>;
};
export type ScenarioDimensionState = {id: string; baseline_stage: number | null; scenario_stage: number | null; assumed: boolean};
export type Simulation = {
id: string; run_id: string; revision: number; selection_hash: string;
status: "queued" | "computing" | "ready" | "failed";
assumptions: {key: string; label: string; target_dimension_id: string; assumed_stage: number; premise: string}[];
baseline_probability_view: ProbabilityView; probability_view: ProbabilityView | null;
dimensions: ScenarioDimensionState[]; remaining_inputs: string[]; reason: string;
error_code: string | null;
};
export const RECOMMENDATION_LABELS: Record<string, string> = {
advance: "可推进",
supplement: "补充材料后再判断",
stop: "不建议推进",
insufficient_information: "信息不足,无法给出意见",
};
export const RUN_STATUS_LABELS: Record<string, string> = {
queued: "排队中", running: "执行中", awaiting_input: "等待补充材料",
completed: "已完成", partial: "部分完成", failed: "未完成", cancelled: "已取消",
};
/** 维度呈现方式。同刻度 3–8 维才画雷达;其余给完整列表,绝不补占位轴(AC-44)。
*
* 词汇与后端 services/review_plan.presentation() 保持一致
* (empty/list/grouped/radar/faceted),避免前后端各有一套判定导致界面
* 画出后端判定为"不该画雷达"的图。
*/
export type DimensionLayout = "empty" | "list" | "grouped" | "radar" | "faceted";
export function presentationFor(dimensions: {scale_id?: string}[]): {layout: DimensionLayout; reason: string} {
const total = dimensions.length;
if (!total) return {layout: "empty", reason: EMPTY_DIMENSIONS};
const scales = new Set(dimensions.map((item) => item.scale_id || ""));
if (scales.size > 1) return {layout: "faceted", reason: "维度刻度不一致,按分面展示;不强行归一化,避免比较不可比的量"};
if (total < RADAR_MIN) return {layout: "list", reason: `仅 ${total} 个维度,用状态列表比雷达更清楚`};
if (total > RADAR_MAX) return {layout: "grouped", reason: `${total} 个维度超出雷达可读范围,改用分组列表且全部维度仍可查看`};
return {layout: "radar", reason: "同一刻度且 3–8 维,可用雷达并附等价文字列表"};
}
export function stageName(stage: number | null | undefined): string {
if (stage === null || stage === undefined) return "未核查";
return STAGE_NAMES[stage] || `阶段 ${stage}`;
}
export function stateLabel(state: string): string {
return state === "checked" ? "已核查" : state === "blocked" ? "存在阻断项" : "待核实";
}
/** 演示值:只有 demo 域 + 带固定说明才返回;否则返回 null,界面不得显示数字。 */
export function demoValue(domain: string, view?: ProbabilityView | null) {
const demo = view?.demo_estimate;
if (domain !== "demo" || !demo) return null;
return {percent: Math.round(demo.value * 100), label: demo.label || DEMO_LABEL,
tableVersion: demo.scenario_table_version, scenarioKey: demo.scenario_key};
}
/** 来源徽标(AC-42):文字 + 图标 + 样式三者同时给,去色仍可区分。 */
export function originBadge(origin: string) {
const table: Record<string, {text: string; icon: string; tone: string}> = {
history_derived: {text: "历史提炼", icon: "history", tone: "origin-history"},
manual: {text: "人工创建", icon: "pencil", tone: "origin-manual"},
policy_import: {text: "制度导入", icon: "file-text", tone: "origin-policy"},
};
return table[origin] || {text: `来源未知(${origin || "未提供"})`, icon: "help-circle", tone: "origin-unknown"};
}
export function domainBadge(domain: string) {
return domain === "demo"
? {text: "演示数据域", icon: "flask-conical", tone: "domain-demo"}
: {text: "正式数据域", icon: "shield", tone: "domain-production"};
}
export function scenarioKeyOf(keys: string[], order: string[]): string {
const chosen = new Set(keys);
return order.map((item) => (chosen.has(item) ? "1" : "0")).join("");
}