Files
mini-openclaw-v2/backend/services/prescreen_store.py
T
qiaoqichaoandClaude Code 22f8a37799 feat(v2): T09 预审与推进沙盘——运行只读视图、演示情景与界面守卫
覆盖 AC-11/12/29/30/31/42/44(BR-11/29/30/41/43、E-20/21/22)。

后端:
- contracts/simulation.py:Simulation/SimulationAssumption/ScenarioDimensionState。
  契约层直接禁止 production 域携带演示估计(E-21);ready 必须有结果、
  failed 必须带错误码且不得显示结果、计算中不得预告乐观增幅。
- services/prescreen_sandbox.py:演示情景表(DEMO-TABLE-v1)与推演入口。
  组合查表不是单项加和(assert_not_additive 把这条写成可执行断言);
  只改被假设的维度,其余维度原样保留;失败保留基线并标未完成。
- services/prescreen_store.py:运行视图只读存储,复用 assistant_objects 新 kind
  prescreen_run,不新建表;只提供保存与读取,要改结果必须产生新修订。
- api/prescreen.py:新增 3 个只读端点(运行列表、运行视图、可用假设动作)
  与 1 个情景端点;请求体里没有任何概率字段。越域返回 E-21、冲突假设 E-22、
  过期修订 409。
- contracts/prescreen.py:补 E-20/21/22 三个错误码与其 HTTP 映射。

前端(挂进既有工作台「材料与问题」页,不新建路由、不新增样式表):
- src/lib/prescreen-run.ts:客户端契约、呈现判定(同刻度 3–8 维才画雷达)、
  演示值域判定、来源与数据域徽标(文字+图标+样式,去色仍可区分)。
- src/components/xingbang/PrescreenPanel.tsx:主审意见、演示值(紧邻固定说明)、
  维度雷达与等价文字列表、维度依据抽屉、假设勾选即时推演。
  旧请求晚到或结果与当前选择不一致即丢弃;请求失败保留基线并标未完成。
- scripts/test_prescreen_ui.cjs:界面源码守卫,已注册进 scripts/verify.py。

未做:运行装配结果的写入方仍是执行链路(T10 范围),当前由测试写入;
沙盘尚未接入真实执行,界面数值全部为服务端演示表值。

验证:backend 全量 unittest 469 项 OK;前端 npm run build 通过。

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

47 lines
1.9 KiB
Python
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 预审运行视图的只读存储:把装配好的 ScreeningRun 投影存下来供界面读取。
依据:docs/v2/03-detailed-design.md §9.2、BR-13;AC-12/30。
**不新建表**:复用既有 `assistant_objects`(新 kind `prescreen_run`),旧库结构不变。
运行视图是既有 run 对象的**只读投影**,因此这里只提供保存与读取,
不提供就地修改:要改结果必须产生新的运行修订(BR-13:过程修订不覆盖旧结果)。
"""
from __future__ import annotations
from contracts.prescreen import ScreeningRun
from services import assistant_store as store
KIND = "prescreen_run"
def save(db, screening: dict) -> dict:
"""保存一条已装配的运行视图;非契约形状直接拒绝,不落库。"""
body = ScreeningRun(**screening).model_dump(mode="json")
store.put(db, KIND, body, expected=screening.get("_expected_revision"))
return body
def load(db, project_id: str, run_id: str) -> dict:
"""按项目读取运行视图。不存在或不属于该项目 → KeyError(由接口层转 404)。"""
body = store.get(db, KIND, run_id)
if body.get("project_id") != project_id:
raise KeyError("运行不存在或不属于该项目")
return body
def latest(db, project_id: str) -> dict | None:
"""项目最近一条运行视图;没有就返回 None,不编造空壳运行。"""
rows = [item for item in store.listing(db, KIND, project_id)]
if not rows:
return None
return max(rows, key=lambda item: (item.get("created_at") or "", item.get("revision") or 0))
def listing(db, project_id: str, limit: int = 20) -> list[dict]:
"""项目内运行视图,新的在前。用于界面选择要查看/推演的那一条运行。"""
rows = [item for item in store.listing(db, KIND, project_id)]
rows.sort(key=lambda item: (item.get("created_at") or "", item.get("revision") or 0),
reverse=True)
return rows[:limit]