2026-09-30 09:15:57 +08:00
|
|
|
|
/** 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[];
|
2026-09-30 12:34:45 +08:00
|
|
|
|
// 本次核查实际引用的证据:界面"查看依据"的实体来源(纠偏第一批)
|
|
|
|
|
|
evidence: {id?: string; evidence_id?: string; source?: string; locator?: string;
|
|
|
|
|
|
value?: string; version?: string}[];
|
2026-09-30 09:15:57 +08:00
|
|
|
|
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: "已取消",
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-09-30 09:25:25 +08:00
|
|
|
|
/** 维度呈现方式。同刻度 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} {
|
2026-09-30 09:15:57 +08:00
|
|
|
|
const total = dimensions.length;
|
2026-09-30 09:25:25 +08:00
|
|
|
|
if (!total) return {layout: "empty", reason: EMPTY_DIMENSIONS};
|
2026-09-30 09:15:57 +08:00
|
|
|
|
const scales = new Set(dimensions.map((item) => item.scale_id || ""));
|
2026-09-30 09:25:25 +08:00
|
|
|
|
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 维,可用雷达并附等价文字列表"};
|
2026-09-30 09:15:57 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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("");
|
|
|
|
|
|
}
|