feat(factor): 决议O①卡片死亡回写聚合化——全死才埋(反查卡下全部因子,部分死=卡留活跃池只影响展示;death_reason=全灭:触发因子x入墓园:死因;修D5任一死埋整卡在一卡多因子语境错位) [vps]

This commit is contained in:
2026-10-10 22:24:59 +08:00
parent 6aa0632595
commit c9b448a494
3 changed files with 83 additions and 15 deletions
+1 -1
View File
@@ -782,7 +782,7 @@ def factor_verdict(name: str, body: dict) -> dict:
# 复用 writeback_card_death(失败降级 stderr+death_writeback_missed
# 附记,不阻断转移;pipeline_db 缺省 env SANGUO_PIPELINE_DB→
# data/pipeline.db,与 CLI 同源)
vr.writeback_card_death(None, entry, note, events_path=events_path)
vr.writeback_card_death(None, reg, entry, note, events_path=events_path)
return {"ok": True, "action": action}
+20 -9
View File
@@ -167,22 +167,33 @@ def _append_writeback_missed(events_path: str, hyp_id: str, name: str) -> None:
f.write(json.dumps(ev, ensure_ascii=False) + "\n")
def writeback_card_death(pipeline_db: str | None, entry: dict[str, Any],
cause: str | None,
def writeback_card_death(pipeline_db: str | None,
registry: dict[str, Any],
entry: dict[str, Any], cause: str | None,
events_path: str = "config/registry_events.jsonl") -> bool:
"""D5: 因子入墓园→回写假设卡片(graveyard+死因+因子名).
"""决议O:因子入墓园→卡片聚合回写(全死才埋).
卡 id=entry.hypothesis 以 hyp- 开头才回写(旧 H- 编号非卡片,静默跳过);
registry=全量注册表(调用方在手),反查卡下全部因子——部分死=卡留活跃池
(stderr 提示,非 miss 不附记 events);全 graveyard 才回写,death_reason=
「全灭:触发因子 {name} 入墓园:{cause}」.其余(P2-10 miss 附记/失败不阻断
转移/相对路径绝对化)语义不变.
pipeline_db 缺省走 env SANGUO_PIPELINE_DB→data/pipeline.db.失败不阻断
转移(只 stderr 告警)——注册表转移是正主,卡片回写是附属.
P2-10: 回写未命中不再零痕迹——库不在/卡不在库均 stderr+events 附记
death_writeback_missed(含卡 id+因子名);相对路径警告后 os.path.abspath
绝对化(不拒跑,落库位置明确);库不存在跳过回写不建空库
(pipeline_store._connect 首触自举会静默 CREATE,预检挡在连接前).
"""
hyp_id = str(entry.get("hypothesis") or "")
if not hyp_id.startswith("hyp-"):
return False
# 决议O 聚合:反查卡下全部因子,全死才埋
siblings = [e for e in registry["factors"].values()
if e.get("hypothesis") == hyp_id]
alive = [str(e.get("name")) for e in siblings
if e.get("status") != "graveyard"]
if alive:
print(f"[registry] ℹ️ 卡片 {hyp_id} 部分因子存活({len(alive)} 条),"
f"不埋卡(决议O 全死才埋): {', '.join(alive[:5])}",
file=sys.stderr)
return False
if pipeline_db is None:
pipeline_db = os.environ.get(
"SANGUO_PIPELINE_DB", os.path.join("data", "pipeline.db"))
@@ -201,7 +212,7 @@ def writeback_card_death(pipeline_db: str | None, entry: dict[str, Any],
from sanguo_portfolio import pipeline_store
result = pipeline_store.update_hypothesis_state(
pipeline_db, hyp_id, "graveyard",
death_reason=f"因子 {name} 入墓园: {cause or '未注死因'}",
death_reason=f"全灭:触发因子 {name} 入墓园: {cause or '未注死因'}",
factor_id=name)
except ValueError as e: # 卡片已终态(graveyard/promoted)等非法迁移
print(f"[registry] ⚠️ 卡片 {hyp_id} 已终态,回写跳过: {e}", file=sys.stderr)
@@ -292,7 +303,7 @@ def main(argv: list[str] | None = None) -> int:
ev = append_event(args.events, entry, old, args.status)
print(f"[registry] {args.name}: {old} → {args.status} (event ts={ev['ts']})")
if args.status == "graveyard":
writeback_card_death(args.pipeline_db, entry, args.cause,
writeback_card_death(args.pipeline_db, reg, entry, args.cause,
events_path=args.events)
if args.status == "promoted":
title, body = render_promotion_issue(args.name, entry)
+62 -5
View File
@@ -18,6 +18,7 @@ from sanguo_factor.version_registry import (
save_registry,
transition,
upsert_factor,
writeback_card_death,
)
@@ -221,8 +222,9 @@ def test_graveyard_transition_writes_back_card(tmp_path, monkeypatch):
def test_writeback_skips_non_card_hypothesis(tmp_path):
import sanguo_factor.version_registry as vr
assert vr.writeback_card_death(None, {"hypothesis": "H-2026-09-22-05",
"name": "f"}, "x") is False
assert vr.writeback_card_death(None, {"factors": {}},
{"hypothesis": "H-2026-09-22-05",
"name": "f"}, "x") is False
def _insert_card(db):
@@ -246,7 +248,8 @@ def test_writeback_card_not_in_db_leaves_trace(tmp_path, capsys):
_insert_card(db) # 库存在有表,但回写目标 hyp-missing 不在
events = str(tmp_path / "ev.jsonl")
rc = vr.writeback_card_death(
db, {"hypothesis": "hyp-missing", "name": "f_ghost"}, "死因",
db, {"factors": {}}, {"hypothesis": "hyp-missing",
"name": "f_ghost"}, "死因",
events_path=events)
assert rc is False
err = capsys.readouterr().err
@@ -267,7 +270,8 @@ def test_writeback_relative_pipeline_db_absolutized(tmp_path, monkeypatch, capsy
hyp = _insert_card(db)
events = str(tmp_path / "ev.jsonl")
rc = vr.writeback_card_death(
db, {"hypothesis": hyp, "name": "f_rel"}, "死因", events_path=events)
db, {"factors": {}}, {"hypothesis": hyp, "name": "f_rel"}, "死因",
events_path=events)
assert rc is True
err = capsys.readouterr().err
assert "相对路径" in err
@@ -284,7 +288,8 @@ def test_writeback_missing_db_skipped_not_created(tmp_path, capsys):
db = str(tmp_path / "sub" / "pipeline.db") # 连同父目录都不存在
events = str(tmp_path / "ev.jsonl")
rc = vr.writeback_card_death(
db, {"hypothesis": "hyp-404", "name": "f_lost"}, "死因",
db, {"factors": {}}, {"hypothesis": "hyp-404",
"name": "f_lost"}, "死因",
events_path=events)
assert rc is False
err = capsys.readouterr().err
@@ -332,3 +337,55 @@ def test_upsert_factor_description_built_once_not_overwritten(reg):
# 不传 description=条目无该键(存量因子形态)
upsert_factor(reg, "f2", "H-2")
assert "description" not in reg["factors"]["f2"]
class TestWritebackAggregation:
"""决议O:卡片死亡回写=全死才埋(任一因子死不再埋整卡)."""
def _card_db(self, tmp_path):
from sanguo_portfolio import pipeline_store as ps
db = str(tmp_path / "pipeline.db")
ps.insert_hypothesis(db, {
"title": "高管增持后跑赢", "logic": "x",
"expected_sign": "positive", "falsifiable": "y"})
with __import__("sqlite3").connect(db) as conn:
conn.execute(
"UPDATE hypotheses SET id='hyp-agg1' WHERE title='高管增持后跑赢'")
return db
def test_partial_death_keeps_card_alive(self, tmp_path, capsys):
db = self._card_db(tmp_path)
reg = {"factors": {
"fa_a": {"name": "fa_a", "hypothesis": "hyp-agg1",
"status": "graveyard"}, # 死的那个(触发者)
"fa_b": {"name": "fa_b", "hypothesis": "hyp-agg1",
"status": "incubating"},}} # 活着 → 不埋卡
ok = writeback_card_death(db, reg, reg["factors"]["fa_a"], "IC 两窗反号")
assert ok is False
out = capsys.readouterr().err
assert "部分因子存活" in out and "fa_b" in out
from sanguo_portfolio import pipeline_store as ps
card = [c for c in ps.list_hypotheses(db) if c["id"] == "hyp-agg1"][0]
assert card["state"] == "queued" # 卡留活跃池
def test_all_dead_buries_card_with_new_reason(self, tmp_path):
db = self._card_db(tmp_path)
reg = {"factors": {
"fa_a": {"name": "fa_a", "hypothesis": "hyp-agg1",
"status": "graveyard"},
"fa_b": {"name": "fa_b", "hypothesis": "hyp-agg1",
"status": "graveyard"},}} # 全灭 → 埋
ok = writeback_card_death(db, reg, reg["factors"]["fa_a"], "IC 两窗反号")
assert ok is True
from sanguo_portfolio import pipeline_store as ps
card = [c for c in ps.list_hypotheses(db) if c["id"] == "hyp-agg1"][0]
assert card["state"] == "graveyard"
assert card["deathReason"] == "全灭:触发因子 fa_a 入墓园: IC 两窗反号"
def test_zero_sibling_factor_buries(self, tmp_path):
"""卡下无其他因子(一卡一因子旧语境)→该因子死即全灭,照埋."""
db = self._card_db(tmp_path)
reg = {"factors": {
"fa_a": {"name": "fa_a", "hypothesis": "hyp-agg1",
"status": "graveyard"},}}
assert writeback_card_death(db, reg, reg["factors"]["fa_a"], "x") is True