/** 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; }; 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 = { advance: "可推进", supplement: "补充材料后再判断", stop: "不建议推进", insufficient_information: "信息不足,无法给出意见", }; export const RUN_STATUS_LABELS: Record = { 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 = { 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(""); }