出具缺口不再把节点类型说两遍:上游缺口的前缀在节点名就是类型叫法时只写一次(「反方审稿:」),双盲复算、反方审稿的缺口正文去掉自带的类型(「推翻了 1 句结论:…」「指标「X」两路结果不一致,审核人选择了盲算结果」);时间线和报错里那句照旧带主语

This commit is contained in:
ThearchyHelios
2026-10-05 18:45:59 +08:00
parent 67c7a42a3c
commit 64b6586ecc
6 changed files with 57 additions and 17 deletions
+9 -3
View File
@@ -466,9 +466,15 @@ def _named(claims: list[dict[str, Any]]) -> str:
return names + ("等" if len(claims) > _NAMED else "")
def _refuted_gap(claims: list[dict[str, Any]]) -> str:
"""出具缺口里的那句:「推翻了 N 句结论:「…」」。不带节点名也不带「反方审稿」:出具契约统一加「反方审稿「节点名」:」
(没改名的节点只加「反方审稿:」),正文再说一遍就成了「反方审稿:反方审稿推翻了……」。"""
return f"推翻了 {len(claims)} 句结论:{_named(claims)}"
def _refuted_words(claims: list[dict[str, Any]]) -> str:
"""出具缺口、时间线、报错里的那句:「反方审稿推翻了 N 句结论:「…」」。不带节点名:出具契约统一加前缀。"""
return f"反方审稿推翻了 {len(claims)} 句结论:{_named(claims)}"
"""时间线、报错里的那句:「反方审稿推翻了 N 句结论:「…」」。它们不经出具契约加前缀,主语得自己带上。"""
return f"反方审稿{_refuted_gap(claims)}"
def pick_items(claims: list[dict[str, Any]]) -> list[dict[str, Any]]:
@@ -676,7 +682,7 @@ def _finish(ctx: NodeContext, state: GraphState, outcome: dict[str, Any], *, on_
result: dict[str, Any] = {"challenge_artifact": outcome.get("artifact"), "counts": outcome.get("counts") or {},
"items": pick_items(claims), "on_refuted": on_refuted}
if refuted and on_refuted == "degrade":
result["issuance_gaps"] = [_refuted_words(refuted)]
result["issuance_gaps"] = [_refuted_gap(refuted)]
updates: dict[str, Any] = {"nodes": {ctx.node.id: result}}
spent = outcome.get("usage") or {}
if spent.get("calls"):
+5 -4
View File
@@ -110,10 +110,11 @@ SAME_SQL_ASPECT = "data"
DIFF_TIMEOUT = "两条 SQL 比较超时,未能归纳成具体差异,请对照原文"
DIFF_FAILED = "两条 SQL 比较时出错,未能归纳成具体差异,请对照原文"
NO_SQL = "有一边没有可比较的 SQL,请对照原文"
#: 交给成果节点的缺口(不带节点名:出具契约统一加「双盲复算「节点名」:」)
GAP_BLIND = "指标「{name}」双盲复算不一致,审核人选择了盲算结果"
GAP_NEITHER = "指标「{name}」双盲复算不一致,审核人判断两边都不对"
GAP_UNANSWERED = "指标「{name}」双盲复算不一致,审核人没有作出选择"
#: 交给成果节点的缺口。不带节点名,也不再说「双盲复算」:出具契约统一加「双盲复算「节点名」:」(没改名的节点只加
#: 「双盲复算:」),正文再说一遍就成了「双盲复算:指标「X」双盲复算不一致……」
GAP_BLIND = "指标「{name}」两路结果不一致,审核人选择了盲算结果"
GAP_NEITHER = "指标「{name}」两路结果不一致,审核人判断两边都不对"
GAP_UNANSWERED = "指标「{name}」两路结果不一致,审核人没有作出选择"
GAP_UNCHECKED = "指标「{name}」无法复算:{reason}"
BLIND_SYSTEM = (
+4 -2
View File
@@ -322,7 +322,8 @@ def _upstream_issuance_gaps(state: GraphState, ctx: NodeContext) -> list[str]:
这是给「把关类」节点留的通用口子:节点觉得这次的结论不该完整出具(双盲复算两路对不上、
反方审稿的反驳没有回应),在自己的产出里写 issuance_gaps,成果节点的出具契约照单收进缺口,
出具随之降档。写的时候不用带节点名,这里统一加上「双盲复算「复核销售额」:」这样的前缀,
和别的缺口一样读得出是谁报的。
和别的缺口一样读得出是谁报的。节点名就是类型的叫法(拖进画布没改名)时只写一次:「反方审稿:」,
不写成「反方审稿「反方审稿」:」。缺口正文也不必再提节点类型,前缀已经说了。
只认这个成果节点的上游:另一条支路上的节点管不到这份成果。不是字符串、空白的条目跳过——
这个键谁都能写(数据整形的 JSON 模板也行),写坏了只当没写,不能让出具崩掉。
@@ -340,7 +341,8 @@ def _upstream_issuance_gaps(state: GraphState, ctx: NodeContext) -> list[str]:
reported = payload.get("issuance_gaps")
if not isinstance(reported, list):
continue
prefix = f"{type_label(node.type)}「{node.title}」:"
kind = type_label(node.type)
prefix = f"{kind}:" if node.title == kind else f"{kind}「{node.title}」:"
out.extend(prefix + gap.strip() for gap in reported if isinstance(gap, str) and gap.strip())
return out
+16 -3
View File
@@ -265,7 +265,7 @@ async def test_a_refuted_conclusion_degrades_the_issuance(monkeypatch, shop):
issuance = row.output["_issuance"]
assert issuance["tier"] == "degraded"
assert issuance["gaps"] == [f"反方审稿「审稿」:反方审稿推翻了 1 句结论:「{SOUTH}」"], "缺口不带节点名,前缀由出具契约加"
assert issuance["gaps"] == [f"反方审稿「审稿」:推翻了 1 句结论:「{SOUTH}」"], "缺口不带节点名,前缀由出具契约加"
doc = await report_doc(row.id)
east, south = unit_of(doc, EAST), unit_of(doc, SOUTH)
@@ -275,7 +275,7 @@ async def test_a_refuted_conclusion_degrades_the_issuance(monkeypatch, shop):
assert out["items"] == [{"unit": east, "text": EAST, "verdict": "weakened"},
{"unit": south, "text": SOUTH, "verdict": "refuted"}]
assert out["on_refuted"] == "degrade"
assert out["issuance_gaps"] == [f"反方审稿推翻了 1 句结论:「{SOUTH}」"]
assert out["issuance_gaps"] == [f"推翻了 1 句结论:「{SOUTH}」"]
record = artifact_store.load(out["challenge_artifact"])
assert set(record) == {"report", "doc_artifact", "claims", "model"}
@@ -317,6 +317,19 @@ async def test_a_refuted_conclusion_degrades_the_issuance(monkeypatch, shop):
assert finished.seq <= row.manifest_seq
async def test_a_challenge_named_after_its_type_is_named_once_in_the_gap(monkeypatch, shop):
"""拖进画布没改名的反方审稿:缺口读成「反方审稿:推翻了 1 句结论:…」,前缀和正文都不再重复「反方审稿」"""
Models(monkeypatch, shop)
spec = graph(shop)
next(n for n in spec["nodes"] if n["id"] == "chal")["data"]["label"] = "反方审稿"
row = await finish(spec)
assert row.status == "succeeded", row.error
assert row.output["_issuance"]["gaps"] == [f"反方审稿:推翻了 1 句结论:「{SOUTH}」"]
# 时间线、报错里那一句照旧带主语:它们不经出具契约加前缀
[refuted] = await logs(row.id, "challenge_refuted")
assert refuted["message"] == f"反方审稿推翻了 1 句结论:「{SOUTH}」"
async def test_flag_only_records_and_fail_stops_the_run(monkeypatch, shop):
Models(monkeypatch, shop)
row = await finish(graph(shop, on_refuted="flag"))
@@ -843,7 +856,7 @@ async def test_a_fail_that_would_be_swallowed_degrades_instead(monkeypatch, shop
row = await finish(graph(shop, on_refuted="fail", on_error="continue"))
assert row.status == "succeeded", row.error
out = await output_of(row.id, "chal")
assert out["on_refuted"] == "degrade" and out["issuance_gaps"] == [f"反方审稿推翻了 1 句结论:「{SOUTH}」"]
assert out["on_refuted"] == "degrade" and out["issuance_gaps"] == [f"推翻了 1 句结论:「{SOUTH}」"]
assert row.output["_issuance"]["tier"] == "degraded"
+17 -5
View File
@@ -446,13 +446,25 @@ async def test_choosing_the_blind_result_degrades_and_writes_the_catalog(monkeyp
done = await decide(client, run.id, {"gmv": "b"})
assert done.status == "succeeded", done.error
out = dual_output(done, await events(run.id))
assert out["issuance_gaps"] == ["指标「销售额」双盲复算不一致,审核人选择了盲算结果"]
assert out["issuance_gaps"] == ["指标「销售额」两路结果不一致,审核人选择了盲算结果"]
issuance = done.output["_issuance"]
assert issuance["tier"] == "degraded"
assert issuance["gaps"] == ["双盲复算「复核销售额」:指标「销售额」双盲复算不一致,审核人选择了盲算结果"]
assert issuance["gaps"] == ["双盲复算「复核销售额」:指标「销售额」两路结果不一致,审核人选择了盲算结果"]
assert (await notes_of(source_id)).notes["business_date"]["value"] == {"column": "paid_date"}
async def test_a_dual_check_named_after_its_type_is_named_once_in_the_gap(monkeypatch, shop, client):
"""拖进画布没改名的双盲复算:缺口读成「双盲复算:指标「销售额」两路结果不一致……」,不写两遍「双盲复算」"""
name, _ = shop
Blind(monkeypatch, name, {"销售额": (RIGHT, "gmv")})
spec = graph(name, BY_ORDER_DATE)
next(n for n in spec["nodes"] if n["id"] == "dual")["data"]["label"] = "双盲复算"
run = await start(spec)
done = await decide(client, run.id, {"gmv": "neither"})
assert done.status == "succeeded", done.error
assert done.output["_issuance"]["gaps"] == ["双盲复算:指标「销售额」两路结果不一致,审核人判断两边都不对"]
async def test_neither_degrades_and_writes_nothing(monkeypatch, shop, client):
name, source_id = shop
Blind(monkeypatch, name, {"销售额": (RIGHT, "gmv")})
@@ -461,7 +473,7 @@ async def test_neither_degrades_and_writes_nothing(monkeypatch, shop, client):
assert done.status == "succeeded", done.error
evs = await events(run.id)
out = dual_output(done, evs)
assert out["issuance_gaps"] == ["指标「销售额」双盲复算不一致,审核人判断两边都不对"]
assert out["issuance_gaps"] == ["指标「销售额」两路结果不一致,审核人判断两边都不对"]
assert done.output["_issuance"]["tier"] == "degraded"
assert not logs(evs, "dual_check_catalog") and await notes_of(source_id) is None
@@ -476,7 +488,7 @@ async def test_a_missing_choice_is_recorded_as_no_choice(monkeypatch, shop, clie
evs = await events(run.id)
out = dual_output(done, evs)
assert out["choices"] == {} and "choice" not in out["metrics"][0]
assert out["issuance_gaps"] == ["指标「销售额」双盲复算不一致,审核人没有作出选择"]
assert out["issuance_gaps"] == ["指标「销售额」两路结果不一致,审核人没有作出选择"]
[resolved] = logs(evs, "dual_check_resolved")
assert resolved["choices"] == {} and resolved["gaps"] == out["issuance_gaps"]
assert "没有作出选择" in resolved["message"] and "两边都不对" not in resolved["message"]
@@ -505,7 +517,7 @@ async def test_on_reject_fail_stops_the_run(monkeypatch, shop, client):
assert done.status == "failed" and "审核人没有采用原流程的结果" in (done.error or "") and "销售额" in done.error
evs = await events(run.id)
assert [d["gaps"] for d in logs(evs, "dual_check_resolved")] == [
["指标「销售额」双盲复算不一致,审核人选择了盲算结果"]]
["指标「销售额」两路结果不一致,审核人选择了盲算结果"]]
# 审核人的选择照样写进了目录:那是对数据的判断,和运行停不停无关
entry = await notes_of(source_id)
assert entry.notes["business_date"]["value"] == {"column": "paid_date"} and entry.version == 1
@@ -93,3 +93,9 @@ async def test_without_issuance_gaps_nothing_changes():
issuance = row.output["_issuance"]
assert issuance["tier"] == "formal"
assert issuance["gaps"] == []
async def test_a_node_named_after_its_type_is_named_once():
"""节点名就是类型的叫法(拖进画布没改名的):前缀只写一次「数据整形:」,不写成「数据整形「数据整形」:」"""
row = await finish(graph(gaps_node("recheck", "数据整形", ["两路结果不一致"])))
assert row.output["_issuance"]["gaps"] == ["数据整形:两路结果不一致"]