双盲复算:盲算触顶、按无法复算记时(_limit_text 那一路),把盲算交回的理由(渲染后的正文,已经过遮罩)截一段带进原因(「盲算已达每项最多查询次数(1 次);盲算交回的理由:…」),审批卡、缺口说明上看得到它查到了什么,没有理由的照旧。原值所在的格子落在「查询区间里这张表没有数据」的列上时不派盲算、按无法复算记;盲算交回的格子落在这样的列上时不判一致、按无法复算记;原因都写「查询区间里这张表没有数据(…)」

This commit is contained in:
ThearchyHelios
2026-10-20 10:33:34 +08:00
parent 6fa2e297db
commit a86991b57f
2 changed files with 81 additions and 6 deletions
+35 -1
View File
@@ -69,6 +69,7 @@ from app.core.events import EventType
from app.db.base import SessionLocal
from app.db.models import DataSource
from app.data import catalog_view
from app.data.coverage import NO_DATA_SHORT, no_data_of
from app.engine import deadline, recompute
from app.engine.approval import read_decision
from app.engine.context import NodeContext, NodeError, NodeRejected
@@ -635,6 +636,10 @@ async def _query_input(item: dict[str, Any], sources: _Sources, aliases: dict[st
hidden = await sources.hidden(artifact)
if column.lower() in hidden:
return {}, f"输入 {item['path']} 取自设置了遮罩的列,不能用于复算"
if (gap := no_data_of(snap, column)) is not None:
# 原值所在的那一格落在「查询区间里这张表没有数据」的列上(data/coverage.py):那个 0 不代表真的是 0,盲算查到 0
# 也说明不了一致,不白派一次盲算,按无法复算记
return {}, f"{NO_DATA_SHORT}(输入 {item['path']}:{gap.get('message') or ''})"
value = cell_value(item.get("value"))
if not _number(value):
return {}, f"输入 {item['path']} 不是数值"
@@ -1003,6 +1008,30 @@ def _settled(found: Mapping[str, Any]) -> bool:
return found.get("limit") == "max_queries" and found.get("value") is not None
#: 无法复算的原因里带上盲算交回的理由时最多截多少字(理由是渲染后的正文,已经过遮罩)
RATIONALE_CHARS = 160
def _rationale_tail(found: Mapping[str, Any]) -> str:
"""盲算触顶、按无法复算记时(_limit_text 那一路),它交回的理由截一段接在原因后面:审批卡、缺口说明上看得到它查到了
什么。没有理由的交回空串(原因照旧)。"""
text = " ".join(str(found.get("rationale") or "").split())
if not text:
return ""
return f";盲算交回的理由:{text[:RATIONALE_CHARS]}{'…' if len(text) > RATIONALE_CHARS else ''}"
def _no_data_cell(artifact: Any, column: Any, load: Callable[[str], Any]) -> dict[str, Any] | None:
"""这一格所在的列「查询区间里这张表没有数据」(查询快照的 no_data,data/coverage.py):那一条,不是为 None。"""
if not isinstance(artifact, str) or not artifact or not isinstance(column, str):
return None
try:
snapshot = load(artifact)
except (ValueError, OSError):
return None
return no_data_of(snapshot, column)
def _limit_text(limit: str | None, options: _Options) -> str:
"""盲算为什么停下,接在「盲算」后面读:「盲算已达每项最多查询次数(6 次)」。"""
if limit == "max_queries" and options.max_queries is not None:
@@ -1027,12 +1056,17 @@ def _compare(row: dict[str, Any], metric: dict[str, Any], results: dict[str, Any
row.update(status=UNCHECKED, reason="盲算没有执行")
return
if found.get("limit") and not _settled(found):
row.update(status=UNCHECKED, reason="盲算" + _limit_text(found["limit"], options))
row.update(status=UNCHECKED, reason="盲算" + _limit_text(found["limit"], options) + _rationale_tail(found))
return
if found.get("value") is None:
row.update(status=UNCHECKED, reason=found.get("reason") or "盲算没有交回可用的数值",
**{k: found[k] for k in ("cause", "run_seconds") if k in found}) # 运行时长上限将至(_blind)
return
if gap := _no_data_cell(found.get("artifact"), found.get("column"), load):
# 盲算交回的那一格落在「查询区间里这张表没有数据」的列上(data/coverage.py):那个 0 不代表真的是 0,拿它和原值
# 比一致不一致都说明不了什么,按无法复算记
row.update(status=UNCHECKED, reason=f"{NO_DATA_SHORT}(盲算查到的那一格:{gap.get('message') or ''})")
return
values[inp["path"]] = found["value"]
outcome = recompute.recompute(metric, recompute.Changes(inputs=values), recompute.Sources(
load=load, outputs=outputs, masked=masks, queries=queries))
+46 -5
View File
@@ -271,9 +271,11 @@ async def test_two_metrics_using_one_cell_are_checked_once(monkeypatch, shop):
async def test_hitting_a_limit_counts_as_unchecked(monkeypatch, shop):
"""查询次数用完了还没查到要交的那一格:按无法复算计入缺口;一个指标都没核成,产出是 unchecked,不是 agreed。"""
"""查询次数用完了还没查到要交的那一格:按无法复算计入缺口;一个指标都没核成,产出是 unchecked,不是 agreed。
盲算交回的理由截一段带进原因:审批卡、缺口说明上看得到它查到了什么。"""
name, _ = shop
Blind(monkeypatch, name, {"销售额": ([BLIND_TOTAL, RIGHT], "gmv")})
Blind(monkeypatch, name, {"销售额": ([BLIND_TOTAL, RIGHT], "gmv")},
rationale="只查到了不含退款的合计,还没来得及按支付日核对。")
run = await start(graph(name, RIGHT), options={"verify": {"dual_check": {"max_queries": 1}}})
assert run.status == "succeeded", run.error
evs = await events(run.id)
@@ -281,15 +283,16 @@ async def test_hitting_a_limit_counts_as_unchecked(monkeypatch, shop):
out = dual_output(run, evs)
assert out["status"] == "unchecked" and out["metrics"][0]["status"] == "unchecked"
assert out["counts"] == {"agreed": 0, "disagreed": 0, "unchecked": 1}
assert out["issuance_gaps"] == ["指标「销售额」无法复算:盲算已达每项最多查询次数(1 次)"]
reason = "盲算已达每项最多查询次数(1 次);盲算交回的理由:只查到了不含退款的合计,还没来得及按支付日核对。"
assert out["issuance_gaps"] == [f"指标「销售额」无法复算:{reason}"]
[said] = logs(evs, "dual_check_unchecked")
assert said["metrics"] == [{"id": "gmv", "name": "销售额", "reason": "盲算已达每项最多查询次数(1 次)"}]
assert said["metrics"] == [{"id": "gmv", "name": "销售额", "reason": reason}]
assert not logs(evs, "dual_check_agreed")
assert not [e for e in evs if e.type == "human.requested"]
[finished] = [e for e in evs if e.type == "node.finished" and e.node_id == "dual"]
assert finished.data["evidence"][0]["status"] == "unchecked"
assert run.output["_issuance"]["tier"] == "degraded"
assert run.output["_issuance"]["gaps"] == ["双盲复算「复核销售额」:指标「销售额」无法复算:盲算已达每项最多查询次数(1 次)"]
assert run.output["_issuance"]["gaps"] == [f"双盲复算「复核销售额」:指标「销售额」无法复算:{reason}"]
async def test_an_answer_on_the_last_allowed_query_still_counts(monkeypatch, shop):
@@ -1575,3 +1578,41 @@ async def test_the_report_catalog_leaves_out_the_blind_queries(monkeypatch, shop
if e.type == "tool.end" and e.node_id == "dual" and e.data.get("query_artifact")]
ledger = [entry for e in evs if e.type == "node.finished" for entry in e.data.get("evidence") or []]
assert blind_queries and not any(entry.get("artifact") in blind_queries for entry in ledger)
# --------------------------------------------------------------------------
# 查询区间里这张表没有数据(data/coverage.py):不判一致,按无法复算记
# --------------------------------------------------------------------------
#: 订单表只有 2026 年的数:2030 年那一周表里没有数据,COUNT 是 0
EMPTY_WEEK = "BETWEEN '2030-01-07' AND '2030-01-13'"
NO_ORDERS = f"SELECT COUNT(*) AS orders FROM orders WHERE status <> 'refunded' AND paid_date {EMPTY_WEEK}"
ORDERS = {"id": "orders", "name": "订单数", "unit": "单", "expression": "cell(nodes.fetch, 0, 'orders')"}
async def test_an_original_cell_without_data_is_not_checked(monkeypatch, shop):
"""原值所在的格子落在 no_data 的列上:那个 0 不代表真的是 0,盲算查到 0 也说明不了一致。不派盲算,按无法复算记。"""
name, _ = shop
blind = Blind(monkeypatch, name, {"订单数": (NO_ORDERS, "orders")})
run = await start(graph(name, NO_ORDERS, metrics=[ORDERS], dual={"definitions": {"orders": "已支付订单数"}}))
assert run.status == "succeeded", run.error
evs = await events(run.id)
out = dual_output(run, evs)
[row] = out["metrics"]
assert row["status"] == "unchecked" and row["reason"].startswith("查询区间里这张表没有数据(输入 cell(nodes.fetch")
assert "2030-01-07 到 2030-01-13" in row["reason"]
assert "step" not in blind.calls, "原值就没有数据:不白派一次盲算"
assert out["counts"] == {"agreed": 0, "disagreed": 0, "unchecked": 1}
async def test_a_blind_cell_without_data_is_not_agreed(monkeypatch, shop):
"""盲算交回的格子落在 no_data 的列上(它查了一段表里没有数据的区间):不判一致,按无法复算记。"""
name, _ = shop
buyers = {"id": "orders", "name": "订单数", "unit": "单", "expression": "cell(nodes.fetch, 0, 'orders')"}
original = f"SELECT COUNT(*) AS orders FROM orders WHERE status <> 'refunded' AND paid_date {WEEK}"
Blind(monkeypatch, name, {"订单数": (NO_ORDERS.replace("AS orders", "AS n"), "n")})
run = await start(graph(name, original, metrics=[buyers], dual={"definitions": {"orders": "已支付订单数"}}))
assert run.status == "succeeded", run.error
[row] = dual_output(run, await events(run.id))["metrics"]
assert row["status"] == "unchecked", row
assert row["reason"].startswith("查询区间里这张表没有数据(盲算查到的那一格:「n」查的是 2030-01-07 到 2030-01-13")