Files
mini-openclaw-v2/src/lib/prescreen-run.ts
T
qiaoqichaoandClaude Code 02289661a8 feat(v2): 纠偏第一批——执行链产出新版预审记录(真实运行验收)
覆盖 AC-46/47/48/49(BR-45/46/47、E-27)。审计确认的 P1 缺口"正常发起评审不产生
新版预审记录"由此关闭:视图由执行链出口产生,不再依赖测试或脚本注入。

后端:
- services/prescreen.py:新增 screening_from_run(),把一次真实运行的执行结果装配成
  预审视图;维度与阶段按**本次实际执行的组件与核查项**投影
  (dimensions_and_evidence:冻结方案声明过、且本次真的执行过的技能/规则才算维度;
  经验核查等方案外伪步骤不算;阶段只表达证据成熟度,不看结论好坏)。
- services/assistant_worker.py:运行出口(含失败路径)统一调用 save_prescreen_view(),
  运行结束即落库视图;装配失败单独记事件,不让运行一起丢。
- contracts/prescreen.py:视图带 evidence(界面"查看依据"的来源实体);
  新增运行级错误码 RUN_STEP_FAILED(E-27)。
- 维度状态用本次执行的步骤与核查项算(含跑通但无问题的项),
  只看待办 finding 会把通过的维度误判成未核查。

前端:面板新增"核查结果"区(每条可打开依据抽屉,显示来源、定位、原文片段与组件版本)
与降级记录展示;没有证据时明确写"按缺证处理",不编造。

测试:backend/test_prescreen_from_run.py 12 项,用真实 enqueue + DurableWorker.process_next
(临时库)断言视图由执行链产生、失败与模型不可用如实留痕、重跑新增记录。
含反例:执行前不得存在任何记录。

实际验收(浏览器渲染 + 真实运行):状态 partial、10 维(5 已核查/2 待核实/3 阻断)、
4 条核查结果、14 条证据、1 条 RUN_STEP_FAILED 未完成项;记录见
docs/v2/17-batch-delivery-records.md(含剩余缺口:界面点击未自动化、材料确认未在浏览器走完、
演示角色需 XINGBANG_DEMO_ROLE_LOGIN=1、模型未配置)。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-30 12:34:45 +08:00

110 lines
6.3 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.
/** 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("");
}