证据接口重新渲染分析结论的引用改用组装、出口复核同一个函数:_analysis_rendered 按封存的结论走 evidence.resolve_ref(resolve_ref 加 display,按文档记下的单元格显示规则),结果表的格照结果表记的列类型、显示规则、标识列渲染,和组装时的 _resolve_cell 一致。以前不看列类型和显示规则,文本码值「1203」重渲染成「1,203」、ISO 日期时间带着 T,render_ok 为假,证据面板和导出页报假的「与按结论渲染的结果不一致」。片段上没记引用的(口径卡指标上的可比性、分析范围的编号)照旧按判定语渲染。测试:门店码、时段两种格在片段接口和导出页数据块里都 render_ok

This commit is contained in:
ThearchyHelios
2026-10-21 15:32:21 +08:00
parent 3b06202b15
commit 8a62826034
3 changed files with 70 additions and 14 deletions
+20 -12
View File
@@ -104,6 +104,7 @@ from app.db.base import SessionLocal
from app.db.models import DataSource, Run, RunEvent, Workflow
from app.engine import analysis_claims, direct_select, recompute
from app.engine.evidence import (
CELL_DISPLAY,
ENTITY_KINDS,
GUESS_NOTE,
GUESS_SCHEMA,
@@ -116,6 +117,7 @@ from app.engine.evidence import (
analysis_recheck,
build_catalog,
closest_entities,
doc_cell_display,
find_segment,
guess_sources,
input_eid,
@@ -126,6 +128,7 @@ from app.engine.evidence import (
query_entry_fields,
render_cell,
render_metric,
resolve_ref,
schema_column,
schema_table,
sql_tables,
@@ -1041,17 +1044,22 @@ def _analysis_cited(cite: dict[str, Any], result: dict[str, Any] | None) -> dict
return {"what": "phrase"}
def _analysis_rendered(cite: dict[str, Any], result: dict[str, Any]) -> str | None:
"""按封存的结论重新渲染这一处引用(判定语按文本格、具名标量和结果表的格按单元格的规则)。渲染不出来是 None。"""
try:
cited = _analysis_cited(cite, result)
if cited["what"] == "phrase":
return render_cell(result.get("phrase"), kind="text") if isinstance(result.get("phrase"), str) else None
if cited["what"] == "value":
return render_cell(cited["raw"], cite.get("conv"))
return render_cell(cited["raw"], cite.get("conv"), plain=cite.get("plain") is True)
except RenderError:
return None
def _analysis_rendered(seg: dict[str, Any], cite: dict[str, Any], entry: dict[str, Any], result: dict[str, Any],
doc: dict[str, Any]) -> str | None:
"""按封存的结论把这一处引用重新解析一遍,交回渲染出来的字(渲染不出来是 None)。和组装、出口复核同一个函数
(evidence.resolve_ref,按文档记下的那一版单元格显示规则):判定语按文本格,具名标量按单元格的规则,结果表的格照结果表
记的列类型(文本码值 "1203" 不加千分位)、显示规则(ISO 日期时间的 T 换成空格)、标识列不分千位。片段上没记引用的
(口径卡指标上的可比性、分析范围里的编号)按判定语渲染。"""
ref = seg.get("ref")
if not isinstance(ref, str) or not ref.strip():
phrase = result.get("phrase")
try:
return render_cell(phrase, kind="text") if isinstance(phrase, str) else None
except RenderError:
return None
alias = str(cite.get("alias") or entry.get("alias") or "")
again = resolve_ref(ref, {alias: {**entry, "result": result}}, display=doc_cell_display(doc) or CELL_DISPLAY)
return again.get("rendered") if again.get("status") == "resolved" else None
async def _analysis_chain(seg: dict[str, Any], cite: dict[str, Any], entry: dict[str, Any], doc: dict[str, Any],
@@ -1082,7 +1090,7 @@ async def _analysis_chain(seg: dict[str, Any], cite: dict[str, Any], entry: dict
in_ledger = sealed.trusted and (str(entry.get("node_id") or ""), artifact) in sealed.ledger \
if chained is None else sealed.trusted and chained
eid = make_eid(LEDGER_KIND, artifact or None, {})
rendered = _analysis_rendered(cite, result) if result is not None else None
rendered = _analysis_rendered(seg, cite, entry, result, doc) if result is not None else None
judged: dict[str, Any] = {
"step": "analysis", "alias": alias, "op": entry.get("op"), "mode": entry.get("mode"),
"method_label": method_label(entry.get("op"), entry.get("mode")),
+5 -2
View File
@@ -2106,8 +2106,11 @@ def resolve_support(ref: str, catalog: dict[str, Any], *, loader: Loader | _Snap
def resolve_ref(ref: str, catalog: dict[str, Any], *, cells_allowed: bool = True,
loader: Loader | _Snapshots | None = None) -> dict[str, Any]:
"""片段上记的 ref(m:gmv|万、i:week、v:Q1.r0.amount)→ Citation。证据接口点开片段时用它重新解析。"""
loader: Loader | _Snapshots | None = None, display: int | None = None) -> dict[str, Any]:
"""片段上记的 ref(m:gmv|万、i:week、v:Q1.r0.amount)→ Citation。证据接口点开片段时用它重新解析。display 是按哪一版
单元格显示规则渲染(文档记下的那一版,doc_cell_display;复核按它),不给按 loader 的(缺省当前版)。"""
if display is not None:
loader = _snapshots(loader, display=display)
return resolve_marker(_parse_ref(ref), catalog, cells_allowed=cells_allowed, loader=loader)
@@ -178,6 +178,51 @@ async def test_named_values_and_result_cells_point_at_what_they_cite(client):
assert cell["note"] == "该值由系统从分析结论的结果表中取出、按固定规则显示,未经模型转写"
#: 按门店拆的快照:门店码是文本("1203"),渲染时不加千分位
STORES: dict[str, Any] = {
"columns": ["store", "week", "gmv"], "column_types": {"store": "text", "week": "text", "gmv": "number"},
"rows": [["1203", "W41", 820], ["1203", "W42", 700], ["1305", "W41", 600], ["1305", "W42", 560],
["2201", "W41", 700], ["2201", "W42", 666]],
"sql": "SELECT store, week, SUM(gmv) AS gmv FROM orders GROUP BY store, week ORDER BY store, week",
"source": "shop", "truncated": False, "row_count": 6,
}
#: 按时段拆的快照:时段是 ISO 日期时间(带 T),文档按 2 版显示规则写成空格
SLOTS: dict[str, Any] = {
"columns": ["slot", "week", "gmv"], "column_types": {"slot": "text", "week": "text", "gmv": "number"},
"rows": [["2026-10-12T08:00:00", "W41", 820], ["2026-10-12T08:00:00", "W42", 700],
["2026-10-12T20:00:00", "W41", 600], ["2026-10-12T20:00:00", "W42", 560]],
"sql": "SELECT slot, week, SUM(gmv) AS gmv FROM orders GROUP BY slot, week ORDER BY slot, week",
"source": "shop", "truncated": False, "row_count": 4,
}
@pytest.mark.parametrize("snapshot, by, shown", [
(STORES, "store", "1203"),
(SLOTS, "slot", "2026-10-12 08:00:00"),
])
async def test_a_result_cell_rerenders_the_way_it_was_composed(client, monkeypatch, snapshot, by, shown):
"""证据接口按组装时同一套规则重新渲染结果表的格(evidence.resolve_ref,按文档记下的单元格显示规则):文本码值照结果表
记的列类型不加千分位,ISO 日期时间的 T 换成空格。以前不看列类型和显示规则,渲染成「1,203」「2026-10-12T08:00:00」,
render_ok 为假,证据面板和导出页报假的「与按结论渲染的结果不一致」。"""
import tests.test_analysis_evidence_api as here
monkeypatch.setattr(here, "SNAPSHOT", snapshot)
q = await artifact_store.put_json(snapshot, kind="query_snapshot")
result = await analyzed({"Q2": (snapshot, q)}, by=[by])
ref = f"v:A1.r0.{by}"
made = await sealed_run(f"{by} [[{ref}]] 变化 [[v:A1.r0.delta]]。[[see:A1]]", result=result)
segment = next(s for s in iter_segments(made["doc"]) if s.get("ref") == ref)
assert segment["text"] == shown
body = (await client.get(f"/api/runs/{made['run']}/evidence/segments/{segment['id']}")).json()
judged = body["chain"][0]
assert judged["rendered"] == shown and judged["render_ok"] is True
assert judged["cited"] == {"what": "cell", "row": 0, "column": by, "raw": snapshot["rows"][0][0]}
# 导出页的数据块是同一条链
data = data_block((await client.get(f"/api/runs/{made['run']}/report.html")).text)
chain = data["chains"][data["refs"][segment["id"]]]
assert chain[0]["step"] == "analysis" and chain[0]["render_ok"] is True
async def test_the_recompute_gets_the_sealed_inputs_by_hash(client, monkeypatch):
"""复算的 load 交工件原内容(不加出处、不改列的类型),只认封存范围内交回过的查询;别的一律当取不回。"""
seen: dict[str, Any] = {}