feat(v2): T11 改进治理与演示交付——对照门禁、泄漏作废、三模块演示脚本
覆盖 AC-15/16/34/37/41(BR-15/16/40、E-11/24)。
后端:
- contracts/improvement.py:ImprovementCandidate + ContrastReport/ContrastMetrics。
零分母不得给比率或覆盖率(样本不足不是 0%);非独立评测不得给改进/退化结论;
已发布必须引用既有不可变版本;没有对照证据不得进入待审核或已发布。
- services/improvements.py:
· evaluate_contrast 在服务端判定泄漏——预测锁定前读到标签即整次评测作废(E-11),
保留集来源进开发集的样本作废并单列,指标按分子/可观察分母/覆盖率/未观察分开报;
· transition 状态机:draft→evaluating→ready→in_review→approved→released,
自批拒绝、退化与结论不明拒绝发布、发布幂等且只引用既有版本、撤回只影响新运行。
- api/prescreen.py:候选列表、提出与状态机三个端点;评测请求体提交样本,
泄漏与指标一律由服务端计算,不接受客户端交来的"成品报告"。
- contracts/prescreen.py:补 E-11/E-24 错误码与 409 映射。
前端一致性修正:
- src/lib/prescreen-run.ts 的呈现判定改用后端既有词汇
(empty/list/grouped/radar/faceted,与 services/review_plan.presentation 一致),
避免前后端各一套判定导致界面画出后端判定为不该画雷达的图。
演示交付:
- scripts/xingbang_prescreen_demo.py:按 08 §7 八步演示,只驱动已实现入口,
未实现的步骤(批量清单、历史提炼、卡片治理)如实标注原因;
工程演示与模型效果分列,本地专项/Jev/真实概率一律标未验证或未接通。
- backend/test_demo_script.py:冒烟断言八步状态、未实现原因与分列口径。
验证:backend 全量 unittest 519 项 OK;前端 npm run build 通过。
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -43,10 +43,11 @@ assert.match(lib, /domain !== "demo"/, "lib 必须在非演示域返回 null");
|
||||
// 5. 雷达只对同刻度 3–8 维绘制,其余给完整列表;空维度不补占位轴(AC-44)
|
||||
assert.match(lib, /RADAR_MIN = 3/);
|
||||
assert.match(lib, /RADAR_MAX = 8/);
|
||||
assert.match(lib, /kind: "radar"/);
|
||||
assert.match(lib, /layout: "radar"/);
|
||||
assert.match(lib, /DimensionLayout = "empty" \| "list" \| "grouped" \| "radar" \| "faceted"/, "呈现词汇必须与后端 review_plan.presentation 一致");
|
||||
assert.match(lib, /EMPTY_DIMENSIONS = "当前方案尚未配置核查维度"/);
|
||||
assert.match(panel, /shown\.kind === "radar" && <RadarChart/, "只有判定为雷达时才画雷达");
|
||||
assert.match(panel, /shown\.kind === "empty" && <div className="ra-empty">/, "空维度用空状态,不补六轴");
|
||||
assert.match(panel, /shown\.layout === "radar" && <RadarChart/, "只有判定为雷达时才画雷达");
|
||||
assert.match(panel, /shown\.layout === "empty" && <div className="ra-empty">/, "空维度用空状态,不补六轴");
|
||||
assert.match(panel, /文字列表与图等价/, "图必须有等价文字列表");
|
||||
|
||||
// 6. 乱序与过期响应必须丢弃(AC-31)
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
"""兴邦预审 P0 模拟闭环演示脚本(T11)。
|
||||
|
||||
依据:docs/v2/08-data-pilot-and-demo.md §7 的八步演示顺序。
|
||||
|
||||
**诚实性要求**(08 §7 末、06 §6):脚本只驱动本仓库已经实现并验证过的入口;
|
||||
没有实现的步骤打印"未实现"及其原因,不伪造屏幕状态;模型效果与工程演示
|
||||
分列,模拟标签不得包装成真实业务验证。
|
||||
|
||||
用法:backend/venv/Scripts/python.exe scripts/xingbang_prescreen_demo.py
|
||||
全程在临时目录内使用临时数据库,不触碰真实 xingbang_data。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(ROOT / "backend"))
|
||||
|
||||
from contracts.prescreen import ErrorCode # noqa: E402
|
||||
from services import (assistant_store as store, improvements, judgment_adapters, # noqa: E402
|
||||
judgment_provider as jp, prescreen, prescreen_policy,
|
||||
prescreen_sandbox, prescreen_store, review_plan, trajectory)
|
||||
|
||||
DONE = "已执行"
|
||||
NOT_DONE = "未实现"
|
||||
MODEL_UNVERIFIED = "未验证(无权重与授权)"
|
||||
|
||||
FINDINGS = [
|
||||
{"code": "corporate.cashflow", "result": "unknown", "stage": 1, "evidence_ids": ["EV-1"]},
|
||||
{"code": "corporate.title", "result": "unknown", "stage": 1, "evidence_ids": ["EV-2"]},
|
||||
{"code": "corporate.deal", "result": "checked", "stage": 2, "evidence_ids": ["EV-3"]},
|
||||
]
|
||||
DIMENSIONS = [
|
||||
{"id": "DC-CASH", "component_refs": [{"id": "corporate.cashflow"}]},
|
||||
{"id": "DC-TITLE", "component_refs": [{"id": "corporate.title"}]},
|
||||
{"id": "DC-DEAL", "component_refs": [{"id": "corporate.deal"}]},
|
||||
]
|
||||
PLAN = {
|
||||
"plan": {"id": "corporate.default", "version": "1.0.0"},
|
||||
"skills": [{"id": "corporate.cashflow", "version": "1.0.0"}],
|
||||
"rules": [], "template": {"id": "template.ai-pre-review", "version": "1.0.0"},
|
||||
"content_hash": "demo-hash", "dimensions": DIMENSIONS,
|
||||
}
|
||||
STEPS = [
|
||||
(1, "3,000 条候选清单、生成版本与多结果分布", NOT_DONE,
|
||||
"T04 的服务已在单测中验证,但本脚本不重复驱动批量生成;清单需真实数据授权"),
|
||||
(2, "选开发集案例,展开问询/补件/会议事件,提炼 Skill 与 advisory Rule", NOT_DONE,
|
||||
"历史提炼走 T05 的治理入口,需要真实历史材料;此处不伪造提炼结果"),
|
||||
(3, "自然语言改适用范围 + 人工新增卡片,统一审核发布", NOT_DONE,
|
||||
"卡片治理入口属 T05;演示脚本不代替审核动作"),
|
||||
(4, "数据卡 + Skills + Rules 组方案,维度随方案变化,Tools 只在依据中展示", DONE, None),
|
||||
(5, "缺证 → 主审核查 → 问题与补件 → 演示概率来源", DONE, None),
|
||||
(6, "勾选动作比较情景、撤销恢复基线", DONE, None),
|
||||
(7, "回放完整 trajectory,对齐模拟会议记录,查看材料版本差异", DONE, None),
|
||||
(8, "从漏检提出改进候选,隔离样本对照后人工审核发布,旧版仍可回放", DONE, None),
|
||||
]
|
||||
|
||||
|
||||
def run_demo() -> dict:
|
||||
facts: dict = {"steps": [], "sandbox": {}, "trajectory": {}, "improvement": {}}
|
||||
with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as tmp:
|
||||
db_path = Path(tmp) / "demo.sqlite3"
|
||||
store.initialize(db_path)
|
||||
|
||||
# 步骤 4:方案维度(去重与刻度判定)
|
||||
dimensions = review_plan.resolve_dimensions([
|
||||
{"id": "DC-CASH", "label": "回款核验", "scale_id": "evidence-stage",
|
||||
"scale_version": "1.0.0", "order": 1, "component_refs": [{"id": "corporate.cashflow", "version": "1.0.0"}],
|
||||
"levels": [{"value": i, "label": name, "criterion": f"证据阶段:{name}"} for i, name in
|
||||
enumerate(("未提供", "仅有说明", "资料待核", "交叉核验", "人工复核"))]},
|
||||
{"id": "DC-TITLE", "label": "权属核验", "scale_id": "evidence-stage",
|
||||
"scale_version": "1.0.0", "order": 2, "component_refs": [{"id": "corporate.title", "version": "1.0.0"}],
|
||||
"levels": [{"value": i, "label": name, "criterion": f"证据阶段:{name}"} for i, name in
|
||||
enumerate(("未提供", "仅有说明", "资料待核", "交叉核验", "人工复核"))]},
|
||||
{"id": "DC-DEAL", "label": "交易要素", "scale_id": "evidence-stage",
|
||||
"scale_version": "1.0.0", "order": 3, "component_refs": [{"id": "corporate.deal", "version": "1.0.0"}],
|
||||
"levels": [{"value": i, "label": name, "criterion": f"证据阶段:{name}"} for i, name in
|
||||
enumerate(("未提供", "仅有说明", "资料待核", "交叉核验", "人工复核"))]},
|
||||
])
|
||||
presentation = review_plan.presentation(dimensions)
|
||||
facts["presentation"] = presentation["layout"]
|
||||
|
||||
# 步骤 5:预审执行(专项关闭,无校准概率)
|
||||
config = jp.resolve_config(False)
|
||||
decision = prescreen_policy.decide(FINDINGS)
|
||||
decision["findings"] = FINDINGS
|
||||
decision["missing_inputs"] = ["近六个月银行流水", "设备权属材料"]
|
||||
screening = prescreen.assemble(
|
||||
{"id": "run-demo", "project_id": "DEMO-026", "revision": 1,
|
||||
"created_at": store.now(), "input_snapshot_id": "snap-demo"},
|
||||
PLAN, decision, config)
|
||||
with store.transaction(db_path) as db:
|
||||
prescreen_store.save(db, screening)
|
||||
facts["recommendation"] = screening["recommendation"]
|
||||
facts["probability_is_null"] = screening["probability"] is None
|
||||
|
||||
# 步骤 6:沙盘(演示值只在演示域、组合查表)
|
||||
with store.transaction(db_path) as db:
|
||||
stored = prescreen_store.load(db, "DEMO-026", "run-demo")
|
||||
baseline = prescreen_sandbox.simulate(stored, [])
|
||||
both = prescreen_sandbox.simulate(stored, ["assume_bank_flow", "assume_title"])
|
||||
facts["sandbox"] = {
|
||||
"baseline": baseline["probability_view"]["demo_estimate"]["value"],
|
||||
"combined": both["probability_view"]["demo_estimate"]["value"],
|
||||
"label": baseline["probability_view"]["demo_estimate"]["label"],
|
||||
"unchanged_dimensions": [item["id"] for item in both["dimensions"]
|
||||
if not item["assumed"]],
|
||||
}
|
||||
|
||||
# 步骤 7:轨迹回放与模拟会议结果
|
||||
with store.transaction(db_path) as db:
|
||||
store.put(db, "run", {"id": "run-demo", "project_id": "DEMO-026",
|
||||
"status": "completed", "input_snapshot_id": "snap-demo",
|
||||
"created_at": store.now(), "steps": [
|
||||
{"run_id": "run-demo", "step_id": "corporate.cashflow",
|
||||
"skill": {"id": "corporate.cashflow", "version": "1.0.0"},
|
||||
"status": "completed", "input_snapshot_id": "snap-demo",
|
||||
"started_at": store.now(), "outputs": {"note": "已核查"},
|
||||
"findings": [{"code": "cashflow", "result": "unknown",
|
||||
"evidence_ids": ["EV-1"]}],
|
||||
"errors": [], "missing_inputs": []},
|
||||
{"run_id": "run-demo", "step_id": "corporate.title",
|
||||
"skill": {"id": "corporate.title", "version": "1.0.0"},
|
||||
"status": "failed", "input_snapshot_id": "snap-demo",
|
||||
"started_at": store.now(), "outputs": {},
|
||||
"findings": [], "missing_inputs": [],
|
||||
"errors": [{"code": "SKILL_TIMEOUT",
|
||||
"message": "演示中的失败步骤"}]}]})
|
||||
replay = trajectory.replay(db, "DEMO-026", "run-demo")
|
||||
trajectory.record(db, "DEMO-026",
|
||||
{"raw_code": "附条件通过", "meeting_id": "M-DEMO",
|
||||
"occurred_at": store.now()}, "demo-reviewer",
|
||||
known_snapshots=["snap-demo"])
|
||||
outcomes = trajectory.listing(db, "DEMO-026")
|
||||
facts["trajectory"] = {
|
||||
"steps": [item["status"] for item in replay["steps"]],
|
||||
"failed_preserved": any(item["status"] == "failed" for item in replay["steps"]),
|
||||
"unmatched": len(outcomes["unmatched"]),
|
||||
"observed_denominator": outcomes["counts_toward_denominator"]["observed"],
|
||||
}
|
||||
|
||||
# 步骤 8:改进候选(合成样本、独立评测、可回放旧版)
|
||||
with store.transaction(db_path) as db:
|
||||
candidate = improvements.propose(db, "DEMO-026", {
|
||||
"target": "knowledge", "target_ref": {"id": "card-demo", "version": "1.0.0"},
|
||||
"proposed_revision_ref": {"id": "card-demo", "version": "1.1.0"},
|
||||
"change_summary": "把回款核验经验扩到设备直租",
|
||||
"source_run_ids": ["run-demo"]}, "demo-manager", "demo")
|
||||
evaluation = {"baseline_ref": {"id": "current", "version": "1.0.0"},
|
||||
"candidate_ref": {"id": "prescreen.v2", "version": "0.2.0"},
|
||||
"dataset_ref": "demo/synthetic-manifest.json",
|
||||
"split_ref": "demo-split.v1", "independent": True,
|
||||
"samples": [
|
||||
{"id": "S-1", "split": "dev", "observed": True,
|
||||
"baseline_pass": True, "candidate_pass": True,
|
||||
"predicted_at": "2026-09-29T01:00:00+00:00",
|
||||
"label_revealed_at": "2026-09-29T03:00:00+00:00"},
|
||||
{"id": "S-2", "split": "dev", "observed": True,
|
||||
"baseline_pass": False, "candidate_pass": True,
|
||||
"predicted_at": "2026-09-29T01:00:00+00:00",
|
||||
"label_revealed_at": "2026-09-29T03:00:00+00:00"}]}
|
||||
evaluated = improvements.transition(db, candidate["id"], "evaluate", "demo-reviewer",
|
||||
evaluation=evaluation)
|
||||
improvements.transition(db, evaluated["id"], "submit", "demo-reviewer")
|
||||
approved = improvements.transition(db, evaluated["id"], "approve", "demo-reviewer")
|
||||
released = improvements.transition(
|
||||
db, approved["id"], "release", "demo-reviewer",
|
||||
publisher=lambda body: {"id": "card-demo", "version": "1.1.0"})
|
||||
facts["improvement"] = {"verdict": evaluated["contrast"]["verdict"],
|
||||
"status": released["status"],
|
||||
"released_ref": released["released_ref"]}
|
||||
|
||||
# 专项:离线 fixture 可跑,live 未接通
|
||||
facts["judgment_offline"] = judgment_adapters.load_fixture(
|
||||
"question_bank_extraction.json")["rubric_version"]
|
||||
facts["demo_table_version"] = prescreen_sandbox.DEMO_TABLE_VERSION
|
||||
facts["budget_code"] = ErrorCode.JUDGMENT_CONTEXT_EXCEEDED.value
|
||||
return facts
|
||||
|
||||
|
||||
def main() -> int:
|
||||
facts = run_demo()
|
||||
print("兴邦预审 P0 模拟闭环演示(合成数据 · 不连接真实数据与模型)")
|
||||
print("=" * 78)
|
||||
for index, title, status, reason in STEPS:
|
||||
print(f"{index}. [{status}] {title}")
|
||||
if reason:
|
||||
print(f" 原因:{reason}")
|
||||
print("-" * 78)
|
||||
print("工程演示(本仓库已实现并验证的入口)")
|
||||
print(f" · 维度呈现:{facts['presentation']}")
|
||||
print(f" · 主审意见:{facts['recommendation']} · 真实概率为空:{facts['probability_is_null']}")
|
||||
sandbox = facts["sandbox"]
|
||||
print(f" · 沙盘:基线 {sandbox['baseline']:.0%} → 组合 {sandbox['combined']:.0%}"
|
||||
f"({sandbox['label']})")
|
||||
print(f" · 未变化的维度仍列出:{sandbox['unchanged_dimensions']}")
|
||||
trace = facts["trajectory"]
|
||||
print(f" · 轨迹步骤状态:{trace['steps']} · 失败步骤保留:{trace['failed_preserved']}")
|
||||
print(f" · 会议结果:unmatched {trace['unmatched']} 条 · 已观察分母 {trace['observed_denominator']}")
|
||||
imp = facts["improvement"]
|
||||
print(f" · 改进对照:{imp['verdict']} → {imp['status']}({imp['released_ref']})")
|
||||
print("-" * 78)
|
||||
print("模型效果(与上面的工程演示分开计)")
|
||||
print(f" · 本地专项:{MODEL_UNVERIFIED};当前只有离线 fixture({facts['judgment_offline']})")
|
||||
print(f" · Jev:未接通(无端点与凭据)")
|
||||
print(f" · 真实审批概率:未校准,恒为空;界面上的百分数来自演示表 "
|
||||
f"{facts['demo_table_version']},不是模型输出")
|
||||
print(f" · 超预算时的拒绝码({facts['budget_code']})证明预算守卫生效,与概率无关")
|
||||
print("=" * 78)
|
||||
print("以上均为合成数据与演示值,不能作为真实业务效果或审批依据。")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user