feat(api+frontend): D7 验收反馈——因子详情页(出生档案)+来源列布局+进度首轮报轮次 [vps]
用户 VPS 实测四条反馈(877723a 首班):
① 因子详情页:卡片因子点名/工厂行点击原都只切工厂页或无反应
- GET /pipeline/factors/{name}/detail:registry 条目(origin/
hypothesis/首版 expression/source/justification/effective_from)
+出生批次=decompose_jobs 台账中 registered 含该名的最新 job
- 前端 FactorDetail.vue(/pipeline/factors/:name):出生档案卡
(来源假设回链/出生方式/批次回链/表达式/承载逻辑)+近窗 t+IC 趋势
② 总览无变化=数据驱动确认,非缺陷(唯一卡已 building;落新卡即现
琥珀项;地铁图属 D1-D6 后续批次)——spec 记录,无代码
③ 工厂来源列:width 130→min-width 200,徽标与回链竖排(挤行修复)
④ 轮次进度起跑即报「第 1/3 轮 · 生成候选中(≤8 条)…」——
初代 LLM 生成期不带轮次字样,像没进循环(用户质询点出)
测试:detail 端点+出生档案断言(decomposer birth/manual null/404)
+FactorDetail.spec 两件;前端 76 绿+构建过+后端 1707 绿
This commit is contained in:
@@ -126,6 +126,10 @@ async def run_decompose(client: LLMClient, card: dict, *,
|
||||
passed: list[dict] = []
|
||||
failed: list[dict] = []
|
||||
rounds = 0
|
||||
# 起跑即报第 1 轮(验收反馈④:初代生成期不带轮次字样,像没进循环)
|
||||
if on_progress is not None:
|
||||
on_progress(round_no=1, total_rounds=max_rounds, passed=0, regen=0,
|
||||
message="生成候选中(≤8 条)…")
|
||||
pending = normalize_candidates(
|
||||
await client.chat_json(build_decompose_messages(card, existing_names=existing_names),
|
||||
max_tokens=4000))
|
||||
|
||||
@@ -557,6 +557,45 @@ def factor_ic_trend(name: str) -> dict:
|
||||
"rollingT": [t for _, t in pairs], "baseline": baseline}
|
||||
|
||||
|
||||
@router.get("/pipeline/factors/{name}/detail")
|
||||
def factor_detail(name: str) -> dict:
|
||||
"""因子详情/出生档案(D7 验收反馈①):谁生的/哪批生的/点得回去.
|
||||
|
||||
decomposer 因子附 birth=台账中 registered 含该名的最新批次
|
||||
(jobId/startedAt/rounds);manual 因子 birth=null.时间线由前端
|
||||
用这些原料拼(出生→入批→末评),不在后端造展示结构.
|
||||
"""
|
||||
from sanguo_factor import version_registry as vr
|
||||
from sanguo_portfolio.strategy_registry import ensure_runtime_registry
|
||||
reg = vr.load_registry(ensure_runtime_registry(
|
||||
_factor_registry_path(), os.path.join("config", "factor_registry.yaml")))
|
||||
e = reg["factors"].get(name)
|
||||
if e is None or e.get("status") == "graveyard":
|
||||
raise HTTPException(404, f"因子不存在或已入墓园: {name}")
|
||||
versions = e.get("versions") or []
|
||||
first = versions[0] if versions else {}
|
||||
params = first.get("params") or {}
|
||||
points, last_eval = _eval_latest_points()
|
||||
birth = None
|
||||
hyp_id = e.get("hypothesis")
|
||||
if e.get("origin") == "decomposer" and hyp_id:
|
||||
for job in pipeline_store.list_decompose_jobs(_pipeline_db(), hyp_id):
|
||||
if any(c.get("name") == name for c in (job.get("registered") or [])):
|
||||
birth = {"jobId": job["jobId"], "startedAt": job["startedAt"],
|
||||
"rounds": job["rounds"], "hypId": hyp_id}
|
||||
break
|
||||
return {"factor": {
|
||||
"name": name, "status": e["status"],
|
||||
"origin": e.get("origin") or "manual", "hypothesis": hyp_id,
|
||||
"expression": params.get("expression"),
|
||||
"source": params.get("source"),
|
||||
"justification": params.get("justification"),
|
||||
"createdAt": first.get("effective_from"),
|
||||
"version": str(first.get("v")) if first else "1",
|
||||
"icRecentT": (points.get(name) or {}).get("t"),
|
||||
"lastEvalDate": last_eval}, "birth": birth}
|
||||
|
||||
|
||||
@router.post("/pipeline/factors/{name}/verdict")
|
||||
def factor_verdict(name: str, body: dict) -> dict:
|
||||
"""晋级评审三按钮(触点②):promote/graveyard 走 version_registry 状态机;
|
||||
|
||||
Reference in New Issue
Block a user