diff --git a/sanguo_api/routes_pipeline.py b/sanguo_api/routes_pipeline.py index 50594446..3d58b01f 100644 --- a/sanguo_api/routes_pipeline.py +++ b/sanguo_api/routes_pipeline.py @@ -451,6 +451,7 @@ def factors() -> dict: # D7 件②:origin 身份戳+来源卡回链血统(manual=缺省) "origin": e.get("origin") or "manual", "hypothesis": e.get("hypothesis"), + "description": e.get("description") or "", "icRecentT": t, # 最近批(12M 滚动窗)即近窗 t "icFullT": None, # eval_store 每批单窗 t,无全史批数据源,如实 null "promotedAtT": e.get("promotion_t") or None, @@ -587,6 +588,7 @@ def factor_detail(name: str) -> dict: return {"factor": { "name": name, "status": e["status"], "origin": e.get("origin") or "manual", "hypothesis": hyp_id, + "description": e.get("description") or "", "expression": params.get("expression"), "source": params.get("source"), "justification": params.get("justification"), @@ -1137,7 +1139,8 @@ async def _decompose_worker(hyp_id: str, config, job_id: str | None = None) -> d for cand in result["passed"]: src = derive_source(cand["expression"]) # 已过门,必为 str vr.upsert_factor(reg, cand["name"], hypothesis=hyp_id, - status="incubating", origin="decomposer") + status="incubating", origin="decomposer", + description=cand.get("description")) vr.add_version(reg, cand["name"], commit="decompose", params={"expression": cand["expression"], "source": src, "origin": "decomposer", @@ -1145,6 +1148,7 @@ async def _decompose_worker(hyp_id: str, config, job_id: str | None = None) -> d effective_from=today) registered.append({"name": cand["name"], "source": src, "expression": cand["expression"], + "description": cand.get("description", ""), "justification": cand["justification"]}) vr.save_registry(rt, reg) if card["state"] != "building": # building 态二次分解=纯增量注册(D3 矩阵无自迁移边) diff --git a/tests/api/test_routes_pipeline_factors.py b/tests/api/test_routes_pipeline_factors.py index 670142a4..32061b23 100644 --- a/tests/api/test_routes_pipeline_factors.py +++ b/tests/api/test_routes_pipeline_factors.py @@ -59,3 +59,21 @@ def test_revise_verdict_does_not_write_back(client, tmp_path): card = next(c for c in pipeline_store.list_hypotheses( str(tmp_path / "pipeline.db")) if c["id"] == hyp) assert card["state"] != "graveyard" and not card.get("deathReason") + + +def test_factors_and_detail_carry_description(client, tmp_path): + """2026-10-09 补丁:description 条目带出,factors 列表与出生档案都可见.""" + import yaml + name, hyp = _seed_factor_card(client) # 建卡+写 registry(assessable) + reg_path = os.environ["SANGUO_FACTOR_REGISTRY"] + with open(reg_path, encoding="utf-8") as f: + reg = yaml.safe_load(f) + reg["factors"][name]["description"] = "毛利变动因子自述" + with open(reg_path, "w", encoding="utf-8") as f: + yaml.safe_dump(reg, f, allow_unicode=True) + items = client.get("/api/v1/pipeline/factors").json()["items"] + assert next(i for i in items if i["id"] == name)["description"] == "毛利变动因子自述" + d = client.get(f"/api/v1/pipeline/factors/{name}/detail").json() + assert d["factor"]["description"] == "毛利变动因子自述" + # 存量无 description=空串非 null(前端免判空) + assert all(isinstance(i.get("description", ""), str) for i in items) diff --git a/tests/api/test_routes_pipeline_hypotheses.py b/tests/api/test_routes_pipeline_hypotheses.py index 2cd6c079..8a523d6b 100644 --- a/tests/api/test_routes_pipeline_hypotheses.py +++ b/tests/api/test_routes_pipeline_hypotheses.py @@ -155,6 +155,7 @@ class TestAuth: DECOMP_GOOD = {"factors": [ {"name": "llm_mom20", "expression": "cs_rank(ts_mean(close, 20))", + "description": "20 日收盘均价动量,量度股价短期延续性", "justification": "20 日动量承载假设"}]} @@ -218,7 +219,7 @@ class TestDecompose: _, queue = fake_llm queue.append({"factors": [ {"name": "llm_bad", "expression": "cs_rank(pledge_pct)", - "justification": "j"}]}) + "description": "质押比例因子", "justification": "j"}]}) queue.append(DECOMP_GOOD) hyp = _seed_card(client) job = _wait_job(client, hyp) @@ -267,10 +268,12 @@ class TestDecompose: assert next(i for i in items if i["id"] == hyp)["state"] == "building" queue.append({"factors": [ {"name": "llm_vol5", "expression": "cs_rank(ts_std(close, 5))", + "description": "5 日波动率因子自述", "justification": "5 日波动承载假设"}]}) job2 = _wait_job(client, hyp) assert job2["status"] == "completed" assert job2["registered"][0]["name"] == "llm_vol5" + assert job2["registered"][0]["description"] == "5 日波动率因子自述" items = client.get("/api/v1/pipeline/hypotheses").json()["items"] assert next(i for i in items if i["id"] == hyp)["state"] == "building" factors = _yaml.safe_load(reg.read_text())["factors"]